Skip to content

Commit 3760685

Browse files
committed
concord-cli: add remote-run command
1 parent ed80c81 commit 3760685

File tree

13 files changed

+456
-94
lines changed

13 files changed

+456
-94
lines changed

cli/src/main/java/com/walmartlabs/concord/cli/App.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import picocli.CommandLine.Option;
2626
import picocli.CommandLine.Spec;
2727

28-
@Command(name = "concord", subcommands = {Lint.class, Run.class, SelfUpdate.class})
28+
@Command(name = "concord", subcommands = {Lint.class, Run.class, RemoteRun.class, SelfUpdate.class})
2929
public class App implements Runnable {
3030

3131
@Spec

cli/src/main/java/com/walmartlabs/concord/cli/CliConfig.java

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@
2222

2323
import com.fasterxml.jackson.databind.JsonMappingException;
2424
import com.fasterxml.jackson.databind.JsonNode;
25+
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
2526
import com.fasterxml.jackson.databind.node.ObjectNode;
2627
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
28+
import com.google.common.annotations.VisibleForTesting;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
2731

2832
import javax.annotation.Nullable;
2933
import java.io.IOException;
@@ -34,10 +38,71 @@
3438
import java.util.Optional;
3539

3640
import static java.nio.charset.StandardCharsets.UTF_8;
41+
import static java.util.Objects.requireNonNull;
42+
import static org.fusesource.jansi.Ansi.ansi;
3743

3844
public 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,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.walmartlabs.concord.cli;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2025 Walmart Inc.
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import java.io.IOException;
24+
25+
import static org.fusesource.jansi.Ansi.ansi;
26+
27+
public final class Confirmation {
28+
29+
public static boolean confirm(String message) throws IOException {
30+
System.out.println(ansi().fgBrightYellow().bold().a(message).reset());
31+
int response = System.in.read();
32+
// y == 121, Y == 89
33+
return response == 121 || response == 89;
34+
}
35+
36+
private Confirmation() {
37+
}
38+
}

0 commit comments

Comments
 (0)