@@ -20,9 +20,13 @@ use std::io::{self, BufRead, IsTerminal, Write};
2020#[ cfg( unix) ]
2121use std:: os:: unix:: fs:: { OpenOptionsExt , PermissionsExt } ;
2222use std:: path:: PathBuf ;
23+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
2324
2425const 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.
2731struct 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 .
296234fn 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.
306271fn 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).
533678fn prompt_line ( prompt : & str ) -> String {
534679 print ! ( "{prompt}" ) ;
0 commit comments