Add bindings option for image building

This commit adds configuration to the Maven and Gradle plugins to
allow a list of volume mount bindings to be provided to the image
building goal and task. This enables service bindings to be mounted
in the builder image that are recognized by buildpacks to support
custom certificates, build tool configuration, APM integration, and
other buildpack features.

Fixes gh-23518
This commit is contained in:
Scott Frederick
2021-02-22 13:10:54 -06:00
parent 8cb24a426d
commit 89555a8745
24 changed files with 546 additions and 66 deletions

View File

@@ -31,6 +31,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -181,6 +182,23 @@ public class BuildRequestTests {
.withMessage("Buildpacks must not be null");
}
@Test
void withBindingsAddsBindings() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withBindings = request.withBindings(Binding.of("/host/path:/container/path:ro"),
Binding.of("volume-name:/container/path:rw"));
assertThat(request.getBindings()).isEmpty();
assertThat(withBindings.getBindings()).containsExactly(Binding.of("/host/path:/container/path:ro"),
Binding.of("volume-name:/container/path:rw"));
}
@Test
void withBindingsWhenBindingsIsNullThrowsException() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
assertThatIllegalArgumentException().isThrownBy(() -> request.withBindings((List<Binding>) null))
.withMessage("Bindings must not be null");
}
private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

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.
@@ -36,6 +36,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
@@ -88,6 +89,18 @@ class LifecycleTests {
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void executeWithBindingsExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest().withBindings(Binding.of("/host/src/path:/container/dest/path:ro"),
Binding.of("volume-name:/container/volume/path:rw"));
createLifecycle(request).execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-bindings.json"));
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void executeExecutesPhasesWithPlatformApi03() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());

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.
@@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.build;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig.Update;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
@@ -65,7 +66,7 @@ class PhaseTests {
Update update = mock(Update.class);
phase.apply(update);
verify(update).withUser("root");
verify(update).withBind("/var/run/docker.sock", "/var/run/docker.sock");
verify(update).withBinding(Binding.from("/var/run/docker.sock", "/var/run/docker.sock"));
verify(update).withCommand("/cnb/lifecycle/test", NO_ARGS);
verify(update).withLabel("author", "spring-boot");
verifyNoMoreInteractions(update);
@@ -108,12 +109,12 @@ class PhaseTests {
void applyWhenWithBindsUpdatesConfigurationWithBinds() {
Phase phase = new Phase("test", false);
VolumeName volumeName = VolumeName.of("test");
phase.withBinds(volumeName, "/test");
phase.withBinding(Binding.from(volumeName, "/test"));
Update update = mock(Update.class);
phase.apply(update);
verify(update).withCommand("/cnb/lifecycle/test");
verify(update).withLabel("author", "spring-boot");
verify(update).withBind(volumeName, "/test");
verify(update).withBinding(Binding.from(volumeName, "/test"));
verifyNoMoreInteractions(update);
}

View File

@@ -0,0 +1,73 @@
/*
* 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.
* 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.buildpack.platform.docker.type;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link Binding}.
*
* @author Scott Frederick
*/
class BindingTests {
@Test
void ofReturnsValue() {
Binding binding = Binding.of("host-src:container-dest:ro");
assertThat(binding.getValue()).isEqualTo("host-src:container-dest:ro");
}
@Test
void ofWithNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.of(null))
.withMessageContaining("Value must not be null");
}
@Test
void fromReturnsValue() {
Binding binding = Binding.from("host-src", "container-dest");
assertThat(binding.getValue()).isEqualTo("host-src:container-dest");
}
@Test
void fromWithNullSourceThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.from((String) null, "container-dest"))
.withMessageContaining("Source must not be null");
}
@Test
void fromWithNullDestinationThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.from("host-src", null))
.withMessageContaining("Destination must not be null");
}
@Test
void fromVolumeNameSourceReturnsValue() {
Binding binding = Binding.from(VolumeName.of("host-src"), "container-dest");
assertThat(binding.getValue()).isEqualTo("host-src:container-dest");
}
@Test
void fromVolumeNameSourceWithNullSourceThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.from((VolumeName) null, "container-dest"))
.withMessageContaining("SourceVolume must not be null");
}
}

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.
@@ -56,7 +56,7 @@ class ContainerConfigTests extends AbstractJsonTests {
update.withCommand("ls", "-l");
update.withArgs("-h");
update.withLabel("spring", "boot");
update.withBind("bind-source", "bind-dest");
update.withBinding(Binding.from("bind-source", "bind-dest"));
update.withEnv("name1", "value1");
update.withEnv("name2", "value2");
});

View File

@@ -0,0 +1,12 @@
{
"User" : "root",
"Image" : "pack.local/ephemeral-builder",
"Cmd" : [ "/cnb/lifecycle/creator", "-app", "/workspace", "-platform", "/platform", "-run-image", "docker.io/cloudfoundry/run:latest", "-layers", "/layers", "-cache-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "-process-type=web", "docker.io/library/my-application:latest" ],
"Env" : [ "CNB_PLATFORM_API=0.4" ],
"Labels" : {
"author" : "spring-boot"
},
"HostConfig" : {
"Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache", "pack-cache-b35197ac41ea.launch:/launch-cache", "/host/src/path:/container/dest/path:ro", "volume-name:/container/volume/path:rw" ]
}
}