diff --git a/build.gradle.kts b/build.gradle.kts index 65ed7c0..d88add7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,5 +10,6 @@ val rewriteVersion = rewriteRecipe.rewriteVersion.get() dependencies { implementation(platform("org.openrewrite:rewrite-bom:$rewriteVersion")) implementation("org.openrewrite:rewrite-core") + implementation("org.openrewrite:rewrite-yaml") testImplementation("org.openrewrite:rewrite-test") } diff --git a/src/main/java/org/openrewrite/docker/DockerImageVersion.java b/src/main/java/org/openrewrite/docker/DockerImageVersion.java index 9c250e0..e38e2ea 100644 --- a/src/main/java/org/openrewrite/docker/DockerImageVersion.java +++ b/src/main/java/org/openrewrite/docker/DockerImageVersion.java @@ -25,8 +25,8 @@ public class DockerImageVersion { @Nullable String version; - @Override - public String toString() { - return imageName + (version != null ? ":" + version : ""); + public static DockerImageVersion of(String value) { + String[] imageVersionStr = value.trim().split(":"); + return new DockerImageVersion(imageVersionStr[0], imageVersionStr.length > 1 ? imageVersionStr[1].split(" ")[0] : null); } } diff --git a/src/main/java/org/openrewrite/docker/search/FindDockerImageUses.java b/src/main/java/org/openrewrite/docker/search/FindDockerImageUses.java index 3513be3..0510fbc 100644 --- a/src/main/java/org/openrewrite/docker/search/FindDockerImageUses.java +++ b/src/main/java/org/openrewrite/docker/search/FindDockerImageUses.java @@ -15,17 +15,22 @@ */ package org.openrewrite.docker.search; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; import org.openrewrite.docker.DockerImageVersion; import org.openrewrite.docker.table.DockerBaseImages; +import org.openrewrite.docker.trait.ImageMatcher; import org.openrewrite.marker.SearchResult; +import org.openrewrite.text.Find; +import org.openrewrite.text.PlainText; +import org.openrewrite.trait.Reference; -import java.util.List; -import java.util.stream.Collectors; +import java.nio.file.Path; +import java.util.*; -import static org.openrewrite.docker.trait.Traits.dockerfile; +import static java.util.stream.Collectors.joining; public class FindDockerImageUses extends Recipe { transient DockerBaseImages dockerBaseImages = new DockerBaseImages(this); @@ -37,25 +42,52 @@ public String getDisplayName() { @Override public String getDescription() { - return "Produce an impact analysis of base images used in Dockerfiles."; + return "Produce an impact analysis of base images used in Dockerfiles, .gitlab-ci files, Kubernetes Deployment file, etc."; } @Override public TreeVisitor getVisitor() { - return dockerfile().asVisitor((docker, ctx) -> { - List froms = docker.getFroms(); - if (!froms.isEmpty()) { - for (DockerImageVersion from : froms) { - dockerBaseImages.insertRow(ctx, new DockerBaseImages.Row( - from.getImageName(), - from.getVersion() == null ? "" : from.getVersion() - )); + return new TreeVisitor() { + + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree instanceof SourceFileWithReferences) { + SourceFileWithReferences sourceFile = (SourceFileWithReferences) tree; + Path sourcePath = sourceFile.getSourcePath(); + Collection references = sourceFile.getReferences().findMatches(new ImageMatcher()); + Map> matches = new HashMap<>(); + for (Reference ref : references) { + DockerImageVersion from = DockerImageVersion.of(ref.getValue()); + dockerBaseImages.insertRow(ctx, + new DockerBaseImages.Row(sourcePath.toString(), from.getImageName(), from.getVersion()) + ); + matches.computeIfAbsent(ref.getTree(), t -> new ArrayList<>()).add(ref); + } + return new ReferenceFindSearchResultVisitor(matches).visit(tree, ctx, getCursor()); + } + return tree; + } + }; + } + + @Value + @EqualsAndHashCode(callSuper = false) + private static class ReferenceFindSearchResultVisitor extends TreeVisitor { + Map> matches; + + @Override + public @Nullable Tree postVisit(Tree tree, ExecutionContext ctx) { + List references = matches.get(tree); + if (references != null) { + if (tree instanceof PlainText) { + String find = references.stream().map(Reference::getValue).sorted().collect(joining("|")); + return new Find(find, true, null, null, null, null, true) + .getVisitor() + .visitNonNull(tree, ctx); } - return SearchResult.found(docker.getTree(), - froms.stream().map(DockerImageVersion::toString) - .collect(Collectors.joining(", "))); + return SearchResult.found(tree, references.get(0).getValue()); } - return docker.getTree(); - }); + return tree; + } } } diff --git a/src/main/java/org/openrewrite/docker/table/DockerBaseImages.java b/src/main/java/org/openrewrite/docker/table/DockerBaseImages.java index c226836..9f8fda9 100644 --- a/src/main/java/org/openrewrite/docker/table/DockerBaseImages.java +++ b/src/main/java/org/openrewrite/docker/table/DockerBaseImages.java @@ -16,6 +16,7 @@ package org.openrewrite.docker.table; import lombok.Value; +import org.jspecify.annotations.Nullable; import org.openrewrite.Column; import org.openrewrite.DataTable; import org.openrewrite.Recipe; @@ -30,12 +31,17 @@ public DockerBaseImages(Recipe recipe) { @Value public static class Row { + @Column(displayName = "Source path", + description = "The source file containing the image reference.") + String sourcePath; + @Column(displayName = "Image name", description = "The full name of the image.") String imageName; @Column(displayName = "Tag", description = "The tag, if any. If no tag is specified, this will be empty.") + @Nullable String tag; } } diff --git a/src/main/java/org/openrewrite/docker/trait/Dockerfile.java b/src/main/java/org/openrewrite/docker/trait/Dockerfile.java deleted file mode 100644 index dc13348..0000000 --- a/src/main/java/org/openrewrite/docker/trait/Dockerfile.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.docker.trait; - -import lombok.Value; -import org.jspecify.annotations.Nullable; -import org.openrewrite.Cursor; -import org.openrewrite.docker.DockerImageVersion; -import org.openrewrite.text.PlainText; -import org.openrewrite.trait.SimpleTraitMatcher; -import org.openrewrite.trait.Trait; - -import java.util.ArrayList; -import java.util.List; - -@Value -public class Dockerfile implements Trait

{ - Cursor cursor; - - public List<DockerImageVersion> getFroms() { - List<DockerImageVersion> froms = new ArrayList<>(); - for (String line : getTree().getText().split("\\R")) { - if (line.startsWith("FROM")) { - String[] imageVersionStr = line.substring("FROM".length()).trim().split(":"); - froms.add(new DockerImageVersion( - imageVersionStr[0], - imageVersionStr.length > 1 ? imageVersionStr[1].split(" ")[0] : null - )); - } - } - return froms; - } - - public static class Matcher extends SimpleTraitMatcher<Dockerfile> { - - @Override - protected @Nullable Dockerfile test(Cursor cursor) { - Object value = cursor.getValue(); - if (value instanceof PlainText) { - PlainText text = (PlainText) value; - String fileName = text.getSourcePath().toFile().getName(); - if (fileName.equals("Dockerfile") || fileName.equals("Containerfile")) { - return new Dockerfile(cursor); - } - } - return null; - } - } -} diff --git a/src/main/java/org/openrewrite/docker/trait/DockerfileImageReference.java b/src/main/java/org/openrewrite/docker/trait/DockerfileImageReference.java new file mode 100644 index 0000000..7fc025f --- /dev/null +++ b/src/main/java/org/openrewrite/docker/trait/DockerfileImageReference.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 the original author or authors. + * <p> + * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * https://docs.moderne.io/licensing/moderne-source-available-license + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.docker.trait; + +import lombok.Value; +import org.openrewrite.Cursor; +import org.openrewrite.SourceFile; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.text.PlainText; +import org.openrewrite.trait.Reference; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +@Value +public class DockerfileImageReference implements Reference { + Cursor cursor; + String value; + + @Override + public Kind getKind() { + return Kind.IMAGE; + } + + public static class Provider implements Reference.Provider { + @Override + public boolean isAcceptable(SourceFile sourceFile) { + if (sourceFile instanceof PlainText) { + PlainText text = (PlainText) sourceFile; + String fileName = text.getSourcePath().toFile().getName(); + return (fileName.endsWith("Dockerfile") || fileName.equals("Containerfile")) && + (text.getText().contains("FROM") || text.getText().contains("from")); + } + return false; + } + + @Override + public Set<Reference> getReferences(SourceFile sourceFile) { + Cursor c = new Cursor(new Cursor(null, Cursor.ROOT_VALUE), sourceFile); + String[] words = ((PlainText) sourceFile).getText() + .replaceAll("\\s*#.*?\\n", "") // remove comments + .replaceAll("\".*?\"", "") // remove string literals + .split("\\s+"); + + Set<Reference> references = new HashSet<>(); + ArrayList<String> imageVariables = new ArrayList<>(); + for (int i = 0, wordsLength = words.length; i < wordsLength; i++) { + if ("from".equalsIgnoreCase(words[i])) { + String image = words[i + 1].startsWith("--platform") ? words[i + 2] : words[i + 1]; + references.add(new DockerfileImageReference(c, image)); + } else if ("as".equalsIgnoreCase(words[i])) { + imageVariables.add(words[i + 1]); + } else if (words[i].startsWith("--from") && words[i].split("=").length == 2) { + String image = words[i].split("=")[1]; + if (!imageVariables.contains(image) && !StringUtils.isNumeric(image)) { + references.add(new DockerfileImageReference(c, image)); + } + } + } + + return references; + } + } +} diff --git a/src/main/java/org/openrewrite/docker/trait/Traits.java b/src/main/java/org/openrewrite/docker/trait/ImageMatcher.java similarity index 67% rename from src/main/java/org/openrewrite/docker/trait/Traits.java rename to src/main/java/org/openrewrite/docker/trait/ImageMatcher.java index 2753f6f..2c767da 100644 --- a/src/main/java/org/openrewrite/docker/trait/Traits.java +++ b/src/main/java/org/openrewrite/docker/trait/ImageMatcher.java @@ -15,12 +15,17 @@ */ package org.openrewrite.docker.trait; -public class Traits { +import org.openrewrite.trait.Reference; - private Traits() { +public class ImageMatcher implements Reference.Matcher { + + @Override + public boolean matchesReference(Reference reference) { + return reference.getKind() == Reference.Kind.IMAGE; } - public static Dockerfile.Matcher dockerfile() { - return new Dockerfile.Matcher(); + @Override + public Reference.Renamer createRenamer(String newName) { + return reference -> newName; } } diff --git a/src/main/java/org/openrewrite/docker/trait/YamlImageReference.java b/src/main/java/org/openrewrite/docker/trait/YamlImageReference.java new file mode 100644 index 0000000..3a90252 --- /dev/null +++ b/src/main/java/org/openrewrite/docker/trait/YamlImageReference.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 the original author or authors. + * <p> + * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * https://docs.moderne.io/licensing/moderne-source-available-license + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.docker.trait; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.trait.Reference; +import org.openrewrite.trait.SimpleTraitMatcher; +import org.openrewrite.yaml.trait.YamlReference; +import org.openrewrite.yaml.tree.Yaml; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Value +@EqualsAndHashCode(callSuper = false) +public class YamlImageReference extends YamlReference { + Cursor cursor; + + @Override + public Reference.Kind getKind() { + return Reference.Kind.IMAGE; + } + + public static class Provider extends YamlProvider { + private static final SimpleTraitMatcher<YamlReference> matcher = new SimpleTraitMatcher<YamlReference>() { + private final AtomicBoolean found = new AtomicBoolean(false); + + @Override + protected @Nullable YamlReference test(Cursor cursor) { + Object value = cursor.getValue(); + if (value instanceof Yaml.Scalar) { + if (found.get()) { + found.set(false); + return new YamlImageReference(cursor); + } else if ("image".equals(((Yaml.Scalar) value).getValue())) { + found.set(true); + } + } + return null; + } + }; + + @Override + public SimpleTraitMatcher<YamlReference> getMatcher() { + return matcher; + } + } +} diff --git a/src/main/resources/META-INF/services/org.openrewrite.trait.Reference$Provider b/src/main/resources/META-INF/services/org.openrewrite.trait.Reference$Provider new file mode 100644 index 0000000..94c0c37 --- /dev/null +++ b/src/main/resources/META-INF/services/org.openrewrite.trait.Reference$Provider @@ -0,0 +1,2 @@ +org.openrewrite.docker.trait.YamlImageReference$Provider +org.openrewrite.docker.trait.DockerfileImageReference$Provider diff --git a/src/test/java/org/openrewrite/docker/FindDockerImagesUsedTest.java b/src/test/java/org/openrewrite/docker/FindDockerImagesUsedTest.java index 39d2578..53ff6f7 100644 --- a/src/test/java/org/openrewrite/docker/FindDockerImagesUsedTest.java +++ b/src/test/java/org/openrewrite/docker/FindDockerImagesUsedTest.java @@ -24,8 +24,11 @@ import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import java.util.function.Consumer; + import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.test.SourceSpecs.text; +import static org.openrewrite.yaml.Assertions.yaml; class FindDockerImagesUsedTest implements RewriteTest { @@ -48,7 +51,7 @@ void dockerfile(String path) { SHELL ["sh", "-lc"] """, """ - ~~(nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04)~~>FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04 + FROM ~~(nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04)~~>nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04 LABEL maintainer="Hugging Face" ARG DEBIAN_FRONTEND=noninteractive SHELL ["sh", "-lc"] @@ -59,26 +62,371 @@ void dockerfile(String path) { } @Test - void multistageDockerfile() { + void yamlFileWithMultipleImages() { + rewriteRun( + assertImages("golang:1.7.0", "golang:1.7.0", "golang:1.7.3"), + yaml( + """ + test: + image: golang:1.7.3 + + accp: + image: golang:1.7.0 + + prod: + image: golang:1.7.0 + """, + """ + test: + image: ~~(golang:1.7.3)~~>golang:1.7.3 + + accp: + image: ~~(golang:1.7.0)~~>golang:1.7.0 + + prod: + image: ~~(golang:1.7.0)~~>golang:1.7.0 + """ + ) + ); + } + + @Test + void dockerFile() { rewriteRun( - spec -> spec.dataTable(DockerBaseImages.Row.class, rows -> assertThat(rows) - .containsOnly(new DockerBaseImages.Row("nvidia/cuda", "11.8.0-cudnn8-devel-ubuntu20.04"))), + assertImages("golang:1.7.3"), text( //language=Dockerfile """ - FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04 AS base - LABEL maintainer="Hugging Face" - ARG DEBIAN_FRONTEND=noninteractive - SHELL ["sh", "-lc"] + FROM golang:1.7.3 as builder + WORKDIR /go/src/github.com/alexellis/href-counter/ + RUN go get -d -v golang.org/x/net/html + COPY app.go . + RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . """, """ - ~~(nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04)~~>FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04 AS base - LABEL maintainer="Hugging Face" - ARG DEBIAN_FRONTEND=noninteractive - SHELL ["sh", "-lc"] + FROM ~~(golang:1.7.3)~~>golang:1.7.3 as builder + WORKDIR /go/src/github.com/alexellis/href-counter/ + RUN go get -d -v golang.org/x/net/html + COPY app.go . + RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . + """, + spec -> spec.path("Dockerfile") + ) + ); + } + + @Test + void dockerMultipleStageFileWithLowerCaseText() { + rewriteRun( + assertImages("alpine:latest", "golang:1.7.3"), + text( + //language=Dockerfile + """ + FROM golang:1.7.3 as builder + WORKDIR /go/src/github.com/alexellis/href-counter/ + RUN go get -d -v golang.org/x/net/html + COPY app.go . + RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . + + from alpine:latest + run apk --no-cache add ca-certificates + workdir /root/ + copy --from=builder /go/src/github.com/alexellis/href-counter/app . + cmd ["./app"] + """, + """ + FROM ~~(golang:1.7.3)~~>golang:1.7.3 as builder + WORKDIR /go/src/github.com/alexellis/href-counter/ + RUN go get -d -v golang.org/x/net/html + COPY app.go . + RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . + + from ~~(alpine:latest)~~>alpine:latest + run apk --no-cache add ca-certificates + workdir /root/ + copy --from=builder /go/src/github.com/alexellis/href-counter/app . + cmd ["./app"] + """, + spec -> spec.path("Dockerfile") + ) + ); + } + + @Test + void dockerMultipleStageFileWithImageInFromOption() { + rewriteRun( + assertImages("alpine:latest", "nginx:latest"), + text( + //language=Dockerfile + """ + FROM alpine:latest + COPY --from=nginx:latest /etc/nginx/nginx.conf /etc/nginx/ + COPY --from=nginx:latest /usr/share/nginx/html /usr/share/nginx/html + RUN apk add --no-cache bash + WORKDIR /usr/share/nginx/html + CMD ["ls", "-la", "/usr/share/nginx/html"] + """, + """ + FROM ~~(alpine:latest)~~>alpine:latest + COPY --from=~~(nginx:latest)~~>nginx:latest /etc/nginx/nginx.conf /etc/nginx/ + COPY --from=~~(nginx:latest)~~>nginx:latest /usr/share/nginx/html /usr/share/nginx/html + RUN apk add --no-cache bash + WORKDIR /usr/share/nginx/html + CMD ["ls", "-la", "/usr/share/nginx/html"] + """, + spec -> spec.path("Dockerfile") + ) + ); + } + + @Test + void dockerMultipleStageFileWithFromOptionAsStageNumber() { + rewriteRun( + assertImages("golang:1.23", "scratch"), + text( + //language=Dockerfile + """ + # syntax=docker/dockerfile:1 + FROM golang:1.23 + WORKDIR /src + COPY <<EOF ./main.go + package main + + import "fmt" + + func main() { + fmt.Println("hello, world") + } + EOF + + RUN go build -o /bin/hello ./main.go + + FROM scratch + COPY --from=0 /bin/hello /bin/hello + CMD ["/bin/hello"] + """, + """ + # syntax=docker/dockerfile:1 + FROM ~~(golang:1.23)~~>golang:1.23 + WORKDIR /src + COPY <<EOF ./main.go + package main + + import "fmt" + + func main() { + fmt.Println("hello, world") + } + EOF + + RUN go build -o /bin/hello ./main.go + + FROM ~~(scratch)~~>scratch + COPY --from=0 /bin/hello /bin/hello + CMD ["/bin/hello"] """, spec -> spec.path("Dockerfile") ) ); } + + + @Test + void platformDockerfile() { + rewriteRun( + assertImages("alpine:latest"), + text( + //language=Dockerfile + """ + FROM --platform=linux/arm64 alpine:latest + RUN echo "Hello from ARM64!" > /message.txt + CMD ["cat", "/message.txt"] + """, + """ + FROM --platform=linux/arm64 ~~(alpine:latest)~~>alpine:latest + RUN echo "Hello from ARM64!" > /message.txt + CMD ["cat", "/message.txt"] + """, + spec -> spec.path("Dockerfile") + ) + ); + } + + @Test + void dockerFileIgnoreComment() { + rewriteRun( + assertImages("alpine:latest"), + text( + //language=Dockerfile + """ + # FROM alpine + FROM alpine:latest + """, + """ + # FROM alpine + FROM ~~(alpine:latest)~~>alpine:latest + """, + spec -> spec.path("Dockerfile") + ) + ); + } + + @Test + void gitlabCIFile() { + rewriteRun( + assertImages("maven:latest"), + //language=yaml + yaml( + """ + image: maven:latest + + variables: + MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" + MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" + + cache: + paths: + - .m2/repository/ + - target/ + + build: + stage: build + script: + - mvn $MAVEN_CLI_OPTS compile + + test: + stage: test + script: + - mvn $MAVEN_CLI_OPTS test + + deploy: + stage: deploy + script: + - mvn $MAVEN_CLI_OPTS deploy + only: + - master + """, + """ + image: ~~(maven:latest)~~>maven:latest + + variables: + MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" + MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" + + cache: + paths: + - .m2/repository/ + - target/ + + build: + stage: build + script: + - mvn $MAVEN_CLI_OPTS compile + + test: + stage: test + script: + - mvn $MAVEN_CLI_OPTS test + + deploy: + stage: deploy + script: + - mvn $MAVEN_CLI_OPTS deploy + only: + - master + """, + spec -> spec.path(".gitlab-ci") + ) + ); + } + + @Test + void githubActions() { + rewriteRun( + assertImages("ghcr.io/owner/image"), + //language=yaml + yaml( + """ + container: + image: ghcr.io/owner/image + """, + """ + container: + image: ~~(ghcr.io/owner/image)~~>ghcr.io/owner/image + """, + spec -> spec.path(".github/workflows/ci.yml") + ) + ); + } + + @Test + void kubernetesFile() { + rewriteRun( + assertImages("image", "app:v1.2.3", "account/image:latest", "repo.id/account/bucket/image:v1.2.3@digest"), + //language=yaml + yaml( + """ + apiVersion: v1 + kind: Pod + spec: + containers: + - image: image + name: image-container + --- + apiVersion: v1 + kind: Pod + spec: + containers: + - name: my-container + image: app:v1.2.3 + initContainers: + - image: account/image:latest + name: my-init-container + --- + apiVersion: v1 + kind: Pod + spec: + containers: + - image: repo.id/account/bucket/image:v1.2.3@digest + name: my-container + """, + """ + apiVersion: v1 + kind: Pod + spec: + containers: + - image: ~~(image)~~>image + name: image-container + --- + apiVersion: v1 + kind: Pod + spec: + containers: + - name: my-container + image: ~~(app:v1.2.3)~~>app:v1.2.3 + initContainers: + - image: ~~(account/image:latest)~~>account/image:latest + name: my-init-container + --- + apiVersion: v1 + kind: Pod + spec: + containers: + - image: ~~(repo.id/account/bucket/image:v1.2.3@digest)~~>repo.id/account/bucket/image:v1.2.3@digest + name: my-container + """, + spec -> spec.path(".gitlab-ci") + ) + ); + } + + private static Consumer<RecipeSpec> assertImages(String... expected) { + return spec -> spec.recipe(new FindDockerImageUses()) + .dataTable(DockerBaseImages.Row.class, rows -> + assertThat(rows) + .hasSize(expected.length) + .extracting(it -> it.getImageName() + (it.getTag() == null ? "" : ":" + it.getTag())) + .containsExactlyInAnyOrder(expected) + ); + } }