2323import  com .fasterxml .jackson .databind .DeserializationFeature ;
2424import  com .fasterxml .jackson .databind .JsonMappingException ;
2525import  com .fasterxml .jackson .databind .JsonNode ;
26+ import  com .fasterxml .jackson .databind .exc .UnrecognizedPropertyException ;
2627import  com .fasterxml .jackson .databind .node .ObjectNode ;
2728import  com .fasterxml .jackson .dataformat .yaml .YAMLMapper ;
29+ import  com .google .common .annotations .VisibleForTesting ;
30+ import  org .slf4j .Logger ;
31+ import  org .slf4j .LoggerFactory ;
2832
2933import  javax .annotation .Nullable ;
3034import  java .io .IOException ;
3539import  java .util .Optional ;
3640
3741import  static  java .nio .charset .StandardCharsets .UTF_8 ;
42+ import  static  java .util .Objects .requireNonNull ;
43+ import  static  org .fusesource .jansi .Ansi .ansi ;
3844
3945public  record  CliConfig (Map <String , CliConfigContext > contexts ) {
4046
41-     public  static  CliConfig  load (Path  path ) throws  IOException  {
47+     private  static  final  Logger  log  = LoggerFactory .getLogger (CliConfig .class );
48+ 
49+     public  static  CliConfig .CliConfigContext  load (Verbosity  verbosity , String  context , Overrides  overrides ) {
50+         Path  baseDir  = Paths .get (System .getProperty ("user.home" ), ".concord" );
51+         Path  cfgFile  = baseDir .resolve ("cli.yaml" );
52+         if  (!Files .exists (cfgFile )) {
53+             cfgFile  = baseDir .resolve ("cli.yml" );
54+         }
55+         if  (!Files .exists (cfgFile )) {
56+             CliConfig  cfg  = CliConfig .create ();
57+             return  assertCliConfigContext (cfg , context ).withOverrides (overrides );
58+         }
59+ 
60+         if  (verbosity .verbose ()) {
61+             log .info ("Using CLI configuration file: {} (\" {}\"  context)" , cfgFile , context );
62+         }
63+ 
64+         try  {
65+             CliConfig  cfg  = loadConfigFile (cfgFile );
66+             return  assertCliConfigContext (cfg , context ).withOverrides (overrides );
67+         } catch  (Exception  e ) {
68+             handleCliConfigErrorAndBail (cfgFile .toAbsolutePath ().toString (), e );
69+             throw  new  IllegalStateException ("should be unreachable" );
70+         }
71+     }
72+ 
73+     private  static  void  handleCliConfigErrorAndBail (String  cfgPath , Throwable  e ) {
74+         // unwrap runtime exceptions 
75+         if  (e  instanceof  RuntimeException  ex ) {
76+             if  (ex .getCause () instanceof  IllegalArgumentException ) {
77+                 e  = ex .getCause ();
78+             }
79+         }
80+ 
81+         // handle YAML errors 
82+         if  (e  instanceof  IllegalArgumentException ) {
83+             if  (e .getCause () instanceof  UnrecognizedPropertyException  ex ) {
84+                 System .out .println (ansi ().fgRed ().a ("Invalid format of the CLI configuration file " ).a (cfgPath ).a (". " ).a (ex .getMessage ()));
85+                 System .exit (1 );
86+             }
87+             System .out .println (ansi ().fgRed ().a ("Invalid format of the CLI configuration file " ).a (cfgPath ).a (". " ).a (e .getMessage ()));
88+             System .exit (1 );
89+         }
90+ 
91+         // all other errors 
92+         System .out .println (ansi ().fgRed ().a ("Failed to read the CLI configuration file " ).a (cfgPath ).a (". " ).a (e .getMessage ()));
93+         System .exit (1 );
94+     }
95+ 
96+     private  static  CliConfig .CliConfigContext  assertCliConfigContext (CliConfig  config , String  context ) {
97+         CliConfig .CliConfigContext  result  = config .contexts ().get (context );
98+         if  (result  == null ) {
99+             System .out .println (ansi ().fgRed ().a ("Configuration context not found: " ).a (context ).a (". Check the CLI configuration file." ));
100+             System .exit (1 );
101+         }
102+         return  result ;
103+     }
104+ 
105+     @ VisibleForTesting 
106+     static  CliConfig  loadConfigFile (Path  path ) throws  IOException  {
42107        var  mapper  = new  YAMLMapper ()
43108                .disable (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES );
44109
@@ -100,27 +165,54 @@ public static CliConfig create() {
100165    public  record  Overrides (@ Nullable  Path  secretStoreDir , @ Nullable  Path  vaultDir , @ Nullable  String  vaultId ) {
101166    }
102167
103-     public  record  CliConfigContext (SecretsConfiguration  secrets ) {
168+     public  record  CliConfigContext (@ Nullable   RemoteRunConfiguration   remoteRun ,  SecretsConfiguration  secrets ) {
104169
105-         public  CliConfigContext  withOverrides (Overrides  overrides ) {
170+         public  CliConfigContext  withOverrides (@ Nullable  Overrides  overrides ) {
171+             if  (overrides  == null ) {
172+                 return  this ;
173+             }
174+             var  remoteRun  = this .remoteRun ();
106175            var  secrets  = this .secrets ().withOverrides (overrides );
107-             return  new  CliConfigContext (secrets );
176+             return  new  CliConfigContext (remoteRun ,  secrets );
108177        }
109178    }
110179
180+     public  record  SecretRef (String  orgName , String  secretName ) {
181+ 
182+         public  SecretRef (String  orgName , String  secretName ) {
183+             this .orgName  = orgName  == null  ? "Default"  : orgName ;
184+             if  (this .orgName .isBlank ()) {
185+                 throw  new  IllegalArgumentException ("'orgName' is required" );
186+             }
187+             this .secretName  = requireNonNull (secretName );
188+             if  (this .secretName .isBlank ()) {
189+                 throw  new  IllegalArgumentException ("'secretName' is required" );
190+             }
191+         }
192+     }
193+ 
194+     public  record  RemoteRunConfiguration (@ Nullable  String  baseUrl , @ Nullable  SecretRef  apiKeyRef ) {
195+     }
196+ 
111197    public  record  SecretsConfiguration (VaultConfiguration  vault ,
112198                                       FileSecretsProviderConfiguration  local ,
113199                                       RemoteSecretsProviderConfiguration  remote ) {
114200
115-         public  SecretsConfiguration  withOverrides (Overrides  overrides ) {
201+         public  SecretsConfiguration  withOverrides (@ Nullable  Overrides  overrides ) {
202+             if  (overrides  == null ) {
203+                 return  this ;
204+             }
116205            var  vault  = this .vault ().withOverrides (overrides );
117206            var  localFiles  = this .local ().withOverrides (overrides );
118207            return  new  SecretsConfiguration (vault , localFiles , this .remote );
119208        }
120209
121210        public  record  VaultConfiguration (Path  dir , String  id ) {
122211
123-             public  VaultConfiguration  withOverrides (Overrides  overrides ) {
212+             public  VaultConfiguration  withOverrides (@ Nullable  Overrides  overrides ) {
213+                 if  (overrides  == null ) {
214+                     return  this ;
215+                 }
124216                return  new  VaultConfiguration (
125217                        Optional .ofNullable (overrides .vaultDir ()).orElse (this .dir ()),
126218                        Optional .ofNullable (overrides .vaultId ()).orElse (this .id ()));
@@ -129,7 +221,10 @@ public VaultConfiguration withOverrides(Overrides overrides) {
129221
130222        public  record  FileSecretsProviderConfiguration (boolean  enabled , boolean  writable , Path  dir ) {
131223
132-             public  FileSecretsProviderConfiguration  withOverrides (Overrides  overrides ) {
224+             public  FileSecretsProviderConfiguration  withOverrides (@ Nullable  Overrides  overrides ) {
225+                 if  (overrides  == null ) {
226+                     return  this ;
227+                 }
133228                return  new  FileSecretsProviderConfiguration (
134229                        this .enabled ,
135230                        this .writable ,
0 commit comments