@@ -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,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.
306267fn 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).
533674fn prompt_line ( prompt : & str ) -> String {
534675 print ! ( "{prompt}" ) ;
0 commit comments