Skip to content

Commit

Permalink
Merge pull request #90 from kumadee/master
Browse files Browse the repository at this point in the history
feat: Specify `targetStage` in multi-stage container build
  • Loading branch information
lexemmens authored Aug 24, 2023
2 parents 9de0c88 + d07a78a commit 3108e83
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 2 deletions.
7 changes: 7 additions & 0 deletions docs/modules/ROOT/pages/goals/build.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ When this option is specified _and_ `pullAlways` is also specified, builds will
|tags
|An array consisting of one or more tags to attach to the built container image. Tags will be appended at the end of the image name.

|targetStage
|Set the target build stage to build. When building a Containerfile with multiple build stages, it can be used to specify an intermediate build stage by name as the final stage for the resulting image. Commands after the target stage is skipped.

**Default value is**: `null` (not specified).

**See**: https://docs.podman.io/en/latest/markdown/podman-build.1.html

|containerFile
|The name of the `Containerfile` to build. If you are using a `Dockerfile` you should change this parameter.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class PodmanBuildCommand extends AbstractPodmanCommand {
private static final String NO_CACHE_CMD = "--no-cache";
private static final String BUILD_ARG_CMD = "--build-arg";
private static final String PLATFORM_CMD = "--platform";
private static final String TARGET_STAGE_CMD = "--target";
private static final String SUBCOMMAND = "build";

private PodmanBuildCommand(Log log, PodmanConfiguration podmanConfig, CommandExecutorDelegate delegate) {
Expand Down Expand Up @@ -149,6 +150,17 @@ public Builder setPlatform(String platform){
return this;
}

/**
* Sets the platform for the resulting image rather using the default of the build system
*
* @param targetStage Sets the target build stage to build in a multi-stage build.
* @return This builder instance
*/
public Builder setTargetStage(String targetStage){
command.withOption(TARGET_STAGE_CMD, targetStage);
return this;
}

public Builder addBuildArgs(Map<String, String> args) {
Map<String, String> allBuildArgs = new HashMap<>(args);
allBuildArgs.putAll(getBuildArgsFromSystem());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ public abstract class AbstractImageBuildConfiguration {
@Parameter
protected String platform;

/**
* Specify the final target stage to build. It is passed to the "--target" option of podman.
* <p>
*
* @see "https://docs.podman.io/en/latest/markdown/podman-build.1.html"
*/
@Parameter
protected String targetStage;

/**
* Will be set when this class is validated using the #initAndValidate() method
*/
Expand Down Expand Up @@ -340,6 +349,15 @@ public Optional<String> getPlatform(){
return Optional.ofNullable(platform);
}

/**
* Returns the final target stage to build.
*
* @return if set, the final target stage to build defined in Containerfile.
*/
public Optional<String> getTargetStage(){
return Optional.ofNullable(targetStage);
}

/**
* Returns a boolean indicating whether this configuration is valid
*
Expand Down Expand Up @@ -369,6 +387,7 @@ protected boolean isContainerFileEmpty(Log log, Path fullContainerFilePath) thro
}

protected void determineBuildStages(Log log, Path fullContainerFilePath) throws MojoExecutionException {
boolean foundTargetStage = false;
try (Stream<String> containerFileStream = Files.lines(fullContainerFilePath)) {
List<String> content = containerFileStream.collect(Collectors.toList());
for (String line : content) {
Expand All @@ -377,7 +396,9 @@ protected void determineBuildStages(Log log, Path fullContainerFilePath) throws
isMultistageContainerFile = true;

String stage = matcher.group(3);

if (Objects.equals(stage, targetStage)) {
foundTargetStage = true;
}
log.debug("Found a stage named: " + stage);
}
}
Expand All @@ -386,6 +407,12 @@ protected void determineBuildStages(Log log, Path fullContainerFilePath) throws
log.error(msg, e);
throw new MojoExecutionException(msg, e);
}

if (targetStage != null && isMultistageContainerFile && !foundTargetStage) {
String msg = String.format("Target stage '%s' was not found in the given Containerfile.", targetStage);
log.error(msg);
throw new MojoExecutionException(msg);
}
}

/**
Expand Down Expand Up @@ -516,6 +543,15 @@ public void setContainerFileDir(File containerFileDir) {
this.containerFileDir = containerFileDir;
}

/**
* Sets the final target stage to build.
*
* @param targetStage The final target stage to build.
*/
public void setTargetStage(String targetStage) {
this.targetStage = targetStage;
}

/**
* Retrieves a collection of build arguments to provide to podman build using --build-arg
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ public List<String> build(SingleImageConfiguration image) throws MojoExecutionEx
if (platform.isPresent()) {
builder = builder.setPlatform(platform.get());
}


Optional<String> targetStage = image.getBuild().getTargetStage();
if (targetStage.isPresent()) {
builder.setTargetStage(targetStage.get());
}

builder.addBuildArgs(image.getBuild().getArgs());

return builder.build().execute();
Expand Down
87 changes: 87 additions & 0 deletions src/test/java/nl/lexemmens/podman/BuildMojoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,93 @@ public void testMultiStageBuildWithCustomTagPerStagePodman1x() throws MojoExecut
);
}

@Test
public void testMultiStageBuildWithUnknownTargetStageThrowsException() {
PodmanConfiguration podman = new TestPodmanConfigurationBuilder().setTlsVerify(TlsVerify.FALSE).build();
String unknownStage = "healthcheck-builder";
SingleImageConfiguration image = new TestSingleImageConfigurationBuilder("sample")
.setContainerfileDir("src/test/resources/multistagecontainerfile")
.setTags(new String[]{"0.2.1"})
.setCreateLatestTag(false)
.setTargetStage(unknownStage)
.build();

configureMojo(podman, image, true, false, false, true, true);
when(mavenProject.getBuild()).thenReturn(build);
when(build.getDirectory()).thenReturn("target");

try {
buildMojo.execute();
fail("Expected an exception, however none was thrown.");
} catch (MojoExecutionException e) {
assertEquals(String.format("Target stage '%s' was not found in the given Containerfile.", unknownStage), e.getMessage());
}
}

@Test
public void testMultiStageBuildWithGivenTargetStage() throws MojoExecutionException, IOException, URISyntaxException {
URI sampleBuildOutputUri = PushMojoTest.class.getResource("/multistagecontainerfile/samplebuildoutput-phase2.txt").toURI();
Path sampleBuildOutputPath = Paths.get(sampleBuildOutputUri);

List<String> buildOutputUnderTest;
try (Stream<String> buildSampleOutput = Files.lines(sampleBuildOutputPath)) {
buildOutputUnderTest = buildSampleOutput.collect(Collectors.toList());
}

Assertions.assertNotNull(buildOutputUnderTest);

String targetStage = "phase";
PodmanConfiguration podman = new TestPodmanConfigurationBuilder().setTlsVerify(TlsVerify.FALSE).build();
SingleImageConfiguration image = new TestSingleImageConfigurationBuilder("sample")
.setContainerfileDir("src/test/resources/multistagecontainerfile")
.setTags(new String[]{"0.2.1"})
.setCreateLatestTag(false)
.setTargetStage(targetStage)
.build();
configureMojo(podman, image, true, false, false, false, true);

when(mavenProject.getBuild()).thenReturn(build);
when(build.getDirectory()).thenReturn("target");
when(serviceHubFactory.createServiceHub(isA(Log.class), isA(MavenProject.class), isA(MavenFileFilter.class), isA(PodmanConfiguration.class), isA(SkopeoConfiguration.class), isA(Settings.class), isA(SettingsDecrypter.class), isA(MavenProjectHelper.class))).thenReturn(serviceHub);
when(serviceHub.getContainerfileDecorator()).thenReturn(containerfileDecorator);
when(serviceHub.getPodmanExecutorService()).thenReturn(podmanExecutorService);
when(serviceHub.getSecurityContextService()).thenReturn(securityContextService);
when(serviceHub.getMavenProjectHelper()).thenReturn(mavenProjectHelper);
when(podmanExecutorService.build(isA(SingleImageConfiguration.class))).thenReturn(buildOutputUnderTest);

buildMojo.execute();

// Verify logging
verify(log, times(1)).info("Detected multistage Containerfile...");

// At random verify some lines
verify(log, times(1)).debug("Processing line: 'STEP 1: FROM nexus.example:15000/adoptopenjdk/openjdk11:11.0.3 AS base'");
verify(log, times(1)).debug("Processing candidate: 'STEP 7: LABEL Build-User=sample-user2 Git-Repository-Url=null'");

// Verify stage detection
verify(log, times(1)).debug("Processing stage in Containerfile: base");
verify(log, times(1)).debug("Processing stage in Containerfile: phase");
verify(log, times(0)).debug("Processing stage in Containerfile: phase2");

// Verify hashes for stages
verify(log, times(1)).info("Final image for stage base is: 7e72c870614");
verify(log, times(1)).info("Final image for stage phase is: 7f55eab001a");
verify(log, times(0)).info("Final image for stage phase2 is: d2efc6645cb");

// Verify tagging image
verify(log, times(0)).info("Tagging container image 7f55eab001a from stage phase as registry.example.com/sample:0.2.1");
verify(log, times(0)).info("Tagging container image d2efc6645cb from stage phase2 as registry.example.com/sample:0.2.1");
verify(log, times(1)).info("Tagging container image 7f55eab001adf2dfeas8adc03ef847dd3d2b4fa42b4fa418ca4cdeb6eaef8f3b as registry.example.com/sample:0.2.1");

verify(podmanExecutorService, times(1)).tag("7f55eab001adf2dfeas8adc03ef847dd3d2b4fa42b4fa418ca4cdeb6eaef8f3b", "registry.example.com/sample:0.2.1");

verify(log, times(1)).info("Built container image.");

verifyContainerCatalog(
"registry.example.com/sample:0.2.1"
);
}

private void configureMojo(PodmanConfiguration podman, SingleImageConfiguration image, boolean skipAuth, boolean skipAll, boolean skipBuild, boolean skipTag, boolean failOnMissingContainerFile) {
buildMojo.podman = podman;
buildMojo.skip = skipAll;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ public TestSingleImageConfigurationBuilder setPullAlways(boolean pullAlways) {
return this;
}

public TestSingleImageConfigurationBuilder setTargetStage(String targetStage){
image.getBuild().setTargetStage(targetStage);
return this;
}

public SingleImageConfiguration build() {
return image;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,30 @@ public void testBuildWithCustomRootAndRunRootDir() throws MojoExecutionException
delegate.getCommandAsString());
}

@Test
public void testTargetStage() throws MojoExecutionException {
when(mavenProject.getBuild()).thenReturn(build);
when(build.getDirectory()).thenReturn("target");

PodmanConfiguration podmanConfig = new TestPodmanConfigurationBuilder().setTlsVerify(TRUE).initAndValidate(mavenProject, log).build();
SingleImageConfiguration image = new TestSingleImageConfigurationBuilder("test_image")
.setFormat(OCI)
.setLayers(false)
.setContainerfileDir("src/test/resources")
.setTargetStage("phase2")
.initAndValidate(mavenProject, log, true)
.build();

String sampleImageHash = "this_would_normally_be_an_image_hash";
InterceptorCommandExecutorDelegate delegate = new InterceptorCommandExecutorDelegate(Collections.singletonList(sampleImageHash));
podmanExecutorService = new PodmanExecutorService(log, podmanConfig, delegate);

podmanExecutorService.build(image);

Assertions.assertEquals("podman build --tls-verify=true --format=oci --file="
+ image.getBuild().getTargetContainerFile() + " --no-cache=false --layers=false --target=phase2 .", delegate.getCommandAsString());

}

private static class InterceptorCommandExecutorDelegate implements CommandExecutorDelegate {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Building container image...
STEP 1: FROM nexus.example:15000/adoptopenjdk/openjdk11:11.0.3 AS base
Getting image source signatures
Copying blob sha256:df15d82c4ffe976522b836c1f0c300e289f797cb2e302df1b7d0b917af7d3795
Copying blob sha256:c29f4698c17768b4bf36dde25d584231424db9f4b9c9e67cce8980cfc50efbc1
Copying blob sha256:b08df05e829707fdd711d6669671a0dd0431a20ca7ae89d7cd415e0025ba3a84
Copying blob sha256:ad26192fb1166465550d702a8418c67ecda7b089149870f48673d8f0fb5ab8c1
Copying blob sha256:95c2c089cb32f9959093f091b223b04c3a8e0e0aa8b703c0052ebfe9be5f0b3c
Copying blob sha256:67300986eedeec2c7442a3b30e65142200b72dc72dc6da2e945a0f06d5e9183b
Copying config sha256:c83be72afce332f5c3c3ae4992f3340479fee3115d7f449afebf39d0b6eb16b6
Writing manifest to image destination
Storing signatures
STEP 2: LABEL Build-User=example Git-Repository-Url=null
--> ab579b718da
STEP 3: ENV RUN_CMD="exec java -jar a-sample-jar-file.jar"
--> d8874e40423
STEP 4: WORKDIR /application
--> 932d8db203e
STEP 5: ENTRYPOINT /application
--> 7e72c870614
STEP 6: FROM 7e72c870614c842cefe268dec15cd84d8abd64be16a0c4f76d4883846b1e6104 AS phase
STEP 7: LABEL Build-User=sample-user2 Git-Repository-Url=null
--> f6d4d237662
STEP 8: COPY target/a-sample-jar-file.jar ./
--> 1149d5e6695
STEP 9: ENTRYPOINT ${RUN_CMD}
--> 7f55eab001a
7f55eab001adf2dfeas8adc03ef847dd3d2b4fa42b4fa418ca4cdeb6eaef8f3b

0 comments on commit 3108e83

Please sign in to comment.