Skip to content

Commit e29adb7

Browse files
committed
concord-cli: add remote-run command
1 parent 8e2a726 commit e29adb7

File tree

13 files changed

+456
-95
lines changed

13 files changed

+456
-95
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
@@ -23,8 +23,12 @@
2323
import com.fasterxml.jackson.databind.DeserializationFeature;
2424
import com.fasterxml.jackson.databind.JsonMappingException;
2525
import com.fasterxml.jackson.databind.JsonNode;
26+
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
2627
import com.fasterxml.jackson.databind.node.ObjectNode;
2728
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
29+
import com.google.common.annotations.VisibleForTesting;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
2832

2933
import javax.annotation.Nullable;
3034
import java.io.IOException;
@@ -35,10 +39,71 @@
3539
import java.util.Optional;
3640

3741
import static java.nio.charset.StandardCharsets.UTF_8;
42+
import static java.util.Objects.requireNonNull;
43+
import static org.fusesource.jansi.Ansi.ansi;
3844

3945
public 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,
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)