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

@@ -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<BuildpackReference> buildpacks;
private final List<Binding> bindings;
BuildRequest(ImageReference name, Function<Owner, TarArchive> 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<Owner, TarArchive> applicationContent, ImageReference builder,
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks) {
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
List<Binding> 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<BuildpackReference> 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<Binding> 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<BuildpackReference> getBuildpacks() {
return this.buildpacks;
}
/**
* Return the collection of bindings to mount to the build container.
* @return the bindings
*/
public List<Binding> getBindings() {
return this.bindings;
}
/**
* Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file

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.
@@ -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;
}

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.
@@ -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<String> args = new ArrayList<>();
private final Map<VolumeName, String> binds = new LinkedHashMap<>();
private final List<Binding> bindings = new ArrayList<>();
private final Map<String, String> 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);
}

View File

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

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.
@@ -47,7 +47,7 @@ public class ContainerConfig {
private final String json;
ContainerConfig(String user, ImageReference image, String command, List<String> args, Map<String, String> labels,
Map<String, String> binds, Map<String, String> env) throws IOException {
List<Binding> bindings, Map<String, String> 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<String, String> labels = new LinkedHashMap<>();
private final Map<String, String> binds = new LinkedHashMap<>();
private final List<Binding> bindings = new ArrayList<>();
private final Map<String, String> env = new LinkedHashMap<>();
@@ -121,7 +121,7 @@ public class ContainerConfig {
private ContainerConfig run(Consumer<Update> 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);
}
/**

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" ]
}
}