2222
2323import  com .fasterxml .jackson .databind .JsonMappingException ;
2424import  com .fasterxml .jackson .databind .JsonNode ;
25+ import  com .fasterxml .jackson .databind .exc .UnrecognizedPropertyException ;
2526import  com .fasterxml .jackson .databind .node .ObjectNode ;
2627import  com .fasterxml .jackson .dataformat .yaml .YAMLMapper ;
28+ import  com .google .common .annotations .VisibleForTesting ;
29+ import  org .slf4j .Logger ;
30+ import  org .slf4j .LoggerFactory ;
2731
2832import  javax .annotation .Nullable ;
2933import  java .io .IOException ;
3438import  java .util .Optional ;
3539
3640import  static  java .nio .charset .StandardCharsets .UTF_8 ;
41+ import  static  java .util .Objects .requireNonNull ;
42+ import  static  org .fusesource .jansi .Ansi .ansi ;
3743
3844public  record  CliConfig (Map <String , CliConfigContext > contexts ) {
3945
40-     public  static  CliConfig  load (Path  path ) throws  IOException  {
46+     private  static  final  Logger  log  = LoggerFactory .getLogger (CliConfig .class );
47+ 
48+     public  static  CliConfig .CliConfigContext  load (Verbosity  verbosity , String  context , Overrides  overrides ) {
49+         Path  baseDir  = Paths .get (System .getProperty ("user.home" ), ".concord" );
50+         Path  cfgFile  = baseDir .resolve ("cli.yaml" );
51+         if  (!Files .exists (cfgFile )) {
52+             cfgFile  = baseDir .resolve ("cli.yml" );
53+         }
54+         if  (!Files .exists (cfgFile )) {
55+             CliConfig  cfg  = CliConfig .create ();
56+             return  assertCliConfigContext (cfg , context ).withOverrides (overrides );
57+         }
58+ 
59+         if  (verbosity .verbose ()) {
60+             log .info ("Using CLI configuration file: {} (\" {}\"  context)" , cfgFile , context );
61+         }
62+ 
63+         try  {
64+             CliConfig  cfg  = loadConfigFile (cfgFile );
65+             return  assertCliConfigContext (cfg , context ).withOverrides (overrides );
66+         } catch  (Exception  e ) {
67+             handleCliConfigErrorAndBail (cfgFile .toAbsolutePath ().toString (), e );
68+             throw  new  IllegalStateException ("should be unreachable" );
69+         }
70+     }
71+ 
72+     private  static  void  handleCliConfigErrorAndBail (String  cfgPath , Throwable  e ) {
73+         // unwrap runtime exceptions 
74+         if  (e  instanceof  RuntimeException  ex ) {
75+             if  (ex .getCause () instanceof  IllegalArgumentException ) {
76+                 e  = ex .getCause ();
77+             }
78+         }
79+ 
80+         // handle YAML errors 
81+         if  (e  instanceof  IllegalArgumentException ) {
82+             if  (e .getCause () instanceof  UnrecognizedPropertyException  ex ) {
83+                 System .out .println (ansi ().fgRed ().a ("Invalid format of the CLI configuration file " ).a (cfgPath ).a (". " ).a (ex .getMessage ()));
84+                 System .exit (1 );
85+             }
86+             System .out .println (ansi ().fgRed ().a ("Invalid format of the CLI configuration file " ).a (cfgPath ).a (". " ).a (e .getMessage ()));
87+             System .exit (1 );
88+         }
89+ 
90+         // all other errors 
91+         System .out .println (ansi ().fgRed ().a ("Failed to read the CLI configuration file " ).a (cfgPath ).a (". " ).a (e .getMessage ()));
92+         System .exit (1 );
93+     }
94+ 
95+     private  static  CliConfig .CliConfigContext  assertCliConfigContext (CliConfig  config , String  context ) {
96+         CliConfig .CliConfigContext  result  = config .contexts ().get (context );
97+         if  (result  == null ) {
98+             System .out .println (ansi ().fgRed ().a ("Configuration context not found: " ).a (context ).a (". Check the CLI configuration file." ));
99+             System .exit (1 );
100+         }
101+         return  result ;
102+     }
103+ 
104+     @ VisibleForTesting 
105+     static  CliConfig  loadConfigFile (Path  path ) throws  IOException  {
41106        var  mapper  = new  YAMLMapper ();
42107
43108        JsonNode  defaults  = mapper .readTree (readDefaultConfig ());
@@ -98,27 +163,54 @@ public static CliConfig create() {
98163    public  record  Overrides (@ Nullable  Path  secretStoreDir , @ Nullable  Path  vaultDir , @ Nullable  String  vaultId ) {
99164    }
100165
101-     public  record  CliConfigContext (SecretsConfiguration  secrets ) {
166+     public  record  CliConfigContext (@ Nullable   RemoteRunConfiguration   remoteRun ,  SecretsConfiguration  secrets ) {
102167
103-         public  CliConfigContext  withOverrides (Overrides  overrides ) {
168+         public  CliConfigContext  withOverrides (@ Nullable  Overrides  overrides ) {
169+             if  (overrides  == null ) {
170+                 return  this ;
171+             }
172+             var  remoteRun  = this .remoteRun ();
104173            var  secrets  = this .secrets ().withOverrides (overrides );
105-             return  new  CliConfigContext (secrets );
174+             return  new  CliConfigContext (remoteRun ,  secrets );
106175        }
107176    }
108177
178+     public  record  SecretRef (String  orgName , String  secretName ) {
179+ 
180+         public  SecretRef (String  orgName , String  secretName ) {
181+             this .orgName  = orgName  == null  ? "Default"  : orgName ;
182+             if  (this .orgName .isBlank ()) {
183+                 throw  new  IllegalArgumentException ("'orgName' is required" );
184+             }
185+             this .secretName  = requireNonNull (secretName );
186+             if  (this .secretName .isBlank ()) {
187+                 throw  new  IllegalArgumentException ("'secretName' is required" );
188+             }
189+         }
190+     }
191+ 
192+     public  record  RemoteRunConfiguration (@ Nullable  String  baseUrl , @ Nullable  SecretRef  apiKeyRef ) {
193+     }
194+ 
109195    public  record  SecretsConfiguration (VaultConfiguration  vault ,
110196                                       FileSecretsProviderConfiguration  local ,
111197                                       RemoteSecretsProviderConfiguration  remote ) {
112198
113-         public  SecretsConfiguration  withOverrides (Overrides  overrides ) {
199+         public  SecretsConfiguration  withOverrides (@ Nullable  Overrides  overrides ) {
200+             if  (overrides  == null ) {
201+                 return  this ;
202+             }
114203            var  vault  = this .vault ().withOverrides (overrides );
115204            var  localFiles  = this .local ().withOverrides (overrides );
116205            return  new  SecretsConfiguration (vault , localFiles , this .remote );
117206        }
118207
119208        public  record  VaultConfiguration (Path  dir , String  id ) {
120209
121-             public  VaultConfiguration  withOverrides (Overrides  overrides ) {
210+             public  VaultConfiguration  withOverrides (@ Nullable  Overrides  overrides ) {
211+                 if  (overrides  == null ) {
212+                     return  this ;
213+                 }
122214                return  new  VaultConfiguration (
123215                        Optional .ofNullable (overrides .vaultDir ()).orElse (this .dir ()),
124216                        Optional .ofNullable (overrides .vaultId ()).orElse (this .id ()));
@@ -127,7 +219,10 @@ public VaultConfiguration withOverrides(Overrides overrides) {
127219
128220        public  record  FileSecretsProviderConfiguration (boolean  enabled , boolean  writable , Path  dir ) {
129221
130-             public  FileSecretsProviderConfiguration  withOverrides (Overrides  overrides ) {
222+             public  FileSecretsProviderConfiguration  withOverrides (@ Nullable  Overrides  overrides ) {
223+                 if  (overrides  == null ) {
224+                     return  this ;
225+                 }
131226                return  new  FileSecretsProviderConfiguration (
132227                        this .enabled ,
133228                        this .writable ,
0 commit comments