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:
Scott Frederick
2021-02-18 17:28:25 -06:00
parent d8fe9de682
commit f54f784f80
74 changed files with 3895 additions and 211 deletions

View File

@@ -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();
}
}

View File

@@ -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"));
}
}

View File

@@ -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) {