Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
Expand Down Expand Up @@ -46,17 +48,22 @@ class ParsedDockerComposeFile {
LoaderOptions options = new LoaderOptions();
options.setMaxAliasesForCollections(1_000);
DumperOptions dumperOptions = new DumperOptions();
Resolver resolver = new Resolver();

SafeConstructor constructor = new SafeConstructor(options) {
@Override
protected Object constructObject(Node node) {
if (node.getTag().equals(new Tag("!reset")) || node.getTag().equals(new Tag("!override"))) {
if (node.getTag().equals(new Tag("!reset"))) {
return null;
}
if (node.getTag().equals(new Tag("!override"))) {
// !override replaces the value from previous compose files, so keep the value as if it was untagged
node.setTag(resolveUntaggedTag(node, resolver));
}
return super.constructObject(node);
}
};
Yaml yaml = new Yaml(constructor, new Representer(dumperOptions), dumperOptions, options, new Resolver());
Yaml yaml = new Yaml(constructor, new Representer(dumperOptions), dumperOptions, options, resolver);
try (FileInputStream fileInputStream = FileUtils.openInputStream(composeFile)) {
composeFileContent = yaml.load(fileInputStream);
} catch (Exception e) {
Expand All @@ -76,6 +83,18 @@ protected Object constructObject(Node node) {
parseAndValidate();
}

private static Tag resolveUntaggedTag(Node node, Resolver resolver) {
switch (node.getNodeId()) {
case mapping:
return Tag.MAP;
case sequence:
return Tag.SEQ;
default:
ScalarNode scalarNode = (ScalarNode) node;
return resolver.resolve(NodeId.scalar, scalarNode.getValue(), scalarNode.isPlain());
}
}

private void parseAndValidate() {
final Map<String, ?> servicesMap;
if (composeFileContent.containsKey("version") && "2.0".equals(composeFileContent.get("version"))) {
Expand Down Expand Up @@ -117,7 +136,7 @@ private void parseAndValidate() {
composeFileName,
serviceName
);
break;
continue;
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,16 @@ void shouldGetDependencyImagesWhenOverriding() {
assertThat(dockerComposeFiles.getDependencyImages())
.containsExactlyInAnyOrder("alpine:3.17", "redis:b", "mysql:b", "aservice:latest");
}

@Test
void shouldGetDependencyImagesWhenOverridingWithOverrideTag() {
DockerComposeFiles dockerComposeFiles = new DockerComposeFiles(
Lists.newArrayList(
new File("src/test/resources/docker-compose-imagename-overriding-tag-a.yml"),
new File("src/test/resources/docker-compose-imagename-overriding-tag-b.yml")
)
);
assertThat(dockerComposeFiles.getDependencyImages())
.containsExactlyInAnyOrder("postgres:16", "redis:7", "mysql:8.4");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,63 @@ void shouldObtainImageFromDockerfileBuildWithContext() {
); // redis, mysql from compose file, alpine:3.17 from Dockerfile build
}

@Test
void shouldObtainImageNamesFromOverrideTag() {
File file = new File("src/test/resources/docker-compose-imagename-overriding-tag-b.yml");
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
assertThat(parsedFile.getServiceNameToImageNames())
.as("values tagged with !override are kept")
.containsOnly(
entry("db", Sets.newHashSet("postgres:16")),
entry("redis", Sets.newHashSet("redis:7")),
entry("mysql", Sets.newHashSet("mysql:8.4"))
);
}

@Test
void shouldKeepQuotedValuesTaggedWithOverrideAsStrings() {
File file = new File("src/test/resources/docker-compose-imagename-overriding-tag-quoted.yml");
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
assertThat(parsedFile.getServiceNameToImageNames())
.as("quoted values tagged with !override are not implicitly typed")
.containsOnly(entry("db", Sets.newHashSet("1.0")));
}

@Test
void shouldIgnoreImageNamesRemovedWithResetTag() {
File file = new File("src/test/resources/docker-compose-imagename-reset-tag.yml");
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
assertThat(parsedFile.getServiceNameToImageNames())
.as("values tagged with !reset are removed")
.containsOnly(entry("redis", Sets.newHashSet("redis:7")));
}

@Test
void shouldContinueAfterServiceWithUnknownStructure() {
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(
ImmutableMap.of(
"services",
ImmutableMap.of("unknown", "not a map", "redis", ImmutableMap.of("image", "redis:7"))
)
);
assertThat(parsedFile.getServiceNameToImageNames())
.as("services after one with an unknown structure are still parsed")
.containsOnly(entry("redis", Sets.newHashSet("redis:7")));
}

@Test
void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() {
File file = new File("src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml");
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
assertThat(parsedFile.getServiceNameToImageNames())
.as("v1 services may be named like top-level elements or extension fields of newer formats")
.containsOnly(
entry("name", Sets.newHashSet("busybox:1.36")),
entry("networks", Sets.newHashSet("redis:7")),
entry("x-api", Sets.newHashSet("private.example/api:1.0"))
);
}

@Test
void shouldSupportALotOfAliases() throws Exception {
File file = temporaryFolder.resolve("tmp-docker-compose.yml").toFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
db:
image: postgres:15
redis:
image: redis:6
mysql:
image: mysql:8.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
db: !override
image: postgres:16
redis:
image: !override redis:7
mysql:
image: mysql:8.4
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
services:
db:
image: !override "1.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name:
image: busybox:1.36
networks:
image: redis:7
x-api:
image: private.example/api:1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
db:
image: !reset null
redis:
image: redis:7
Loading