Add buildpack option for image building
This commit adds configuration to the Maven and Gradle plugins to allow a list of buildpacks to be provided to the image building goal and task. Fixes gh-21722
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,11 +23,21 @@ import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.gradle.testkit.runner.BuildResult;
|
||||
import org.gradle.testkit.runner.TaskOutcome;
|
||||
import org.junit.jupiter.api.TestTemplate;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
|
||||
@@ -145,7 +155,72 @@ class BootBuildImageIntegrationTests {
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithLaunchScript() {
|
||||
void buildsImageWithBuildpackFromBuilder() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
|
||||
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
|
||||
}
|
||||
finally {
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
void buildsImageWithBuildpackFromDirectory() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
writeBuildpackContent();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Hello World buildpack");
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
void buildsImageWithBuildpackFromTarGzip() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
writeBuildpackContent();
|
||||
tarGzipBuildpackContent();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Hello World buildpack");
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void buildsImageWithBuildpackFromImage() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
|
||||
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
|
||||
}
|
||||
finally {
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithLaunchScript() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage");
|
||||
@@ -154,7 +229,7 @@ class BootBuildImageIntegrationTests {
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithBuilderError() {
|
||||
void failsWithBuilderError() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
@@ -163,7 +238,7 @@ class BootBuildImageIntegrationTests {
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithInvalidImageName() {
|
||||
void failsWithInvalidImageName() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--imageName=example/Invalid-Image-Name");
|
||||
@@ -173,7 +248,7 @@ class BootBuildImageIntegrationTests {
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithPublishMissingPublishRegistry() {
|
||||
void failsWithPublishMissingPublishRegistry() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--publishImage");
|
||||
@@ -182,7 +257,7 @@ class BootBuildImageIntegrationTests {
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithWarPackaging() {
|
||||
void failsWithWarPackaging() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "-PapplyWarPlugin");
|
||||
@@ -190,6 +265,15 @@ class BootBuildImageIntegrationTests {
|
||||
assertThat(result.getOutput()).contains("Executable jar file required for building image");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithBuildpackNotInBuilder() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
|
||||
assertThat(result.getOutput()).contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void buildsImageWithWarPackagingAndJarConfiguration() throws IOException {
|
||||
writeMainClass();
|
||||
@@ -210,7 +294,7 @@ class BootBuildImageIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMainClass() {
|
||||
private void writeMainClass() throws IOException {
|
||||
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||
examplePackage.mkdirs();
|
||||
File main = new File(examplePackage, "Main.java");
|
||||
@@ -230,23 +314,70 @@ class BootBuildImageIntegrationTests {
|
||||
writer.println();
|
||||
writer.println("}");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
private void writeLongNameResource() throws IOException {
|
||||
StringBuilder name = new StringBuilder();
|
||||
new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i));
|
||||
Path path = this.gradleBuild.getProjectDir().toPath()
|
||||
.resolve(Paths.get("src", "main", "resources", name.toString()));
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
}
|
||||
|
||||
private void writeBuildpackContent() throws IOException {
|
||||
File buildpackDir = new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world");
|
||||
buildpackDir.mkdirs();
|
||||
File descriptor = new File(buildpackDir, "buildpack.toml");
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(descriptor))) {
|
||||
writer.println("api = \"0.2\"");
|
||||
writer.println("[buildpack]");
|
||||
writer.println("id = \"example/hello-world\"");
|
||||
writer.println("version = \"0.0.1\"");
|
||||
writer.println("name = \"Hello World Buildpack\"");
|
||||
writer.println("homepage = \"https://github.com/buildpacks/samples/tree/main/buildpacks/hello-world\"");
|
||||
writer.println("[[stacks]]\n");
|
||||
writer.println("id = \"io.buildpacks.stacks.bionic\"");
|
||||
}
|
||||
File binDir = new File(buildpackDir, "bin");
|
||||
binDir.mkdirs();
|
||||
FileAttribute<Set<PosixFilePermission>> attribute = PosixFilePermissions
|
||||
.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx"));
|
||||
File detect = Files.createFile(Paths.get(binDir.getAbsolutePath(), "detect"), attribute).toFile();
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(detect))) {
|
||||
writer.println("#!/usr/bin/env bash");
|
||||
writer.println("set -eo pipefail");
|
||||
writer.println("exit 0");
|
||||
}
|
||||
File build = Files.createFile(Paths.get(binDir.getAbsolutePath(), "build"), attribute).toFile();
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(build))) {
|
||||
writer.println("#!/usr/bin/env bash");
|
||||
writer.println("set -eo pipefail");
|
||||
writer.println("echo \"---> Hello World buildpack\"");
|
||||
writer.println("echo \"---> done\"");
|
||||
writer.println("exit 0");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLongNameResource() {
|
||||
StringBuilder name = new StringBuilder();
|
||||
new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i));
|
||||
try {
|
||||
Path path = this.gradleBuild.getProjectDir().toPath()
|
||||
.resolve(Paths.get("src", "main", "resources", name.toString()));
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
private void tarGzipBuildpackContent() throws IOException {
|
||||
Path tarGzipPath = Paths.get(this.gradleBuild.getProjectDir().getAbsolutePath(), "hello-world.tgz");
|
||||
try (TarArchiveOutputStream tar = new TarArchiveOutputStream(
|
||||
new GzipCompressorOutputStream(Files.newOutputStream(Files.createFile(tarGzipPath))))) {
|
||||
writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/buildpack.toml"),
|
||||
"buildpack.toml", 0644);
|
||||
writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/detect"),
|
||||
"bin/detect", 0777);
|
||||
writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/build"),
|
||||
"bin/build", 0777);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFileToTar(TarArchiveOutputStream tar, File file, String name, int mode) throws IOException {
|
||||
TarArchiveEntry entry = new TarArchiveEntry(file, name);
|
||||
entry.setMode(mode);
|
||||
tar.putArchiveEntry(entry);
|
||||
IOUtils.copy(Files.newInputStream(file.toPath()), tar);
|
||||
tar.closeArchiveEntry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.gradle.tasks.bundling;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -28,6 +29,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -221,4 +223,31 @@ class BootBuildImageTests {
|
||||
assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.NEVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoBuildpacksAreConfiguredThenRequestUsesDefaultBuildpacks() {
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBuildpacksAreConfiguredThenRequestHasBuildpacks() {
|
||||
this.buildImage.setBuildpacks(Arrays.asList("example/buildpack1", "example/buildpack2"));
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly(
|
||||
BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() {
|
||||
this.buildImage.buildpacks(Arrays.asList("example/buildpack1", "example/buildpack2"));
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly(
|
||||
BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenIndividualEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() {
|
||||
this.buildImage.buildpack("example/buildpack1");
|
||||
this.buildImage.buildpack("example/buildpack2");
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly(
|
||||
BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
||||
import com.sun.jna.Platform;
|
||||
import io.spring.gradle.dependencymanagement.DependencyManagementPlugin;
|
||||
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension;
|
||||
import org.antlr.v4.runtime.Lexer;
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.conn.HttpClientConnectionManager;
|
||||
@@ -49,6 +50,7 @@ import org.jetbrains.kotlin.daemon.client.KotlinCompilerClient;
|
||||
import org.jetbrains.kotlin.gradle.model.KotlinProject;
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin;
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPlugin;
|
||||
import org.tomlj.Toml;
|
||||
|
||||
import org.springframework.asm.ClassVisitor;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
@@ -116,7 +118,8 @@ public class GradleBuild {
|
||||
new File(pathOfJarContaining(HttpRequest.class)), new File(pathOfJarContaining(Module.class)),
|
||||
new File(pathOfJarContaining(Versioned.class)),
|
||||
new File(pathOfJarContaining(ParameterNamesModule.class)),
|
||||
new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class)));
|
||||
new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class)),
|
||||
new File(pathOfJarContaining(Toml.class)), new File(pathOfJarContaining(Lexer.class)));
|
||||
}
|
||||
|
||||
private String pathOfJarContaining(Class<?> type) {
|
||||
|
||||
Reference in New Issue
Block a user