Skip to content

Commit 10f5046

Browse files
update in oss to make enterprise cli interactive
1 parent 8ab9f6b commit 10f5046

2 files changed

Lines changed: 217 additions & 76 deletions

File tree

src/interactive.rs

Lines changed: 216 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ use std::io::{self, BufRead, IsTerminal, Write};
2020
#[cfg(unix)]
2121
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
2222
use std::path::PathBuf;
23+
use std::sync::atomic::{AtomicBool, Ordering};
2324

2425
const ENV_FILE_NAME: &str = ".parseable.env";
2526

27+
/// Guard to avoid parsing `.parseable.env` more than once per process.
28+
static ENV_FILE_LOADED: AtomicBool = AtomicBool::new(false);
29+
2630
/// A required or optional env var that the user may need to provide.
2731
struct EnvPrompt {
2832
env_var: &'static str,
@@ -62,82 +66,12 @@ pub fn prompt_missing_envs() -> Vec<(String, String)> {
6266
// checks which env vars are already set.
6367
load_env_file();
6468

65-
let prompts = get_env_prompts(&subcommand);
66-
if prompts.is_empty() {
67-
return vec![];
68-
}
69-
70-
// Collect which required envs are still missing after loading the env file
71-
let missing: Vec<&EnvPrompt> = prompts
72-
.iter()
73-
.filter(|p| p.required && std::env::var(p.env_var).is_err())
74-
.collect();
75-
76-
if missing.is_empty() {
77-
return vec![];
78-
}
79-
80-
// Only prompt if stdin is an interactive terminal
81-
if !io::stdin().is_terminal() {
82-
return vec![];
83-
}
84-
85-
println!();
86-
println!(" Missing required environment variable(s) for {subcommand}:");
87-
for m in &missing {
88-
println!(" - {} ({})", m.env_var, m.display_name);
89-
}
90-
println!();
91-
println!(" Starting interactive setup...");
92-
println!();
93-
94-
// Track values collected in this session
69+
let is_interactive = io::stdin().is_terminal();
9570
let mut collected: Vec<(String, String)> = Vec::new();
9671

97-
// Prompt for ALL env vars (required and optional) that are not yet set
98-
for prompt in &prompts {
99-
if std::env::var(prompt.env_var).is_ok() {
100-
continue;
101-
}
102-
103-
let tag = if prompt.required {
104-
"required"
105-
} else {
106-
"optional, press Enter to skip"
107-
};
108-
109-
let value = if prompt.is_secret {
110-
prompt_secret(&format!(
111-
" {} ({}) [{}]: ",
112-
prompt.display_name, prompt.env_var, tag
113-
))
114-
} else {
115-
prompt_line(&format!(
116-
" {} ({}) [{}]: ",
117-
prompt.display_name, prompt.env_var, tag
118-
))
119-
};
120-
121-
let value = value.trim().to_string();
122-
123-
if value.is_empty() {
124-
if prompt.required {
125-
eprintln!(
126-
" Error: {} is required and cannot be empty. Exiting.",
127-
prompt.env_var
128-
);
129-
std::process::exit(1);
130-
}
131-
continue;
132-
}
133-
134-
// SAFETY: This runs single-threaded during startup, before any async
135-
// runtime or additional threads are spawned.
136-
unsafe { std::env::set_var(prompt.env_var, &value) };
137-
collected.push((prompt.env_var.to_string(), value));
138-
}
72+
let prompts = get_env_prompts(&subcommand);
73+
collect_prompts(&prompts, is_interactive, &subcommand, &mut collected);
13974

140-
println!();
14175
collected
14276
}
14377

@@ -182,7 +116,11 @@ fn env_file_path() -> PathBuf {
182116

183117
/// Loads env vars from `.parseable.env` if it exists.
184118
/// Format: KEY=VALUE per line, # comments and empty lines are skipped.
185-
fn load_env_file() {
119+
/// Safe to call multiple times — the file is only parsed once per process.
120+
pub fn load_env_file() {
121+
if ENV_FILE_LOADED.swap(true, Ordering::Relaxed) {
122+
return;
123+
}
186124
let path = env_file_path();
187125
let file = match std::fs::File::open(&path) {
188126
Ok(f) => f,
@@ -292,7 +230,7 @@ fn detect_storage_subcommand() -> Option<String> {
292230
}
293231

294232
/// Returns the list of env var prompts for the given storage subcommand,
295-
/// including any conditionally-required groups like OIDC.
233+
/// including any conditionally-required groups like OIDC and mode-specific vars.
296234
fn get_env_prompts(subcommand: &str) -> Vec<EnvPrompt> {
297235
let mut prompts = get_storage_prompts(subcommand);
298236
prompts.extend(get_tls_prompts());
@@ -302,6 +240,29 @@ fn get_env_prompts(subcommand: &str) -> Vec<EnvPrompt> {
302240
prompts
303241
}
304242

243+
/// Detects the server mode from the `P_MODE` env var or the `--mode` CLI arg.
244+
fn detect_mode() -> Option<String> {
245+
if let Ok(mode) = std::env::var("P_MODE") {
246+
let mode = mode.to_lowercase();
247+
if mode != "all" {
248+
return Some(mode);
249+
}
250+
return None;
251+
}
252+
253+
let args: Vec<String> = std::env::args().collect();
254+
for (i, arg) in args.iter().enumerate() {
255+
if arg == "--mode" {
256+
return args.get(i + 1).map(|v| v.to_lowercase());
257+
}
258+
if let Some(value) = arg.strip_prefix("--mode=") {
259+
return Some(value.to_lowercase());
260+
}
261+
}
262+
263+
None
264+
}
265+
305266
/// Returns storage-specific env var prompts for the given subcommand.
306267
fn get_storage_prompts(subcommand: &str) -> Vec<EnvPrompt> {
307268
match subcommand {
@@ -529,6 +490,186 @@ fn get_kafka_prompts() -> Vec<EnvPrompt> {
529490
prompts
530491
}
531492

493+
/// Checks for missing enterprise-specific environment variables and
494+
/// prompts for them interactively if running in a terminal.
495+
///
496+
/// Must be called **before** `PARSEABLE` is accessed and before license
497+
/// verification, so that `.parseable.env` values are loaded and the user
498+
/// gets a chance to provide missing vars like `P_CLUSTER_SECRET` and
499+
/// the license file paths.
500+
///
501+
/// Returns collected `(env_var, value)` pairs. Call [`save_collected_envs`]
502+
/// after validation succeeds to persist them.
503+
pub fn prompt_enterprise_envs() -> Vec<(String, String)> {
504+
if is_help_or_version_request() {
505+
return vec![];
506+
}
507+
508+
if detect_storage_subcommand().is_none() {
509+
return vec![];
510+
}
511+
512+
// Load previously saved env vars so we pick up P_CLUSTER_SECRET etc.
513+
load_env_file();
514+
515+
let is_interactive = io::stdin().is_terminal();
516+
let mut collected: Vec<(String, String)> = Vec::new();
517+
518+
// Phase 1: base enterprise vars (license, cluster secret, mode)
519+
let base_prompts = get_enterprise_base_prompts();
520+
collect_prompts(
521+
&base_prompts,
522+
is_interactive,
523+
"Parseable Enterprise",
524+
&mut collected,
525+
);
526+
527+
// Phase 2: now P_MODE is in the environment (from env, CLI, .parseable.env,
528+
// or the interactive prompt above), so mode-specific prompts resolve correctly.
529+
let mode_prompts = get_enterprise_mode_prompts();
530+
if !mode_prompts.is_empty() {
531+
let mode = detect_mode().unwrap_or_default();
532+
collect_prompts(
533+
&mode_prompts,
534+
is_interactive,
535+
&format!("{mode} mode"),
536+
&mut collected,
537+
);
538+
}
539+
540+
collected
541+
}
542+
543+
/// Collects missing required env vars from the given prompt list.
544+
/// Prints a header and prompts interactively when running in a terminal.
545+
fn collect_prompts(
546+
prompts: &[EnvPrompt],
547+
is_interactive: bool,
548+
context: &str,
549+
collected: &mut Vec<(String, String)>,
550+
) {
551+
let missing: Vec<&EnvPrompt> = prompts
552+
.iter()
553+
.filter(|p| p.required && std::env::var(p.env_var).is_err())
554+
.collect();
555+
556+
if missing.is_empty() {
557+
return;
558+
}
559+
560+
if !is_interactive {
561+
return;
562+
}
563+
564+
println!();
565+
println!(" Missing required environment variable(s) for {context}:");
566+
for m in &missing {
567+
println!(" - {} ({})", m.env_var, m.display_name);
568+
}
569+
println!();
570+
println!(" Starting interactive setup...");
571+
println!();
572+
573+
for prompt in prompts {
574+
if std::env::var(prompt.env_var).is_ok() {
575+
continue;
576+
}
577+
578+
let tag = if prompt.required {
579+
"required"
580+
} else {
581+
"optional, press Enter to skip"
582+
};
583+
584+
let value = if prompt.is_secret {
585+
prompt_secret(&format!(
586+
" {} ({}) [{}]: ",
587+
prompt.display_name, prompt.env_var, tag
588+
))
589+
} else {
590+
prompt_line(&format!(
591+
" {} ({}) [{}]: ",
592+
prompt.display_name, prompt.env_var, tag
593+
))
594+
};
595+
596+
let value = value.trim().to_string();
597+
598+
if value.is_empty() {
599+
if prompt.required {
600+
eprintln!(
601+
" Error: {} is required and cannot be empty. Exiting.",
602+
prompt.env_var
603+
);
604+
std::process::exit(1);
605+
}
606+
continue;
607+
}
608+
609+
// SAFETY: Single-threaded startup, no other threads running.
610+
unsafe { std::env::set_var(prompt.env_var, &value) };
611+
collected.push((prompt.env_var.to_string(), value));
612+
}
613+
614+
println!();
615+
}
616+
617+
/// Returns base enterprise env var prompts (license, cluster secret, mode).
618+
fn get_enterprise_base_prompts() -> Vec<EnvPrompt> {
619+
let mut prompts = vec![
620+
EnvPrompt {
621+
env_var: "P_LICENSE_DATA_FILE_PATH",
622+
display_name: "License Data File Path",
623+
required: true,
624+
is_secret: false,
625+
},
626+
EnvPrompt {
627+
env_var: "P_LICENSE_SIGNATURE_FILE_PATH",
628+
display_name: "License Signature File Path",
629+
required: true,
630+
is_secret: false,
631+
},
632+
EnvPrompt {
633+
env_var: "P_CLUSTER_SECRET",
634+
display_name: "Cluster Secret",
635+
required: true,
636+
is_secret: true,
637+
},
638+
];
639+
640+
// Enterprise rejects "all" mode, so prompt if not explicitly set
641+
if detect_mode().is_none() {
642+
prompts.push(EnvPrompt {
643+
env_var: "P_MODE",
644+
display_name: "Server Mode (query, ingest, index, prism)",
645+
required: true,
646+
is_secret: false,
647+
});
648+
}
649+
650+
prompts
651+
}
652+
653+
/// Returns mode-specific enterprise env var prompts.
654+
/// Called after phase 1, so P_MODE is guaranteed to be in the environment.
655+
fn get_enterprise_mode_prompts() -> Vec<EnvPrompt> {
656+
match detect_mode().as_deref() {
657+
Some("query") => vec![EnvPrompt {
658+
env_var: "P_HOT_TIER_DIR",
659+
display_name: "Hot Tier Directory Path",
660+
required: true,
661+
is_secret: false,
662+
}],
663+
Some("index") => vec![EnvPrompt {
664+
env_var: "P_INDEX_DIR",
665+
display_name: "Index Storage Directory Path",
666+
required: true,
667+
is_secret: false,
668+
}],
669+
_ => vec![],
670+
}
671+
}
672+
532673
/// Prompts the user for a line of input (visible).
533674
fn prompt_line(prompt: &str) -> String {
534675
print!("{prompt}");

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub mod enterprise;
2929
pub mod event;
3030
pub mod handlers;
3131
pub mod hottier;
32-
mod interactive;
32+
pub mod interactive;
3333
mod livetail;
3434
mod metadata;
3535
pub mod metastore;

0 commit comments

Comments
 (0)