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