diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index 8ce587b385..32d291c61b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +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; @@ -65,6 +66,8 @@ public class BuildRequest { private final List buildpacks; + private final List bindings; + BuildRequest(ImageReference name, Function applicationContent) { Assert.notNull(name, "Name must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null"); @@ -79,11 +82,13 @@ public class BuildRequest { this.publish = false; this.creator = Creator.withVersion(""); this.buildpacks = Collections.emptyList(); + this.bindings = Collections.emptyList(); } BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, - boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks) { + boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, + List bindings) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; @@ -95,6 +100,7 @@ public class BuildRequest { this.pullPolicy = pullPolicy; this.publish = publish; this.buildpacks = buildpacks; + this.bindings = bindings; } /** @@ -106,7 +112,7 @@ public class BuildRequest { Assert.notNull(builder, "Builder must not be null"); return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks); + this.buildpacks, this.bindings); } /** @@ -117,7 +123,7 @@ public class BuildRequest { public BuildRequest withRunImage(ImageReference runImageName) { return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks); + this.buildpacks, this.bindings); } /** @@ -128,7 +134,7 @@ public class BuildRequest { public BuildRequest withCreator(Creator creator) { Assert.notNull(creator, "Creator must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks); + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings); } /** @@ -144,7 +150,7 @@ public class BuildRequest { env.put(name, value); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks); + this.buildpacks, this.bindings); } /** @@ -158,7 +164,7 @@ public class BuildRequest { updatedEnv.putAll(env); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, - this.publish, this.buildpacks); + this.publish, this.buildpacks, this.bindings); } /** @@ -168,7 +174,7 @@ public class BuildRequest { */ public BuildRequest withCleanCache(boolean cleanCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks); + cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings); } /** @@ -178,7 +184,7 @@ public class BuildRequest { */ public BuildRequest withVerboseLogging(boolean verboseLogging) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks); + this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings); } /** @@ -188,7 +194,7 @@ public class BuildRequest { */ public BuildRequest withPullPolicy(PullPolicy pullPolicy) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks); + this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings); } /** @@ -198,7 +204,7 @@ public class BuildRequest { */ public BuildRequest withPublish(boolean publish) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks); + this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings); } /** @@ -219,7 +225,28 @@ public class BuildRequest { public BuildRequest withBuildpacks(List buildpacks) { Assert.notNull(buildpacks, "Buildpacks must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks); + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with updated bindings. + * @param bindings a collection of bindings to mount to the build container + * @return an updated build request + */ + public BuildRequest withBindings(Binding... bindings) { + Assert.notEmpty(bindings, "Bindings must not be empty"); + return withBindings(Arrays.asList(bindings)); + } + + /** + * Return a new {@link BuildRequest} with updated bindings. + * @param bindings a collection of bindings to mount to the build container + * @return an updated build request + */ + public BuildRequest withBindings(List bindings) { + Assert.notNull(bindings, "Bindings must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings); } /** @@ -307,12 +334,20 @@ public class BuildRequest { /** * Return the collection of buildpacks to use when building the image, if provided. - * @return the collection of buildpacks + * @return the buildpacks */ public List getBuildpacks() { return this.buildpacks; } + /** + * Return the collection of bindings to mount to the build container. + * @return the bindings + */ + public List getBindings() { + return this.bindings; + } + /** * Factory method to create a new {@link BuildRequest} from a JAR file. * @param jarFile the source jar file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index 0ab904b9fd..c6adc77096 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -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. @@ -22,6 +22,7 @@ import java.util.function.Consumer; import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; +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; @@ -138,10 +139,13 @@ class Lifecycle implements Closeable { phase.withArgs("-process-type=web"); } phase.withArgs(this.request.getName()); - phase.withBinds(this.layersVolume, Directory.LAYERS); - phase.withBinds(this.applicationVolume, Directory.APPLICATION); - phase.withBinds(this.buildCacheVolume, Directory.CACHE); - phase.withBinds(this.launchCacheVolume, Directory.LAUNCH_CACHE); + phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS)); + phase.withBinding(Binding.from(this.applicationVolume, Directory.APPLICATION)); + phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE)); + phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE)); + if (this.request.getBindings() != null) { + this.request.getBindings().forEach(phase::withBinding); + } phase.withEnv(PLATFORM_API_VERSION_KEY, this.platformVersion.toString()); return phase; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java index ec7c321cd4..234592afd4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java @@ -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. @@ -22,8 +22,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +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.VolumeName; import org.springframework.util.StringUtils; /** @@ -44,7 +44,7 @@ class Phase { private final List args = new ArrayList<>(); - private final Map binds = new LinkedHashMap<>(); + private final List bindings = new ArrayList<>(); private final Map env = new LinkedHashMap<>(); @@ -86,11 +86,10 @@ class Phase { /** * Update this phase with an addition volume binding. - * @param source the source volume - * @param dest the destination location + * @param binding the binding */ - void withBinds(VolumeName source, String dest) { - this.binds.put(source, dest); + void withBinding(Binding binding) { + this.bindings.add(binding); } /** @@ -122,11 +121,11 @@ class Phase { void apply(ContainerConfig.Update update) { if (this.daemonAccess) { update.withUser("root"); - update.withBind(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH); + update.withBinding(Binding.from(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH)); } update.withCommand("/cnb/lifecycle/" + this.name, StringUtils.toStringArray(this.args)); update.withLabel("author", "spring-boot"); - this.binds.forEach(update::withBind); + this.bindings.forEach(update::withBinding); this.env.forEach(update::withEnv); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java new file mode 100644 index 0000000000..5a1169206c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java @@ -0,0 +1,97 @@ +/* + * 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 java.util.Objects; + +import org.springframework.util.Assert; + +/** + * Volume bindings to apply when creating a container. + * + * @author Scott Frederick + * @since 2.5.0 + */ +public final class Binding { + + private final String value; + + private Binding(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + /** + * Create a {@link Binding} with the specified value containing a host source, + * container destination, and options. + * @param value the volume binding value + * @return a new {@link Binding} instance + */ + public static Binding of(String value) { + Assert.notNull(value, "Value must not be null"); + return new Binding(value); + } + + /** + * Create a {@link Binding} from the specified source and destination. + * @param sourceVolume the volume binding host source + * @param destination the volume binding container destination + * @return a new {@link Binding} instance + */ + public static Binding from(VolumeName sourceVolume, String destination) { + Assert.notNull(sourceVolume, "SourceVolume must not be null"); + return from(sourceVolume.toString(), destination); + } + + /** + * Create a {@link Binding} from the specified source and destination. + * @param source the volume binding host source + * @param destination the volume binding container destination + * @return a new {@link Binding} instance + */ + public static Binding from(String source, String destination) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(destination, "Destination must not be null"); + return new Binding(source + ":" + destination); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Binding)) { + return false; + } + Binding binding = (Binding) obj; + return Objects.equals(this.value, binding.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.value); + } + + @Override + public String toString() { + return this.value; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfig.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfig.java index 1d91194938..f432758ff6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfig.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfig.java @@ -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. @@ -47,7 +47,7 @@ public class ContainerConfig { private final String json; ContainerConfig(String user, ImageReference image, String command, List args, Map labels, - Map binds, Map env) throws IOException { + List bindings, Map env) throws IOException { Assert.notNull(image, "Image must not be null"); Assert.hasText(command, "Command must not be empty"); ObjectMapper objectMapper = SharedObjectMapper.get(); @@ -65,7 +65,7 @@ public class ContainerConfig { labels.forEach(labelsNode::put); ObjectNode hostConfigNode = node.putObject("HostConfig"); ArrayNode bindsNode = hostConfigNode.putArray("Binds"); - binds.forEach((source, dest) -> bindsNode.add(source + ":" + dest)); + bindings.forEach((binding) -> bindsNode.add(binding.getValue())); this.json = objectMapper.writeValueAsString(node); } @@ -110,7 +110,7 @@ public class ContainerConfig { private final Map labels = new LinkedHashMap<>(); - private final Map binds = new LinkedHashMap<>(); + private final List bindings = new ArrayList<>(); private final Map env = new LinkedHashMap<>(); @@ -121,7 +121,7 @@ public class ContainerConfig { private ContainerConfig run(Consumer update) { update.accept(this); try { - return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.binds, + return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.bindings, this.env); } catch (IOException ex) { @@ -166,21 +166,11 @@ public class ContainerConfig { } /** - * Update the container config with an additional bind. - * @param sourceVolume the source volume - * @param dest the bind destination + * Update the container config with an additional binding. + * @param binding the binding */ - public void withBind(VolumeName sourceVolume, String dest) { - this.binds.put(sourceVolume.toString(), dest); - } - - /** - * Update the container config with an additional bind. - * @param source the bind source - * @param dest the bind destination - */ - public void withBind(String source, String dest) { - this.binds.put(source, dest); + public void withBinding(Binding binding) { + this.bindings.add(binding); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 87c71af218..92115cc0a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -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) null)) + .withMessage("Bindings must not be null"); + } + private void hasExpectedJarContent(TarArchive archive) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 779217b02a..8e5b26187c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -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()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java index 7f824ed0ce..ffddb46448 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java @@ -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); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/BindingTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/BindingTests.java new file mode 100644 index 0000000000..beda11df1d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/BindingTests.java @@ -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"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfigTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfigTests.java index f8bd42f168..cc94d2d7ae 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfigTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfigTests.java @@ -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"); }); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-bindings.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-bindings.json new file mode 100644 index 0000000000..b3a33778d3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-bindings.json @@ -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" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 0d89b4af08..5de39a6e79 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -136,12 +136,28 @@ a|Buildpacks that the builder should use when building the image. Only the specified buildpacks will be used, overriding the default buildpacks included in the builder. Buildpack references must be in one of the following forms: -* Buildpack in the builder - [urn:cnb:builder:][@] -* Buildpack in a directory on the file system - [file://] -* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]/ -* Buildpack in an OCI image - [docker://]/[:][@] +* Buildpack in the builder - `[urn:cnb:builder:][@]` +* Buildpack in a directory on the file system - `[file://]` +* Buildpack in a gzipped tar (.tgz) file on the file system - `[file://]/` +* Buildpack in an OCI image - `[docker://]/[:][@]` | None, indicating the builder should use the buildpacks included in it. +| `bindings` +| +a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image. +The bindings will be passed unparsed and unvalidated to Docker when creating the builder container. +Bindings must be in one of the following forms: + +* `:[:]` +* `:[:]` + +Where `` can contain: + +* `ro` to mount the volume as read-only in the container +* `rw` to mount the volume as readable and writable in the container +* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value +| + | `cleanCache` | `--cleanCache` | Whether to clean the cache before building. @@ -295,7 +311,7 @@ A buildpack located in a CNB Builder (version may be omitted if there is only on A path to a directory containing buildpack content (not supported on Windows): * `\file:///path/to/buildpack/` -* `/path/to/buildpack/`BootBuildImageIntegrationTests +* `/path/to/buildpack/` A path to a gzipped tar file containing buildpack content: diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 15b1caff06..1ab1a876b1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -45,6 +45,7 @@ import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Creator; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; +import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.ZipFileTarArchive; @@ -87,9 +88,11 @@ public class BootBuildImage extends DefaultTask { private boolean publish; - private ListProperty buildpacks; + private final ListProperty buildpacks; - private DockerSpec docker = new DockerSpec(); + private final ListProperty bindings; + + private final DockerSpec docker = new DockerSpec(); public BootBuildImage() { this.jar = getProject().getObjects().fileProperty(); @@ -99,6 +102,7 @@ public class BootBuildImage extends DefaultTask { Project project = getProject(); this.projectVersion.set(getProject().provider(() -> project.getVersion().toString())); this.buildpacks = getProject().getObjects().listProperty(String.class); + this.bindings = getProject().getObjects().listProperty(String.class); } /** @@ -292,7 +296,7 @@ public class BootBuildImage extends DefaultTask { /** * Returns the buildpacks that will be used when building the image. - * @return the buildpacks + * @return the buildpack references */ @Input @Optional @@ -302,7 +306,7 @@ public class BootBuildImage extends DefaultTask { /** * Sets the buildpacks that will be used when building the image. - * @param buildpacks the buildpacks + * @param buildpacks the buildpack references */ public void setBuildpacks(List buildpacks) { this.buildpacks.set(buildpacks); @@ -317,13 +321,51 @@ public class BootBuildImage extends DefaultTask { } /** - * Adds entries to the environment that will be used when building the image. + * Adds entries to the buildpacks that will be used when building the image. * @param buildpacks the buildpack references */ public void buildpacks(List buildpacks) { this.buildpacks.addAll(buildpacks); } + /** + * Returns the volume bindings that will be mounted to the container when building the + * image. + * @return the bindings + */ + @Input + @Optional + public List getBindings() { + return this.bindings.getOrNull(); + } + + /** + * Sets the volume bindings that will be mounted to the container when building the + * image. + * @param bindings the bindings + */ + public void setBindings(List bindings) { + this.bindings.set(bindings); + } + + /** + * Add an entry to the volume bindings that will be mounted to the container when + * building the image. + * @param binding the binding + */ + public void binding(String binding) { + this.bindings.add(binding); + } + + /** + * Add entries to the volume bindings that will be mounted to the container when + * building the image. + * @param bindings the bindings + */ + public void bindings(List bindings) { + this.bindings.addAll(bindings); + } + /** * Returns the Docker configuration the builder will use. * @return docker configuration. @@ -388,6 +430,7 @@ public class BootBuildImage extends DefaultTask { request = customizePullPolicy(request); request = customizePublish(request); request = customizeBuildpacks(request); + request = customizeBindings(request); return request; } @@ -448,6 +491,14 @@ public class BootBuildImage extends DefaultTask { return request; } + private BuildRequest customizeBindings(BuildRequest request) { + List bindings = this.bindings.getOrNull(); + if (bindings != null && !bindings.isEmpty()) { + return request.withBindings(bindings.stream().map(Binding::of).collect(Collectors.toList())); + } + return request; + } + private String translateTargetJavaVersion() { return this.targetJavaVersion.get().getMajorVersion() + ".*"; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index cfe5c2501d..e1f9f03c97 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -219,6 +219,17 @@ class BootBuildImageIntegrationTests { } } + @TestTemplate + void failsWithBindingContainingInvalidCertificate() throws IOException { + writeMainClass(); + writeLongNameResource(); + writeCertificateBindingFiles(); + BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); + assertThat(result.getOutput()).contains("failed to decode certificate") + .contains("/platform/bindings/certificates/test.crt"); + } + @TestTemplate void failsWithLaunchScript() throws IOException { writeMainClass(); @@ -380,4 +391,17 @@ class BootBuildImageIntegrationTests { tar.closeArchiveEntry(); } + private void writeCertificateBindingFiles() throws IOException { + File bindingDir = new File(this.gradleBuild.getProjectDir(), "bindings/ca-certificates"); + bindingDir.mkdirs(); + File type = new File(bindingDir, "type"); + try (PrintWriter writer = new PrintWriter(new FileWriter(type))) { + writer.print("ca-certificates"); + } + File cert = new File(bindingDir, "test.crt"); + try (PrintWriter writer = new PrintWriter(new FileWriter(cert))) { + writer.println("not a valid certificate"); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 1fcd631034..23836fa57e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -31,6 +31,7 @@ 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 org.springframework.boot.buildpack.platform.docker.type.Binding; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -250,4 +251,31 @@ class BootBuildImageTests { BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2")); } + @Test + void whenNoBindingsAreConfiguredThenRequestHasNoBindings() { + assertThat(this.buildImage.createRequest().getBindings()).isEmpty(); + } + + @Test + void whenBindingsAreConfiguredThenRequestHasBindings() { + this.buildImage.setBindings(Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw")); + assertThat(this.buildImage.createRequest().getBindings()) + .containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw")); + } + + @Test + void whenEntriesAreAddedToBindingsThenRequestHasBindings() { + this.buildImage.bindings(Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw")); + assertThat(this.buildImage.createRequest().getBindings()) + .containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw")); + } + + @Test + void whenIndividualEntriesAreAddedToBindingsThenRequestHasBindings() { + this.buildImage.binding("host-src:container-dest:ro"); + this.buildImage.binding("volume-name:container-dest:rw"); + assertThat(this.buildImage.createRequest().getBindings()) + .containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw")); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBindingContainingInvalidCertificate.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBindingContainingInvalidCertificate.gradle new file mode 100644 index 0000000000..c20cac9d30 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBindingContainingInvalidCertificate.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + bindings = [ "${projectDir}/bindings/ca-certificates:/platform/bindings/certificates" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 881b1ededd..d8f67fe82d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -157,13 +157,29 @@ a|Buildpacks that the builder should use when building the image. Only the specified buildpacks will be used, overriding the default buildpacks included in the builder. Buildpack references must be in one of the following forms: -* Buildpack in the builder - [urn:cnb:builder:][@] -* Buildpack in a directory on the file system - [file://] -* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]/ -* Buildpack in an OCI image - [docker://]/[:][@] +* Buildpack in the builder - `[urn:cnb:builder:][@]` +* Buildpack in a directory on the file system - `[file://]` +* Buildpack in a gzipped tar (.tgz) file on the file system - `[file://]/` +* Buildpack in an OCI image - `[docker://]/[:][@]` | | None, indicating the builder should use the buildpacks included in it. +| `bindings` +a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image. +The bindings will be passed unparsed and unvalidated to Docker when creating the builder container. +Bindings must be in one of the following forms: + +* `:[:]` +* `:[:]` + +Where `` can contain: + +* `ro` to mount the volume as read-only in the container +* `rw` to mount the volume as readable and writable in the container +* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value +| +| + | `cleanCache` | Whether to clean the cache before building. | `spring-boot.build-image.cleanCache` diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index c194ecafac..31335275df 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -168,6 +168,15 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests { }); } + @TestTemplate + void failsWithBindingContainingInvalidCertificate(MavenBuild mavenBuild) { + mavenBuild.project("build-image-bindings").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .executeAndFail((project) -> assertThat(buildLog(project)).contains("Building image") + .contains("failed to decode certificate") + .contains("/platform/bindings/ca-certificates/test.crt")); + } + @TestTemplate void failsWhenPublishWithoutPublishRegistryConfigured(MavenBuild mavenBuild) { mavenBuild.project("build-image").goals("package").systemProperty("spring-boot.build-image.publish", "true") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/test.crt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/test.crt new file mode 100644 index 0000000000..e403bc8f31 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/test.crt @@ -0,0 +1 @@ +not a valid certificate \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/type b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/type new file mode 100644 index 0000000000..54619edd16 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/type @@ -0,0 +1 @@ +ca-certificates \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/pom.xml new file mode 100644 index 0000000000..df717aa199 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-bindings + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + + ${basedir}/bindings/ca-certificates:/platform/bindings/ca-certificates + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..e964724dea --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * 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.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 5b52ac926b..dd219ef1d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -26,6 +26,7 @@ import org.apache.maven.artifact.Artifact; 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 org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.Owner; @@ -59,6 +60,8 @@ public class Image { List buildpacks; + List bindings; + /** * The name of the created image. * @return the image name @@ -183,6 +186,9 @@ public class Image { request = request .withBuildpacks(this.buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList())); } + if (this.bindings != null && !this.bindings.isEmpty()) { + request = request.withBindings(this.bindings.stream().map(Binding::of).collect(Collectors.toList())); + } return request; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 78278e35d3..15ee4db410 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; 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 org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; @@ -68,6 +69,7 @@ class ImageTests { assertThat(request.isVerboseLogging()).isFalse(); assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS); assertThat(request.getBuildpacks()).isEmpty(); + assertThat(request.getBindings()).isEmpty(); } @Test @@ -135,6 +137,15 @@ class ImageTests { BuildpackReference.of("example/buildpack2@0.0.2")); } + @Test + void getBuildRequestWhenHasBindingsUsesBindings() { + Image image = new Image(); + image.bindings = Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-dest:ro"), + Binding.of("volume-name:container-dest:rw")); + } + private Artifact createArtifact() { return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", "jar", null, new DefaultArtifactHandler());