Skip to content

Commit 79656f6

Browse files
update in oss to make enterprise cli interactive (#1612)
* update in oss to make enterprise cli interactive * coderabbit comments fix
1 parent 8ab9f6b commit 79656f6

2 files changed

Lines changed: 221 additions & 76 deletions

File tree

src/interactive.rs

Lines changed: 220 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,33 @@ fn get_env_prompts(subcommand: &str) -> Vec<EnvPrompt> {
302240
prompts
303241
}
304242

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

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