Merge pull request #51893 from scottfrederick

Closes gh-51893

* gh-51893:
  Check distro instead of stack ID for run image
This commit is contained in:
Andy Wilkinson
2026-10-01 11:20:03 +01:00
11 changed files with 634 additions and 214 deletions
@@ -129,9 +129,9 @@ public abstract class AbstractBuildLog implements BuildLog {
}
@Override
public void stackIdsDoNotMatch(String runImageStackId, String builderImageStackId) {
log("Warning: Run image stack '%s' does not match builder stack '%s'. Stack IDs are deprecated, but the images may not be compatible."
.formatted(runImageStackId, builderImageStackId));
public void distrosDoNotMatch(String runImageDistro, String builderImageDistro) {
log("Warning: Run image distribution '%s' does not match builder distribution '%s'. The images may not be compatible."
.formatted(runImageDistro, builderImageDistro));
log();
}
@@ -136,13 +136,13 @@ public interface BuildLog {
void sensitiveTargetBindingDetected(Binding binding);
/**
* Log that the stack ID of the run image does not match the stack ID of the builder
* image.
* @param runImageStackId the stack ID of the run image
* @param builderImageStackId the stack ID of the builder image
* Log that the OS distribution of the run image does not match the OS distribution of
* the builder image.
* @param runImageDistro the OS distribution of the run image
* @param builderImageDistro the OS distribution of the builder image
* @since 4.0.9
*/
default void stackIdsDoNotMatch(String runImageStackId, String builderImageStackId) {
default void distrosDoNotMatch(String runImageDistro, String builderImageDistro) {
}
/**
@@ -120,7 +120,7 @@ public class Builder {
request = request.withRunImage(request.getRunImage().withDigest(runImage.getPrimaryDigest()));
runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform);
}
warnIfStackIdsDoNotMatch(runImage, builderImage);
warnIfDistrosDoNotMatch(runImage, builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage);
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, platform, builderMetadata,
@@ -159,11 +159,11 @@ public class Builder {
return ImageReference.of(runImageName).inTaggedOrDigestForm();
}
private void warnIfStackIdsDoNotMatch(Image runImage, Image builderImage) {
StackId runImageStackId = StackId.fromImage(runImage);
StackId builderImageStackId = StackId.fromImage(builderImage);
if (runImageStackId.hasId() && builderImageStackId.hasId() && !runImageStackId.equals(builderImageStackId)) {
this.log.stackIdsDoNotMatch(runImageStackId.toString(), builderImageStackId.toString());
private void warnIfDistrosDoNotMatch(Image runImage, Image builderImage) {
Distro runImageDistro = Distro.fromImage(runImage);
Distro builderImageDistro = Distro.fromImage(builderImage);
if (!runImageDistro.matches(builderImageDistro)) {
this.log.distrosDoNotMatch(runImageDistro.toString(), builderImageDistro.toString());
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2012-present 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.build;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The OS distribution of an image, as described by the target data of the CNB platform
* specification.
*
* @author Moritz Halbritter
*/
final class Distro {
private static final String NAME_LABEL = "io.buildpacks.base.distro.name";
private static final String VERSION_LABEL = "io.buildpacks.base.distro.version";
// Paketo images only set these non-spec labels
private static final String STACK_NAME_LABEL = "io.buildpacks.stack.distro.name";
private static final String STACK_VERSION_LABEL = "io.buildpacks.stack.distro.version";
private final @Nullable String name;
private final @Nullable String version;
private Distro(@Nullable String name, @Nullable String version) {
this.name = name;
this.version = version;
}
/**
* Return whether this distribution matches the given distribution. A missing name or
* version matches any value.
* @param other the distribution to compare with
* @return {@code true} if the distributions match
*/
boolean matches(Distro other) {
return matches(this.name, other.name) && matches(this.version, other.version);
}
private static boolean matches(@Nullable String value, @Nullable String other) {
if (value == null || other == null) {
return true;
}
return value.equals(other);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
if (this.name != null) {
result.append(this.name);
}
if (this.version != null) {
if (!result.isEmpty()) {
result.append(" ");
}
result.append(this.version);
}
return result.toString();
}
/**
* Factory method to create a {@link Distro} from an {@link Image}.
* @param image the source image
* @return the extracted distribution
*/
static Distro fromImage(Image image) {
Assert.notNull(image, "'image' must not be null");
Map<String, String> labels = image.getConfig().getLabels();
String name = getLabel(labels, NAME_LABEL, STACK_NAME_LABEL);
String version = getLabel(labels, VERSION_LABEL, STACK_VERSION_LABEL);
return new Distro(name, version);
}
private static @Nullable String getLabel(Map<String, String> labels, String label, String fallbackLabel) {
String value = labels.get(label);
if (StringUtils.hasText(value)) {
return value;
}
value = labels.get(fallbackLabel);
return StringUtils.hasText(value) ? value : null;
}
}
@@ -1,98 +0,0 @@
/*
* Copyright 2012-present 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.build;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import org.springframework.util.Assert;
/**
* A Stack ID.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class StackId {
private static final String LABEL_NAME = "io.buildpacks.stack.id";
private final @Nullable String value;
StackId(@Nullable String value) {
this.value = value;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return Objects.equals(this.value, ((StackId) obj).value);
}
boolean hasId() {
return this.value != null;
}
@Override
public int hashCode() {
return Objects.hashCode(this.value);
}
@Override
public String toString() {
return (this.value != null) ? this.value : "<null>";
}
/**
* Factory method to create a {@link StackId} from an {@link Image}.
* @param image the source image
* @return the extracted stack ID
*/
static StackId fromImage(Image image) {
Assert.notNull(image, "'image' must not be null");
return fromImageConfig(image.getConfig());
}
/**
* Factory method to create a {@link StackId} from an {@link ImageConfig}.
* @param imageConfig the source image config
* @return the extracted stack ID
*/
private static StackId fromImageConfig(ImageConfig imageConfig) {
String value = imageConfig.getLabels().get(LABEL_NAME);
return new StackId(value);
}
/**
* Factory method to create a {@link StackId} with a given value.
* @param value the stack ID value
* @return a new stack ID instance
*/
static StackId of(String value) {
Assert.hasText(value, "'value' must not be empty");
return new StackId(value);
}
}
@@ -447,22 +447,25 @@ class BuilderTests {
}
@Test
void buildWhenStackIdDoesNotMatchLogsWarning() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image-with-bad-stack.json");
given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
builder.build(request);
assertThat(out.toString()).contains(
"Warning: Run image stack 'org.cloudfoundry.stacks.cfwindowsfs3' does not match builder stack 'io.buildpacks.stacks.bionic'");
assertThat(out.toString()).contains("Running creator");
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
void shouldNotWarnWhenStackIdsDifferWithoutDistro() throws Exception {
String output = buildWith("image.json", "run-image-with-bad-stack.json");
assertThat(output).doesNotContain("Warning");
assertThat(output).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void shouldNotWarnWhenStackIdsDifferButDistrosMatch() throws Exception {
String output = buildWith("image-with-distro.json", "run-image-with-same-distro.json");
assertThat(output).doesNotContain("Warning");
assertThat(output).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void shouldWarnWhenDistrosDoNotMatch() throws Exception {
String output = buildWith("image-with-distro.json", "run-image-with-other-distro.json");
assertThat(output).contains(
"Warning: Run image distribution 'ubuntu 24.04' does not match builder distribution 'ubuntu 26.04'");
assertThat(output).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
@@ -553,6 +556,20 @@ class BuilderTests {
return BuildRequest.of(name, (owner) -> content).withTrustBuilder(true);
}
private String buildWith(String builderImageName, String runImageName) throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage(builderImageName);
Image runImage = loadImage(runImageName);
given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
builder.build(getTestRequest());
return out.toString();
}
private Image loadImage(String name) throws IOException {
return Image.of(getClass().getResourceAsStream(name));
}
@@ -0,0 +1,144 @@
/*
* Copyright 2012-present 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.build;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link Distro}.
*
* @author Moritz Halbritter
*/
class DistroTests {
@Test
@SuppressWarnings("NullAway") // Test null check
void shouldFailWhenImageIsNull() {
assertThatIllegalArgumentException().isThrownBy(() -> Distro.fromImage(null))
.withMessage("'image' must not be null");
}
@Test
void shouldReadBaseLabels() {
Distro distro = Distro.fromImage(image(
Map.of("io.buildpacks.base.distro.name", "ubuntu", "io.buildpacks.base.distro.version", "26.04")));
assertThat(distro).hasToString("ubuntu 26.04");
}
@Test
void shouldFormatNameOnly() {
Distro distro = Distro.fromImage(image(Map.of("io.buildpacks.base.distro.name", "ubuntu")));
assertThat(distro).hasToString("ubuntu");
}
@Test
void shouldFormatVersionOnly() {
Distro distro = Distro.fromImage(image(Map.of("io.buildpacks.base.distro.version", "26.04")));
assertThat(distro).hasToString("26.04");
}
@Test
void shouldFormatEmptyWhenLabelsAreMissing() {
Distro distro = Distro.fromImage(image(Collections.emptyMap()));
assertThat(distro).hasToString("");
}
@Test
void shouldFallBackToStackLabels() {
Distro distro = Distro.fromImage(image(
Map.of("io.buildpacks.stack.distro.name", "ubuntu", "io.buildpacks.stack.distro.version", "26.04")));
assertThat(distro).hasToString("ubuntu 26.04");
}
@Test
void shouldPreferBaseLabelsOverStackLabels() {
Distro distro = Distro
.fromImage(image(Map.of("io.buildpacks.base.distro.name", "ubuntu", "io.buildpacks.base.distro.version",
"26.04", "io.buildpacks.stack.distro.name", "debian", "io.buildpacks.stack.distro.version", "13")));
assertThat(distro).hasToString("ubuntu 26.04");
}
@Test
void shouldMatchWhenNameAndVersionAreEqual() {
Distro distro = Distro.fromImage(image(
Map.of("io.buildpacks.base.distro.name", "ubuntu", "io.buildpacks.base.distro.version", "26.04")));
Distro other = Distro.fromImage(image(
Map.of("io.buildpacks.stack.distro.name", "ubuntu", "io.buildpacks.stack.distro.version", "26.04")));
assertThat(distro.matches(other)).isTrue();
}
@Test
void shouldNotMatchWhenVersionDiffers() {
Distro distro = Distro.fromImage(image(
Map.of("io.buildpacks.base.distro.name", "ubuntu", "io.buildpacks.base.distro.version", "26.04")));
Distro other = Distro.fromImage(image(
Map.of("io.buildpacks.base.distro.name", "ubuntu", "io.buildpacks.base.distro.version", "24.04")));
assertThat(distro.matches(other)).isFalse();
}
@Test
void shouldNotMatchWhenNameDiffers() {
Distro distro = Distro.fromImage(image(Map.of("io.buildpacks.base.distro.name", "ubuntu")));
Distro other = Distro.fromImage(image(Map.of("io.buildpacks.base.distro.name", "debian")));
assertThat(distro.matches(other)).isFalse();
}
@Test
void shouldMatchWhenVersionIsMissingOnOneSide() {
Distro distro = Distro.fromImage(image(
Map.of("io.buildpacks.base.distro.name", "ubuntu", "io.buildpacks.base.distro.version", "26.04")));
Distro other = Distro.fromImage(image(Map.of("io.buildpacks.base.distro.name", "ubuntu")));
assertThat(distro.matches(other)).isTrue();
assertThat(other.matches(distro)).isTrue();
}
@Test
void shouldMatchWhenLabelsAreMissing() {
Distro distro = Distro.fromImage(image(
Map.of("io.buildpacks.base.distro.name", "ubuntu", "io.buildpacks.base.distro.version", "26.04")));
Distro other = Distro.fromImage(image(Collections.emptyMap()));
assertThat(distro.matches(other)).isTrue();
assertThat(other.matches(distro)).isTrue();
}
@Test
void shouldIgnoreStackId() {
Distro distro = Distro.fromImage(image(Map.of("io.buildpacks.stack.id", "io.buildpacks.stacks.resolute")));
Distro other = Distro.fromImage(image(Map.of("io.buildpacks.stack.id", "io.buildpacks.stacks.resolute.tiny")));
assertThat(distro.matches(other)).isTrue();
}
private Image image(Map<String, String> labels) {
Image image = mock(Image.class);
ImageConfig imageConfig = mock(ImageConfig.class);
given(image.getConfig()).willReturn(imageConfig);
given(imageConfig.getLabels()).willReturn(labels);
return image;
}
}
@@ -1,86 +0,0 @@
/*
* Copyright 2012-present 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.build;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link StackId}.
*
* @author Phillip Webb
*/
class StackIdTests {
@Test
@SuppressWarnings("NullAway") // Test null check
void fromImageWhenImageIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> StackId.fromImage(null))
.withMessage("'image' must not be null");
}
@Test
void fromImageWhenLabelIsMissingHasNoId() {
Image image = mock(Image.class);
ImageConfig imageConfig = mock(ImageConfig.class);
given(image.getConfig()).willReturn(imageConfig);
StackId stackId = StackId.fromImage(image);
assertThat(stackId.hasId()).isFalse();
}
@Test
void fromImageCreatesStackId() {
Image image = mock(Image.class);
ImageConfig imageConfig = mock(ImageConfig.class);
given(image.getConfig()).willReturn(imageConfig);
given(imageConfig.getLabels()).willReturn(Collections.singletonMap("io.buildpacks.stack.id", "test"));
StackId stackId = StackId.fromImage(image);
assertThat(stackId).hasToString("test");
assertThat(stackId.hasId()).isTrue();
}
@Test
void ofCreatesStackId() {
StackId stackId = StackId.of("test");
assertThat(stackId).hasToString("test");
}
@Test
void equalsAndHashCode() {
StackId s1 = StackId.of("a");
StackId s2 = StackId.of("a");
StackId s3 = StackId.of("b");
assertThat(s1).hasSameHashCodeAs(s2);
assertThat(s1).isEqualTo(s1).isEqualTo(s2).isNotEqualTo(s3);
}
@Test
void toStringReturnsValue() {
StackId stackId = StackId.of("test");
assertThat(stackId).hasToString("test");
}
}
@@ -0,0 +1,101 @@
{
"Id": "sha256:1332879bc8e38793a45ebe5a750f2a1c35df07ec2aa9c18f694644a9de77359b",
"RepoTags": [
"cloudfoundry/run:base-cnb"
],
"RepoDigests": [
"cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad"
],
"Parent": "",
"Comment": "",
"Created": "2020-03-20T20:18:18.117972538Z",
"Container": "91d1af87c3bb6163cd9c7cb21e6891cd25f5fa3c7417779047776e288c0bc234",
"ContainerConfig": {
"Hostname": "91d1af87c3bb",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"LABEL io.buildpacks.stack.id=io.buildpacks.stacks.bionic"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.base.distro.name": "ubuntu",
"io.buildpacks.base.distro.version": "24.04",
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic"
}
},
"DockerVersion": "18.09.6",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.base.distro.name": "ubuntu",
"io.buildpacks.base.distro.version": "24.04",
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 71248531,
"VirtualSize": 71248531,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/17f0a4530fbc3e2982f9dc8feb8c8ddc124473bdd50130dae20856ac597d82dd/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff",
"MergedDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/merged",
"UpperDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/diff",
"WorkDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853",
"sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f",
"sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d",
"sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75",
"sha256:c1daeb79beb276c7441d9a1d7281433e9a7edb9f652b8996ecc62b51e88a47b2",
"sha256:eb195d29dc1aa6e4239f00e7868deebc5ac12bebe76104e0b774c1ef29ca78e3"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
@@ -0,0 +1,101 @@
{
"Id": "sha256:1332879bc8e38793a45ebe5a750f2a1c35df07ec2aa9c18f694644a9de77359b",
"RepoTags": [
"cloudfoundry/run:base-cnb"
],
"RepoDigests": [
"cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad"
],
"Parent": "",
"Comment": "",
"Created": "2020-03-20T20:18:18.117972538Z",
"Container": "91d1af87c3bb6163cd9c7cb21e6891cd25f5fa3c7417779047776e288c0bc234",
"ContainerConfig": {
"Hostname": "91d1af87c3bb",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"LABEL io.buildpacks.stack.id=io.buildpacks.stacks.bionic"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.stack.distro.name": "ubuntu",
"io.buildpacks.stack.distro.version": "26.04",
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic.tiny"
}
},
"DockerVersion": "18.09.6",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.stack.distro.name": "ubuntu",
"io.buildpacks.stack.distro.version": "26.04",
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic.tiny"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 71248531,
"VirtualSize": 71248531,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/17f0a4530fbc3e2982f9dc8feb8c8ddc124473bdd50130dae20856ac597d82dd/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff",
"MergedDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/merged",
"UpperDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/diff",
"WorkDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853",
"sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f",
"sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d",
"sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75",
"sha256:c1daeb79beb276c7441d9a1d7281433e9a7edb9f652b8996ecc62b51e88a47b2",
"sha256:eb195d29dc1aa6e4239f00e7868deebc5ac12bebe76104e0b774c1ef29ca78e3"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}