diff --git a/src/main/asciidoc/inc/build/_buildx.adoc b/src/main/asciidoc/inc/build/_buildx.adoc index 6d6304268..480d3ec9f 100644 --- a/src/main/asciidoc/inc/build/_buildx.adoc +++ b/src/main/asciidoc/inc/build/_buildx.adoc @@ -46,6 +46,10 @@ is relative to the user's home directory. linux/arm64, darwin/amd64). Each `` element may have a comma separated list of platforms. Empty `` elements are ignored. If no platform architecture is specified, buildx is *not* used. You can use +| *attestations* +| The configuration of attestation modes. The `` element may be set to `min`, +`max`, or `false`. The `` element may be set to `true` or `false`. The `` +element defaults to `min` and the `` element defaults to `false`. |=== .Examples diff --git a/src/main/java/io/fabric8/maven/docker/config/AttestationConfiguration.java b/src/main/java/io/fabric8/maven/docker/config/AttestationConfiguration.java new file mode 100644 index 000000000..e15945a79 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/config/AttestationConfiguration.java @@ -0,0 +1,58 @@ +package io.fabric8.maven.docker.config; + +import io.fabric8.maven.docker.util.EnvUtil; +import java.io.Serializable; +import java.util.List; +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Attestation Configuration + */ +public class AttestationConfiguration implements Serializable { + + /** + * Provenance attestation mode; one of true, false, min, max + */ + @Parameter + private String provenance; + + /** + * Enable Software Bill of Materials attestation + */ + @Parameter + private Boolean sbom; + + public String getProvenance() { + return provenance; + } + + public Boolean getSbom() { + return sbom; + } + + public static class Builder { + + private final AttestationConfiguration config = new AttestationConfiguration(); + private boolean isEmpty = true; + + public AttestationConfiguration build() { + return isEmpty ? null : config; + } + + public AttestationConfiguration.Builder provenance(String provenance) { + config.provenance = provenance; + if (provenance != null) { + isEmpty = false; + } + return this; + } + + public AttestationConfiguration.Builder sbom(Boolean sbom) { + config.sbom = sbom; + if (sbom != null) { + isEmpty = false; + } + return this; + } + } +} diff --git a/src/main/java/io/fabric8/maven/docker/config/BuildXConfiguration.java b/src/main/java/io/fabric8/maven/docker/config/BuildXConfiguration.java index 0b9e3c8e4..e746b436c 100644 --- a/src/main/java/io/fabric8/maven/docker/config/BuildXConfiguration.java +++ b/src/main/java/io/fabric8/maven/docker/config/BuildXConfiguration.java @@ -14,6 +14,7 @@ public class BuildXConfiguration implements Serializable { */ @Parameter private String builderName; + /** * Configuration file to create builder */ @@ -32,10 +33,11 @@ public class BuildXConfiguration implements Serializable { @Parameter private List platforms; - @Nonnull - public List getPlatforms() { - return EnvUtil.splitAtCommasAndTrim(platforms); - } + /** + * Attestation configuration + */ + @Parameter + private AttestationConfiguration attestations; public String getBuilderName() { return builderName; @@ -53,6 +55,14 @@ public boolean isBuildX() { return !getPlatforms().isEmpty(); } + @Nonnull + public List getPlatforms() { + return EnvUtil.splitAtCommasAndTrim(platforms); + } + + public AttestationConfiguration getAttestations() { + return attestations; + } public static class Builder { @@ -94,5 +104,13 @@ public Builder platforms(List platforms) { } return this; } + + public Builder attestations(AttestationConfiguration attestations) { + config.attestations = attestations; + if (attestations != null) { + isEmpty = false; + } + return this; + } } } diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java b/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java index 0bb3aebd7..0810a94ac 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java @@ -45,6 +45,8 @@ public enum ConfigKey { BUILDX_BUILDERNAME("buildx.builderName"), BUILDX_CONFIGFILE("buildx.configFile"), BUILDX_DOCKERSTATEDIR("buildx.dockerStateDir"), + BUILDX_ATTESTATION_PROVENANCE("buildx.attestations.provenance"), + BUILDX_ATTESTATION_SBOM("buildx.attestations.sbom"), CAP_ADD, CAP_DROP, SYSCTLS, diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java b/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java index 9055b0a76..af247c1c1 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java @@ -15,6 +15,7 @@ * limitations under the License. */ +import io.fabric8.maven.docker.config.AttestationConfiguration; import java.io.File; import java.util.ArrayList; import java.util.Collections; @@ -335,6 +336,18 @@ private BuildXConfiguration extractBuildx(BuildXConfiguration config, ValueProvi .configFile(valueProvider.getString(BUILDX_CONFIGFILE, config.getConfigFile())) .dockerStateDir(valueProvider.getString(BUILDX_DOCKERSTATEDIR, config.getDockerStateDir())) .platforms(valueProvider.getList(BUILDX_PLATFORMS, config.getPlatforms())) + .attestations(extractAttestations(config.getAttestations(), valueProvider)) + .build(); + } + + private AttestationConfiguration extractAttestations(AttestationConfiguration config, ValueProvider valueProvider) { + if (config == null) { + config = new AttestationConfiguration(); + } + + return new AttestationConfiguration.Builder() + .provenance(valueProvider.getString(BUILDX_ATTESTATION_PROVENANCE, config.getProvenance())) + .sbom(valueProvider.getBoolean(BUILDX_ATTESTATION_SBOM, config.getSbom())) .build(); } diff --git a/src/main/java/io/fabric8/maven/docker/service/BuildXService.java b/src/main/java/io/fabric8/maven/docker/service/BuildXService.java index 254a53636..7a1e9d1fb 100644 --- a/src/main/java/io/fabric8/maven/docker/service/BuildXService.java +++ b/src/main/java/io/fabric8/maven/docker/service/BuildXService.java @@ -4,6 +4,7 @@ import io.fabric8.maven.docker.access.DockerAccess; import io.fabric8.maven.docker.assembly.BuildDirs; import io.fabric8.maven.docker.assembly.DockerAssemblyManager; +import io.fabric8.maven.docker.config.AttestationConfiguration; import io.fabric8.maven.docker.config.BuildImageConfiguration; import io.fabric8.maven.docker.config.BuildXConfiguration; import io.fabric8.maven.docker.config.ImageConfiguration; @@ -116,19 +117,13 @@ protected void buildX(List buildX, String builderName, BuildDirs buildDi BuildImageConfiguration buildConfiguration = imageConfig.getBuildConfiguration(); List cmdLine = new ArrayList<>(buildX); - cmdLine.add("build"); - cmdLine.add("--progress=plain"); - cmdLine.add("--builder"); - cmdLine.add(builderName); - cmdLine.add("--platform"); - cmdLine.add(String.join(",", platforms)); + append(cmdLine, "build", "--progress=plain", "--builder", builderName, "--platform", + String.join(",", platforms), "--tag", + new ImageName(imageConfig.getName()).getFullName(configuredRegistry)); buildConfiguration.getTags().forEach(t -> { - cmdLine.add("--tag"); - cmdLine.add(new ImageName(imageConfig.getName(), t).getFullName(configuredRegistry)); - } - ); - cmdLine.add("--tag"); - cmdLine.add(new ImageName(imageConfig.getName()).getFullName(configuredRegistry)); + cmdLine.add("--tag"); + cmdLine.add(new ImageName(imageConfig.getName(), t).getFullName(configuredRegistry)); + }); Map args = buildConfiguration.getArgs(); if (args != null) { @@ -137,24 +132,46 @@ protected void buildX(List buildX, String builderName, BuildDirs buildDi cmdLine.add(key + '=' + value); }); } + + AttestationConfiguration attestations = buildConfiguration.getBuildX().getAttestations(); + if (attestations != null) { + if (Boolean.TRUE.equals(attestations.getSbom())) { + cmdLine.add("--sbom=true"); + } + String provenance = attestations.getProvenance(); + if (provenance != null) { + switch (provenance) { + case "min": + case "max": + cmdLine.add("--provenance=mode=" + provenance); + break; + case "false": + case "true": + cmdLine.add("--provenance=" + provenance); + break; + default: + logger.error("Unsupported provenance mode %s", provenance); + } + } + } + if (buildConfiguration.squash()) { cmdLine.add("--squash"); } - if (extraParam != null) { - cmdLine.add(extraParam); - } - String[] ctxCmds; File contextDir = buildConfiguration.getContextDir(); if (contextDir != null) { Path destinationPath = getContextPath(buildArchive); Path dockerFileRelativePath = contextDir.toPath().relativize(buildConfiguration.getDockerFile().toPath()); - ctxCmds = new String[] { "--file=" + destinationPath.resolve(dockerFileRelativePath), destinationPath.toString() }; + append(cmdLine, "--file=" + destinationPath.resolve(dockerFileRelativePath), destinationPath.toString()); } else { - ctxCmds = new String[] { buildDirs.getOutputDirectory().getAbsolutePath() }; + cmdLine.add(buildDirs.getOutputDirectory().getAbsolutePath()); + } + if (extraParam != null) { + cmdLine.add(extraParam); } - int rc = exec.process(cmdLine, ctxCmds); + int rc = exec.process(cmdLine); if (rc != 0) { throw new MojoExecutionException("Error status (" + rc + ") when building"); } @@ -197,12 +214,14 @@ protected String createBuilder(Path configPath, List buildX, ImageConfig } Path builderPath = configPath.resolve(Paths.get("buildx", "instances", builderName)); if(Files.notExists(builderPath)) { + List cmds = new ArrayList<>(buildX); + append(cmds, "create", "--driver", "docker-container", "--name", builderName); String buildConfig = buildXConfiguration.getConfigFile(); - int rc = exec.process(buildX, buildConfig == null - ? new String[] { "create", "--driver", "docker-container", "--name", builderName } - : new String[] { "create", "--driver", "docker-container", "--name", builderName, "--config", - buildDirs.getProjectPath(EnvUtil.resolveHomeReference(buildConfig)).toString() } - ); + if(buildConfig != null) { + append(cmds, "--config", + buildDirs.getProjectPath(EnvUtil.resolveHomeReference(buildConfig)).toString()); + } + int rc = exec.process(cmds); if (rc != 0) { throw new MojoExecutionException("Error status (" + rc + ") while creating builder " + builderName); } @@ -210,12 +229,17 @@ protected String createBuilder(Path configPath, List buildX, ImageConfig return builderName; } + public static List append(List collection, T... members) { + collection.addAll(Arrays.asList(members)); + return collection; + } + interface Builder { void useBuilder(List buildX, String builderName, BuildDirs buildDirs, ImageConfiguration imageConfig, String configuredRegistry, C context) throws MojoExecutionException; } public interface Exec { - int process(List buildX, String... cmd) throws MojoExecutionException; + int process(List cmdArgs) throws MojoExecutionException; } public static class DefaultExec implements Exec { @@ -225,26 +249,19 @@ public DefaultExec(Logger logger) { this.logger = logger; } - @Override public int process(List buildX, String... cmd) throws MojoExecutionException { - List cmdLine; - if (cmd.length > 0) { - cmdLine = new ArrayList<>(buildX); - cmdLine.addAll(Arrays.asList(cmd)); - } else { - cmdLine = buildX; - } + @Override public int process(List cmdArgs) throws MojoExecutionException { try { - logger.info(String.join(" ", cmdLine)); - ProcessBuilder builder = new ProcessBuilder(cmdLine); + logger.info(String.join(" ", cmdArgs)); + ProcessBuilder builder = new ProcessBuilder(cmdArgs); Process process = builder.start(); pumpStream(process.getInputStream()); pumpStream(process.getErrorStream()); return process.waitFor(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - throw new MojoExecutionException("Interrupted while executing " + cmdLine, ex); + throw new MojoExecutionException("Interrupted while executing " + cmdArgs, ex); } catch (IOException ex) { - throw new MojoExecutionException("unable to execute " + cmdLine, ex); + throw new MojoExecutionException("unable to execute " + cmdArgs, ex); } } diff --git a/src/test/java/io/fabric8/maven/docker/BuildMojoTest.java b/src/test/java/io/fabric8/maven/docker/BuildMojoTest.java index 11c89fda3..41fa7e8fb 100644 --- a/src/test/java/io/fabric8/maven/docker/BuildMojoTest.java +++ b/src/test/java/io/fabric8/maven/docker/BuildMojoTest.java @@ -2,6 +2,7 @@ import io.fabric8.maven.docker.access.DockerAccessException; import io.fabric8.maven.docker.assembly.DockerAssemblyManager; +import io.fabric8.maven.docker.config.AttestationConfiguration; import io.fabric8.maven.docker.config.BuildXConfiguration; import io.fabric8.maven.docker.config.ImageConfiguration; import io.fabric8.maven.docker.service.BuildService; @@ -75,12 +76,12 @@ void buildUsingBuildx() throws IOException, MojoExecutionException { givenBuildXService(); givenMavenProject(buildMojo); - givenResolvedImages(buildMojo, Collections.singletonList(singleBuildXImageWithConfiguration(null))); + givenResolvedImages(buildMojo, Collections.singletonList(singleBuildXImageWithContext(null))); givenPackaging("jar"); whenMojoExecutes(); - thenBuildxRun(null, null, true); + thenBuildxRun(null, null, true, null ); } @Test @@ -93,7 +94,7 @@ void buildUsingConfiguredBuildx() throws IOException, MojoExecutionException { whenMojoExecutes(); - thenBuildxRun("src/docker/builder.toml", null, true); + thenBuildxRun("src/docker/builder.toml", null, true, null ); } @Test @@ -111,7 +112,7 @@ void buildUsingConfiguredBuildxWithContext() throws IOException, MojoExecutionEx whenMojoExecutes(); - thenBuildxRun(null, "src/main/docker", true); + thenBuildxRun(null, "src/main/docker", true,null ); } @Test @@ -128,8 +129,7 @@ void buildUsingBuildxWithNonNative() throws IOException, MojoExecutionException .buildArchive(Mockito.any(), Mockito.any(), Mockito.any()); whenMojoExecutes(); - - thenBuildxRun(null, null, false); + thenBuildxRun(null, null, false,null ); } @Test @@ -145,6 +145,58 @@ void buildUsingBuildxWithSquash() throws IOException, MojoExecutionException { thenBuildxRun(null, null, true, "--squash"); } + @Test + void buildUsingBuildxWithSbom() throws IOException, MojoExecutionException { + givenBuildXService(); + + givenMavenProject(buildMojo); + givenResolvedImages(buildMojo, Collections.singletonList(singleBuildXImageWithAttestations(true, null))); + givenPackaging("jar"); + + whenMojoExecutes(); + + thenBuildxRun(null, null, true, "--sbom=true"); + } + + @Test + void buildUsingBuildxWithMaxProvenance() throws IOException, MojoExecutionException { + givenBuildXService(); + + givenMavenProject(buildMojo); + givenResolvedImages(buildMojo, Collections.singletonList(singleBuildXImageWithAttestations(false, "max"))); + givenPackaging("jar"); + + whenMojoExecutes(); + + thenBuildxRun(null, null, true, "--provenance=mode=max"); + } + + @Test + void buildUsingBuildxWithNoProvenance() throws IOException, MojoExecutionException { + givenBuildXService(); + + givenMavenProject(buildMojo); + givenResolvedImages(buildMojo, Collections.singletonList(singleBuildXImageWithAttestations(false, "false"))); + givenPackaging("jar"); + + whenMojoExecutes(); + + thenBuildxRun(null, null, true, "--provenance=false"); + } + + @Test + void buildUsingBuildxWithIncorrectProvenanceMode() throws IOException, MojoExecutionException { + givenBuildXService(); + + givenMavenProject(buildMojo); + givenResolvedImages(buildMojo, Collections.singletonList(singleBuildXImageWithAttestations(false, "garbage"))); + givenPackaging("jar"); + + whenMojoExecutes(); + + thenBuildxRun(null, null, true, null); + } + private void givenBuildXService() { BuildXService buildXService = new BuildXService(dockerAccess, dockerAssemblyManager, log, exec); @@ -165,33 +217,38 @@ private void verifyBuild(int wantedNumberOfInvocations) throws DockerAccessExcep .buildImage(Mockito.any(ImageConfiguration.class), Mockito.any(ImagePullManager.class), Mockito.any(BuildService.BuildContext.class), Mockito.any()); } - private void thenBuildxRun(String relativeConfigFile, String contextDir, boolean nativePlatformIncluded, String... extraParams) throws MojoExecutionException { + private void thenBuildxRun(String relativeConfigFile, String contextDir, + boolean nativePlatformIncluded, String attestation) throws MojoExecutionException { Path buildPath = projectBaseDirectory.toPath().resolve("target/docker/example/latest"); String config = getOsDependentBuild(buildPath, "docker"); - String buildDir = getOsDependentBuild(buildPath, "build"); String configFile = relativeConfigFile != null ? getOsDependentBuild(projectBaseDirectory.toPath(), relativeConfigFile) : null; - String builderName = "maven"; - - String[] cfgCmdLine = configFile == null - ? new String[] { "create", "--driver", "docker-container", "--name", builderName } - : new String[] { "create", "--driver", "docker-container", "--name", builderName, "--config", configFile.replace('/', File.separatorChar) }; - Mockito.verify(exec).process(Arrays.asList("docker", "--config", config, "buildx"), cfgCmdLine); - - String[] ctxCmdLine; - if (contextDir == null) { - ctxCmdLine = new String[] { buildDir }; - } else { - Path contextPath = tmpDir.resolve("docker-build"); - ctxCmdLine = new String[] { "--file=" + contextPath.resolve("Dockerfile"), contextPath.toString() }; + + List cmds = + BuildXService.append(new ArrayList<>(), "docker", "--config", config, "buildx", + "create", "--driver", "docker-container", "--name", "maven"); + if (configFile != null) { + BuildXService.append(cmds, "--config", configFile.replace('/', File.separatorChar)); } + Mockito.verify(exec).process(cmds); if (nativePlatformIncluded) { - List buildXLine = new ArrayList<>(Arrays.asList("docker", "--config", config, "buildx", - "build", "--progress=plain", "--builder", builderName, - "--platform", NATIVE_PLATFORM, "--tag", "example:latest", "--build-arg", "foo=bar")); - buildXLine.addAll(Arrays.asList(extraParams)); + List buildXLine = BuildXService.append(new ArrayList<>(), "docker", "--config", config, "buildx", + "build", "--progress=plain", "--builder", "maven", + "--platform", NATIVE_PLATFORM, "--tag", "example:latest", "--build-arg", "foo=bar"); + + if (attestation != null) { + buildXLine.add(attestation); + } + + if (contextDir == null) { + buildXLine.add(getOsDependentBuild(buildPath, "build")); + } else { + Path contextPath = tmpDir.resolve("docker-build"); + BuildXService.append(buildXLine, "--file=" + contextPath.resolve("Dockerfile"), contextPath.toString() ); + } + buildXLine.add("--load"); - Mockito.verify(exec).process(buildXLine, ctxCmdLine); + Mockito.verify(exec).process(buildXLine); } } @@ -207,6 +264,11 @@ private void whenMojoExecutes() throws IOException, MojoExecutionException { buildMojo.executeInternal(serviceHub); } + private BuildXConfiguration.Builder getBuildXPlatforms(String... platforms) { + return new BuildXConfiguration.Builder() + .platforms(Arrays.asList(platforms)); + } + private BuildXConfiguration getBuildXConfiguration(String configFile, String... platforms) { return new BuildXConfiguration.Builder() .configFile(configFile) @@ -215,19 +277,27 @@ private BuildXConfiguration getBuildXConfiguration(String configFile, String... } private ImageConfiguration singleBuildXImageWithConfiguration(String configFile) { - return singleImageConfiguration(getBuildXConfiguration(configFile, TWO_BUILDX_PLATFORMS), null); + return singleImageConfiguration( + getBuildXPlatforms(TWO_BUILDX_PLATFORMS).configFile(configFile).build(), null); } private ImageConfiguration singleBuildXImageWithContext(String contextDir) { - return singleImageConfiguration(getBuildXConfiguration(null, TWO_BUILDX_PLATFORMS), contextDir); + return singleImageConfiguration(getBuildXPlatforms(TWO_BUILDX_PLATFORMS).build(), + contextDir); } private ImageConfiguration singleBuildXImageNonNative() { - return singleImageConfiguration(getBuildXConfiguration(null, NON_NATIVE_PLATFORM), null); + return singleImageConfiguration(getBuildXPlatforms(NON_NATIVE_PLATFORM).build(), null); } private ImageConfiguration singleBuildXImageWithSquash() { - return singleImageConfigurationWithBuildWithSquash(getBuildXConfiguration(null, TWO_BUILDX_PLATFORMS), null); + return singleImageConfigurationWithBuildWithSquash( + getBuildXPlatforms(TWO_BUILDX_PLATFORMS).build(), null); } + private ImageConfiguration singleBuildXImageWithAttestations(Boolean sbom, String provenance) { + return singleImageConfiguration(getBuildXPlatforms(TWO_BUILDX_PLATFORMS).attestations( + new AttestationConfiguration.Builder().sbom(sbom).provenance(provenance).build()) + .build(), null); + } } diff --git a/src/test/java/io/fabric8/maven/docker/service/RegistryServiceTest.java b/src/test/java/io/fabric8/maven/docker/service/RegistryServiceTest.java index de213ef30..15e2a532e 100644 --- a/src/test/java/io/fabric8/maven/docker/service/RegistryServiceTest.java +++ b/src/test/java/io/fabric8/maven/docker/service/RegistryServiceTest.java @@ -437,30 +437,29 @@ private void thenBuildxImageHasBeenPushed(String providedBuilder, String relativ String builderName = providedBuilder != null ? providedBuilder : "maven"; if (providedBuilder == null) { - Mockito.verify(exec).process(Arrays.asList("docker", "--config", config, "buildx"), - "create", "--driver", "docker-container", "--name", builderName); + Mockito.verify(exec).process(Arrays.asList("docker", "--config", config, "buildx", "create", "--driver", "docker-container", "--name", builderName)); } - List args = new ArrayList<>(); - args.addAll(Arrays.asList( - "docker", "--config", config, "buildx", - "build", "--progress=plain", "--builder", builderName, - "--platform", "linux/amd64,linux/arm64")); + List cmds = + BuildXService.append(new ArrayList<>(), "docker", "--config", config, "buildx", "build", + "--progress=plain", "--builder", builderName, "--platform", + "linux/amd64,linux/arm64", "--tag", + new ImageName(imageConfiguration.getName()).getFullName(registry)); if (tag) { String tagName = imageConfiguration.getBuildConfiguration().getTags().get(0); - args.addAll(Arrays.asList("--tag", new ImageName(imageConfiguration.getName(), tagName).getFullName(registry))); + BuildXService.append(cmds, "--tag", + new ImageName(imageConfiguration.getName(), tagName).getFullName(registry)); } - args.addAll(Arrays.asList("--tag", new ImageName(imageConfiguration.getName()).getFullName(registry) , "--push")); - String[] cmds; if (relativeDockerfile != null) { Path dockerBuild = buildPath.resolve("tmp/docker-build"); - cmds = new String[] { "--file=" + dockerBuild.resolve(relativeDockerfile), dockerBuild.toString() }; + BuildXService.append(cmds, "--file=" + dockerBuild.resolve(relativeDockerfile), dockerBuild.toString()); } else { - cmds = new String[] { buildDir }; + cmds.add(buildDir); } + BuildXService.append(cmds, "--push"); - Mockito.verify(exec).process(args, cmds); + Mockito.verify(exec).process(cmds); } private void thenImageHasBeenTagged() throws DockerAccessException {