Skip to content

Commit 0153bd9

Browse files
committed
runtime-v2, concord-cli: add support for resources.concordCli
1 parent 3c2887d commit 0153bd9

File tree

10 files changed

+333
-89
lines changed

10 files changed

+333
-89
lines changed

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.walmartlabs.concord.common.ConfigurationUtils;
2929
import com.walmartlabs.concord.common.FileVisitor;
3030
import com.walmartlabs.concord.common.IOUtils;
31+
import com.walmartlabs.concord.common.ResourceUtils;
3132
import com.walmartlabs.concord.dependencymanager.DependencyManager;
3233
import com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;
3334
import com.walmartlabs.concord.dependencymanager.DependencyManagerRepositories;
@@ -39,6 +40,7 @@
3940
import com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;
4041
import com.walmartlabs.concord.runtime.v2.ProjectSerializerV2;
4142
import com.walmartlabs.concord.runtime.v2.model.*;
43+
import com.walmartlabs.concord.runtime.v2.model.Resources.ConcordCliResources;
4244
import com.walmartlabs.concord.runtime.v2.runner.InjectorFactory;
4345
import com.walmartlabs.concord.runtime.v2.runner.Runner;
4446
import com.walmartlabs.concord.runtime.v2.runner.guice.ProcessDependenciesModule;
@@ -63,6 +65,8 @@
6365
import java.util.concurrent.Callable;
6466
import java.util.stream.Stream;
6567

68+
import static com.walmartlabs.concord.runtime.v2.model.Resources.DEFAULT_CONCORD_CLI_INCLUDES;
69+
6670
@Command(name = "run", description = "Run the current directory as a Concord process")
6771
public class Run implements Callable<Integer> {
6872

@@ -145,6 +149,11 @@ public Integer call() throws Exception {
145149

146150
Files.copy(src, targetDir.resolve("concord.yml"), StandardCopyOption.REPLACE_EXISTING);
147151
} else if (Files.isDirectory(sourceDir)) {
152+
ConcordCliResources resources = getConcordCliResources(sourceDir);
153+
if (verbosity.verbose()) {
154+
System.out.println("Using CLI resources:\n\t" + resources);
155+
}
156+
148157
targetDir = sourceDir.resolve("target");
149158
if (cleanup && Files.exists(targetDir)) {
150159
if (verbosity.verbose()) {
@@ -154,7 +163,8 @@ public Integer call() throws Exception {
154163
}
155164

156165
// copy everything into target except target
157-
IOUtils.copy(sourceDir, targetDir, "^target$", new CopyNotifier(verbosity.verbose() ? 0 : 100), StandardCopyOption.REPLACE_EXISTING);
166+
CopyNotifier notifier = new CopyNotifier(verbosity.verbose() ? 0 : 100);
167+
ResourceUtils.copyResources(sourceDir, resources.includes(), resources.excludes(), targetDir, notifier, StandardCopyOption.REPLACE_EXISTING);
158168
} else {
159169
throw new IllegalArgumentException("Not a directory or single Concord YAML file: " + sourceDir);
160170
}
@@ -296,6 +306,26 @@ public Integer call() throws Exception {
296306
return 0;
297307
}
298308

309+
private static ConcordCliResources getConcordCliResources(Path sourceDir) {
310+
// try to load the root concord.y?ml and get the concordCli resources config from there
311+
312+
Path rootConcordFile = sourceDir.resolve("concord.yml");
313+
if (!Files.exists(rootConcordFile)) {
314+
rootConcordFile = sourceDir.resolve("concord.yaml");
315+
}
316+
if (!Files.exists(rootConcordFile)) {
317+
return ImmutableConcordCliResources.builder().build();
318+
}
319+
320+
try {
321+
ProjectLoaderV2.Result result = new ProjectLoaderV2(new NoopImportManager())
322+
.loadFromFile(rootConcordFile);
323+
return result.getProjectDefinition().resources().concordCli();
324+
} catch (IOException e) {
325+
throw new RuntimeException("Failed to load root Concord file from " + rootConcordFile, e);
326+
}
327+
}
328+
299329
@SuppressWarnings("unchecked")
300330
private static ProcessInfo processInfo(Map<String, Object> args, List<String> profiles) {
301331
Object processInfoObject = args.get("processInfo");
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.walmartlabs.concord.common;
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 com.walmartlabs.concord.sdk.Constants;
24+
25+
import java.io.IOException;
26+
import java.nio.file.*;
27+
import java.util.ArrayList;
28+
import java.util.Collection;
29+
import java.util.List;
30+
31+
public final class ResourceUtils {
32+
33+
public static void copyResources(Path baseDir, List<String> includePatterns, Path destDir, CopyOption... options) throws IOException {
34+
copyResources(baseDir, includePatterns, List.of(), destDir, null, options);
35+
}
36+
37+
/**
38+
* Copies Concord resources from baseDir to destDir.
39+
* @param baseDir source
40+
* @param includePatterns list of paths (glob, regexes, file paths) to include
41+
* @param excludePatterns list of paths (glob, regexes, file paths) to exclude
42+
* @param destDir destination
43+
* @param visitor a FileVisitor to apply after each copied file (can be null)
44+
* @param options array of CopyOptions
45+
* @throws IOException
46+
*/
47+
public static void copyResources(Path baseDir, List<String> includePatterns, List<String> excludePatterns, Path destDir, FileVisitor visitor, CopyOption... options) throws IOException {
48+
var paths = findResources(baseDir, includePatterns, excludePatterns);
49+
for (var fileName : Constants.Files.PROJECT_ROOT_FILE_NAMES) {
50+
var p = baseDir.resolve(fileName);
51+
paths.add(p);
52+
}
53+
copy(paths, baseDir, destDir, visitor, options);
54+
}
55+
56+
public static List<Path> findResources(Path baseDir, List<String> includePatterns) throws IOException {
57+
return findResources(baseDir, includePatterns, List.of());
58+
}
59+
60+
/**
61+
* Finds paths in baseDir that match includePatterns and do not match excludePatterns.
62+
* @param baseDir source
63+
* @param includePatterns list of paths (glob, regexes, file paths) to include
64+
* @param excludePatterns list of paths (glob, regexes, file paths) to exclude
65+
* @return list of absolute paths
66+
* @throws IOException
67+
*/
68+
public static List<Path> findResources(Path baseDir, List<String> includePatterns, List<String> excludePatterns) throws IOException {
69+
var result = new ArrayList<Path>();
70+
71+
var includeMatchers = includePatterns.stream().map(p -> parsePattern(baseDir, p)).toList();
72+
var excludeMatchers = excludePatterns.stream().map(p -> parsePattern(baseDir, p)).toList();
73+
74+
try (var walker = Files.walk(baseDir)) {
75+
walker.filter(candidate -> !Files.isDirectory(candidate))
76+
.filter(candidate ->
77+
includeMatchers.stream().anyMatch(m -> m.matches(candidate)) &&
78+
excludeMatchers.stream().noneMatch(pattern -> pattern.matches(candidate)))
79+
.forEach(result::add);
80+
}
81+
82+
return result;
83+
}
84+
85+
private static void copy(Collection<Path> files, Path baseDir, Path dest, FileVisitor visitor, CopyOption... options) throws IOException {
86+
for (var f : files) {
87+
if (Files.notExists(f)) {
88+
continue;
89+
}
90+
91+
var src = baseDir.relativize(f);
92+
var dst = dest.resolve(src);
93+
94+
var dstParent = dst.getParent();
95+
if (dstParent != null && Files.notExists(dstParent)) {
96+
Files.createDirectories(dstParent);
97+
}
98+
99+
if (Files.isSymbolicLink(f)) {
100+
Path link = Files.readSymbolicLink(f);
101+
Path target = f.getParent().resolve(link).normalize();
102+
103+
if (!target.startsWith(baseDir)) {
104+
throw new IOException("Symlinks outside the base directory are not supported: " + baseDir + " -> " + target);
105+
}
106+
107+
if (Files.notExists(target)) {
108+
continue;
109+
}
110+
111+
Files.createSymbolicLink(dst, link);
112+
} else {
113+
Files.copy(f, dst, options);
114+
}
115+
116+
if (visitor != null) {
117+
visitor.visit(src, dst);
118+
}
119+
}
120+
}
121+
122+
static PathMatcher parsePattern(Path baseDir, String pattern) {
123+
pattern = pattern.trim();
124+
125+
String normalizedPattern;
126+
if (pattern.startsWith("glob:")) {
127+
normalizedPattern = "glob:" + concat(baseDir, pattern.substring("glob:".length()));
128+
} else if (pattern.startsWith("regex:")) {
129+
normalizedPattern = "regex:" + concat(baseDir, pattern.substring("regex:".length()));
130+
} else {
131+
normalizedPattern = "glob:" + concat(baseDir, pattern
132+
.replace("*", "\\*")
133+
.replace("{", "\\{")
134+
.replace("}", "\\}")
135+
.replace("[", "\\[")
136+
.replace("]", "\\]")
137+
.replace("!", "\\!")
138+
.replace("?", "\\?"));
139+
}
140+
141+
return FileSystems.getDefault().getPathMatcher(normalizedPattern);
142+
}
143+
144+
private static String concat(Path path, String str) {
145+
var separator = "/";
146+
if (str.startsWith("/")) {
147+
separator = "";
148+
}
149+
return path.toAbsolutePath() + separator + str;
150+
}
151+
152+
private ResourceUtils() {
153+
}
154+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.walmartlabs.concord.common;
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 org.junit.jupiter.api.Test;
24+
25+
import java.nio.file.Paths;
26+
27+
import static org.junit.jupiter.api.Assertions.assertFalse;
28+
import static org.junit.jupiter.api.Assertions.assertTrue;
29+
30+
public class ResourceUtilTest {
31+
32+
@Test
33+
public void testParsePattern() {
34+
var baseDir = Paths.get("/tmp/foo");
35+
36+
var pattern = ResourceUtils.parsePattern(baseDir, "glob:*.txt");
37+
assertTrue(pattern.matches(Paths.get("/tmp/foo/bar.txt")));
38+
assertTrue(pattern.matches(Paths.get("/tmp/foo/bar!.txt")));
39+
assertFalse(pattern.matches(Paths.get("/tmp/foo/baz.tmp")));
40+
41+
pattern = ResourceUtils.parsePattern(baseDir, "glob:concord/{**/,}{*.,}concord.{yml,yaml}");
42+
assertTrue(pattern.matches(Paths.get("/tmp/foo/concord/foo.concord.yml")));
43+
assertTrue(pattern.matches(Paths.get("/tmp/foo/concord/bar.concord.yaml")));
44+
assertFalse(pattern.matches(Paths.get("/tmp/foo/qux.concord.yaml")));
45+
46+
pattern = ResourceUtils.parsePattern(baseDir, "regex:.*\\.qux");
47+
assertTrue(pattern.matches(Paths.get("/tmp/foo/bar.qux")));
48+
assertFalse(pattern.matches(Paths.get("/tmp/foo/qux")));
49+
50+
pattern = ResourceUtils.parsePattern(baseDir, "baz.txt");
51+
assertTrue(pattern.matches(Paths.get("/tmp/foo/baz.txt")));
52+
assertFalse(pattern.matches(Paths.get("/tmp/foo/qux.txt")));
53+
}
54+
}

0 commit comments

Comments
 (0)