Add support for building OCI images using the Gradle plugin

Closes gh-19831
This commit is contained in:
Andy Wilkinson
2020-01-22 14:54:32 +00:00
parent bc452bc094
commit 0bd0b2a6c0
12 changed files with 533 additions and 8 deletions

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.gradle.tasks.bundling;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.gradle.junit.GradleCompatibilityExtension;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link BootBuildImage}.
*
* @author Andy Wilkinson
*/
@ExtendWith(GradleCompatibilityExtension.class)
@DisabledIfDockerUnavailable
class BootBuildImageIntegrationTests {
GradleBuild gradleBuild;
@TestTemplate
void bootBuildImageBuildsImage() throws IOException {
writeMainClass();
BuildResult result = this.gradleBuild.build("bootBuildImage");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
ImageReference imageReference = ImageReference.of(ImageName.of(this.gradleBuild.getProjectDir().getName()));
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
}
finally {
new DockerApi().image().remove(imageReference, false);
}
}
private void writeMainClass() {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
examplePackage.mkdirs();
File main = new File(examplePackage, "Main.java");
try (PrintWriter writer = new PrintWriter(new FileWriter(main))) {
writer.println("package example;");
writer.println();
writer.println("import java.io.IOException;");
writer.println();
writer.println("public class Main {");
writer.println();
writer.println(" public static void main(String[] args) throws Exception {");
writer.println(" System.out.println(\"Launched\");");
writer.println(" synchronized(args) {");
writer.println(" args.wait(); // Prevent exit");
writer.println(" }");
writer.println(" }");
writer.println();
writer.println("}");
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2012-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.gradle.tasks.bundling;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.gradle.api.Project;
import org.gradle.testfixtures.ProjectBuilder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.build.BuildRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BootBuildImage}.
*
* @author Andy Wilkinson
*/
class BootBuildImageTests {
@TempDir
File temp;
Project project;
private final BootBuildImage buildImage;
BootBuildImageTests() {
File projectDir = new File(this.temp, "project");
projectDir.mkdirs();
this.project = ProjectBuilder.builder().withProjectDir(projectDir).withName("build-image-test").build();
this.project.setDescription("Test project for BootBuildImage");
this.buildImage = this.project.getTasks().create("buildImage", BootBuildImage.class);
}
@Test
void whenProjectVersionIsUnspecifiedThenItIsIgnoredWhenDerivingImageName() {
BuildRequest request = this.buildImage.createRequest();
assertThat(request.getName().getDomain()).isEqualTo("docker.io");
assertThat(request.getName().getName()).isEqualTo("library/build-image-test");
assertThat(request.getName().getTag()).isEqualTo("latest");
assertThat(request.getName().getDigest()).isNull();
}
@Test
void whenProjectVersionIsSpecifiedThenItIsUsedInTagOfImageName() {
this.project.setVersion("1.2.3");
BuildRequest request = this.buildImage.createRequest();
assertThat(request.getName().getDomain()).isEqualTo("docker.io");
assertThat(request.getName().getName()).isEqualTo("library/build-image-test");
assertThat(request.getName().getTag()).isEqualTo("1.2.3");
assertThat(request.getName().getDigest()).isNull();
}
@Test
void whenImageNameIsSpecifiedThenItIsUsedInRequest() {
this.project.setVersion("1.2.3");
this.buildImage.setImageName("example.com/test/build-image:1.0");
BuildRequest request = this.buildImage.createRequest();
assertThat(request.getName().getDomain()).isEqualTo("example.com");
assertThat(request.getName().getName()).isEqualTo("test/build-image");
assertThat(request.getName().getTag()).isEqualTo("1.0");
assertThat(request.getName().getDigest()).isNull();
}
@Test
void whenIndividualEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() {
this.buildImage.environment("ALPHA", "a");
this.buildImage.environment("BRAVO", "b");
assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b")
.hasSize(2);
}
@Test
void whenEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() {
Map<String, String> environment = new HashMap<String, String>();
environment.put("ALPHA", "a");
environment.put("BRAVO", "b");
this.buildImage.environment(environment);
assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b")
.hasSize(2);
}
@Test
void whenTheEnvironmentIsSetItIsIncludedInTheRequest() {
Map<String, String> environment = new HashMap<String, String>();
environment.put("ALPHA", "a");
environment.put("BRAVO", "b");
this.buildImage.setEnvironment(environment);
assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b")
.hasSize(2);
}
@Test
void whenTheEnvironmentIsSetItReplacesAnyExistingEntriesAndIsIncludedInTheRequest() {
Map<String, String> environment = new HashMap<String, String>();
environment.put("ALPHA", "a");
environment.put("BRAVO", "b");
this.buildImage.environment("C", "Charlie");
this.buildImage.setEnvironment(environment);
assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b")
.hasSize(2);
}
@Test
void whenUsingDefaultConfigurationThenRequestHasVerboseLoggingDisabled() {
assertThat(this.buildImage.createRequest().isVerboseLogging()).isFalse();
}
@Test
void whenVerboseLoggingIsEnabledThenRequestHasVerboseLoggingEnabled() {
this.buildImage.setVerboseLogging(true);
assertThat(this.buildImage.createRequest().isVerboseLogging()).isTrue();
}
@Test
void whenUsingDefaultConfigurationThenRequestHasCleanCacheDisabled() {
assertThat(this.buildImage.createRequest().isCleanCache()).isFalse();
}
@Test
void whenCleanCacheIsEnabledThenRequestHasCleanCacheEnabled() {
this.buildImage.setCleanCache(true);
assertThat(this.buildImage.createRequest().isCleanCache()).isTrue();
}
@Test
void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() {
assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("cloudfoundry/cnb");
}
@Test
void whenBuilderIsConfiguredThenRequestUsesSpecifiedBuilder() {
this.buildImage.setBuilder("example.com/test/builder:1.2");
assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("test/builder");
}
}

View File

@@ -27,9 +27,16 @@ import java.util.Arrays;
import java.util.List;
import java.util.jar.JarFile;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.Versioned;
import com.fasterxml.jackson.databind.Module;
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.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.http.HttpRequest;
import org.apache.http.conn.HttpClientConnectionManager;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.util.GradleVersion;
@@ -41,6 +48,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin;
import org.jetbrains.kotlin.gradle.plugin.KotlinPlugin;
import org.springframework.asm.ClassVisitor;
import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;
@@ -94,7 +102,12 @@ public class GradleBuild {
new File(pathOfJarContaining(KotlinPlugin.class)), new File(pathOfJarContaining(KotlinProject.class)),
new File(pathOfJarContaining(KotlinCompilerClient.class)),
new File(pathOfJarContaining(KotlinGradleSubplugin.class)),
new File(pathOfJarContaining(ArchiveEntry.class)));
new File(pathOfJarContaining(ArchiveEntry.class)), new File(pathOfJarContaining(BuildRequest.class)),
new File(pathOfJarContaining(HttpClientConnectionManager.class)),
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)));
}
private String pathOfJarContaining(Class<?> type) {