diff --git a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java index c62eb8f9964..23831d3f026 100644 --- a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java +++ b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java @@ -7,9 +7,12 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.testcontainers.images.ParsedDockerfile; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.representer.Representer; +import org.yaml.snakeyaml.resolver.Resolver; import java.io.File; import java.io.FileInputStream; @@ -34,10 +37,20 @@ class ParsedDockerComposeFile { private final File composeFile; @Getter - private Map> serviceNameToImageNames = new HashMap<>(); + private final Map> serviceNameToImageNames = new HashMap<>(); ParsedDockerComposeFile(File composeFile) { - Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + // The default is 50 and a big docker-compose.yml file can easily go above that number. 1,000 should give us some room + LoaderOptions options = new LoaderOptions(); + options.setMaxAliasesForCollections(1_000); + DumperOptions dumperOptions = new DumperOptions(); + Yaml yaml = new Yaml( + new SafeConstructor(options), + new Representer(dumperOptions), + dumperOptions, + options, + new Resolver() + ); try (FileInputStream fileInputStream = FileUtils.openInputStream(composeFile)) { composeFileContent = yaml.load(fileInputStream); } catch (Exception e) { @@ -82,7 +95,9 @@ private void parseAndValidate() { return; } - servicesMap = (Map) servicesElement; + @SuppressWarnings("unchecked") + Map temp = (Map) servicesElement; + servicesMap = temp; } else { servicesMap = composeFileContent; } @@ -99,7 +114,8 @@ private void parseAndValidate() { break; } - final Map serviceDefinitionMap = (Map) serviceDefinition; + @SuppressWarnings("unchecked") + final Map serviceDefinitionMap = (Map) serviceDefinition; validateNoContainerNameSpecified(serviceName, serviceDefinitionMap); findServiceImageName(serviceName, serviceDefinitionMap); @@ -107,7 +123,7 @@ private void parseAndValidate() { } } - private void validateNoContainerNameSpecified(String serviceName, Map serviceDefinitionMap) { + private void validateNoContainerNameSpecified(String serviceName, Map serviceDefinitionMap) { if (serviceDefinitionMap.containsKey("container_name")) { throw new IllegalStateException( String.format( @@ -119,20 +135,21 @@ private void validateNoContainerNameSpecified(String serviceName, Map serviceDef } } - private void findServiceImageName(String serviceName, Map serviceDefinitionMap) { - if (serviceDefinitionMap.containsKey("image") && serviceDefinitionMap.get("image") instanceof String) { - final String imageName = (String) serviceDefinitionMap.get("image"); + private void findServiceImageName(String serviceName, Map serviceDefinitionMap) { + Object result = serviceDefinitionMap.get("image"); + if (result instanceof String) { + final String imageName = (String) result; log.debug("Resolved dependency image for Docker Compose in {}: {}", composeFileName, imageName); serviceNameToImageNames.put(serviceName, Sets.newHashSet(imageName)); } } - private void findImageNamesInDockerfile(String serviceName, Map serviceDefinitionMap) { + private void findImageNamesInDockerfile(String serviceName, Map serviceDefinitionMap) { final Object buildNode = serviceDefinitionMap.get("build"); Path dockerfilePath = null; if (buildNode instanceof Map) { - final Map buildElement = (Map) buildNode; + final Map buildElement = (Map) buildNode; final Object dockerfileRelativePath = buildElement.get("dockerfile"); final Object contextRelativePath = buildElement.get("context"); if (dockerfileRelativePath instanceof String && contextRelativePath instanceof String) { diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 15bca203e31..08b2746b386 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -3,17 +3,24 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import lombok.SneakyThrows; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import java.io.File; +import java.io.PrintWriter; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; public class ParsedDockerComposeFileValidationTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test public void shouldValidate() { File file = new File("src/test/resources/docker-compose-container-name-v1.yml"); @@ -129,4 +136,22 @@ public void shouldObtainImageFromDockerfileBuildWithContext() { entry("custom", Sets.newHashSet("alpine:3.17")) ); // redis, mysql from compose file, alpine:3.17 from Dockerfile build } + + @Test + public void shouldSupportALotOfAliases() throws Exception { + File file = temporaryFolder.newFile(); + try (PrintWriter writer = new PrintWriter(file)) { + writer.println("x-entry: &entry"); + writer.println(" key: value"); + writer.println(); + writer.println("services:"); + for (int i = 0; i < 1_000; i++) { + writer.println(" service" + i + ":"); + writer.println(" image: busybox"); + writer.println(" environment:"); + writer.println(" <<: *entry"); + } + } + assertThatNoException().isThrownBy(() -> new ParsedDockerComposeFile(file)); + } }