Skip to content
Open
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 @@ -9,11 +9,12 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
* Representation of a Dockerfile, with partial parsing for extraction of a minimal set of data.
Expand All @@ -22,7 +23,7 @@
public class ParsedDockerfile {

private static final Pattern FROM_LINE_PATTERN = Pattern.compile(
"FROM (?<arg>--[^\\s]+\\s)*(?<image>[^\\s]+).*",
"FROM (?<arg>--[^\\s]+\\s)*(?<image>[^\\s]+)(\\s+AS\\s+(?<stage>[^\\s]+))?.*",
Pattern.CASE_INSENSITIVE
);

Expand Down Expand Up @@ -57,12 +58,25 @@ private List<String> read() {
}

private Set<String> parse(List<String> lines) {
Set<String> imageNames = lines
.stream()
.map(FROM_LINE_PATTERN::matcher)
.filter(Matcher::matches)
.map(matcher -> matcher.group("image"))
.collect(Collectors.toSet());
Set<String> imageNames = new HashSet<>();
Set<String> buildStageNames = new HashSet<>();

for (String line : lines) {
Matcher matcher = FROM_LINE_PATTERN.matcher(line);
if (!matcher.matches()) {
continue;
}
// A FROM referring to a previously declared build stage (e.g. `FROM builder`) is an internal
// stage reference, not an external image to be pulled. Build stage names are case-insensitive.
String image = matcher.group("image");
if (!buildStageNames.contains(image.toLowerCase(Locale.ROOT))) {
imageNames.add(image);
}
String stage = matcher.group("stage");
if (stage != null) {
buildStageNames.add(stage.toLowerCase(Locale.ROOT));
}
}

if (!imageNames.isEmpty()) {
log.debug("Found dependency images in Dockerfile {}: {}", dockerFilePath, imageNames);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,53 @@ void ignoringExtraPlatformArgs() {
.isEqualTo(Sets.newHashSet("someimage"));
}

@Test
void doesNotTreatBuildStageReferencesAsDependencyImages() {
final ParsedDockerfile parsedDockerfile = new ParsedDockerfile(
Arrays.asList("FROM alpine:3.17 AS build", "RUN something", "FROM build", "RUN somethingelse")
);
assertThat(parsedDockerfile.getDependencyImageNames())
.as("does not treat references to a previously declared build stage as images to pull")
.isEqualTo(Sets.newHashSet("alpine:3.17"));
}

@Test
void extractsOnlyExternalImagesFromMultiStageDockerfile() {
final ParsedDockerfile parsedDockerfile = new ParsedDockerfile(
Arrays.asList(
"FROM maven:3.9-eclipse-temurin-17 AS build",
"COPY . /workspace",
"RUN mvn -f /workspace package",
"FROM eclipse-temurin:17-jre",
"COPY --from=build /workspace/target/app.jar /app.jar",
"CMD [\"java\", \"-jar\", \"/app.jar\"]"
)
);
assertThat(parsedDockerfile.getDependencyImageNames())
.as("extracts only external images from a multi-stage Dockerfile")
.isEqualTo(Sets.newHashSet("maven:3.9-eclipse-temurin-17", "eclipse-temurin:17-jre"));
}

@Test
void matchesBuildStageNamesCaseInsensitively() {
final ParsedDockerfile parsedDockerfile = new ParsedDockerfile(
Arrays.asList("FROM someimage As Build", "RUN something", "FROM BUILD", "RUN somethingelse")
);
assertThat(parsedDockerfile.getDependencyImageNames())
.as("matches build stage names case-insensitively, as Docker does")
.isEqualTo(Sets.newHashSet("someimage"));
}

@Test
void retainsImagesOfUnreferencedBuildStages() {
final ParsedDockerfile parsedDockerfile = new ParsedDockerfile(
Arrays.asList("FROM someimage AS unusedstage", "RUN something", "FROM nextimage", "RUN somethingelse")
);
assertThat(parsedDockerfile.getDependencyImageNames())
.as("still extracts images of stages whose alias is never referenced")
.isEqualTo(Sets.newHashSet("someimage", "nextimage"));
}

@Test
void handlesGracefullyIfNoFromLine() {
final ParsedDockerfile parsedDockerfile = new ParsedDockerfile(
Expand Down