Warn when builder and run image stack IDs do not match

Spring Boot's Buildpacks integration inspects the stack IDs contained
in labels on the builder image and run image. Prior to this change, it
was a failure condition when the stack IDs did not match. Stack IDs
have been deprecated in the CNB spec, and it is no longer required
that the builder and run images match. The validation of stack IDs
was changed from an error to a warning to match the `pack` reference
implementation.

See gh-51390

Signed-off-by: Scott Frederick <scottyfred@gmail.com>
This commit is contained in:
Scott Frederick
2026-08-21 10:49:17 +02:00
committed by Stéphane Nicoll
parent b1ac5f53dd
commit 829fd8bb29
4 changed files with 27 additions and 9 deletions
@@ -128,6 +128,13 @@ public abstract class AbstractBuildLog implements BuildLog {
log();
}
@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));
log();
}
private String getDigest(Image image) {
List<String> digests = image.getDigests();
return (digests.isEmpty() ? "" : digests.get(0));
@@ -135,6 +135,16 @@ 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
* @since 4.0.8
*/
default void stackIdsDoNotMatch(String runImageStackId, String builderImageStackId) {
}
/**
* Factory method that returns a {@link BuildLog} the outputs to {@link System#out}.
* @return a build log instance that logs to system out
@@ -120,7 +120,7 @@ public class Builder {
request = request.withRunImage(request.getRunImage().withDigest(runImage.getPrimaryDigest()));
runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform);
}
assertStackIdsMatch(runImage, builderImage);
warnIfStackIdsDoNotMatch(runImage, builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage);
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, platform, builderMetadata,
@@ -159,12 +159,11 @@ public class Builder {
return ImageReference.of(runImageName).inTaggedOrDigestForm();
}
private void assertStackIdsMatch(Image runImage, Image builderImage) {
private void warnIfStackIdsDoNotMatch(Image runImage, Image builderImage) {
StackId runImageStackId = StackId.fromImage(runImage);
StackId builderImageStackId = StackId.fromImage(builderImage);
if (runImageStackId.hasId() && builderImageStackId.hasId()) {
Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId
+ "' does not match builder stack '" + builderImageStackId + "'");
if (runImageStackId.hasId() && builderImageStackId.hasId() && !runImageStackId.equals(builderImageStackId)) {
this.log.stackIdsDoNotMatch(runImageStackId.toString(), builderImageStackId.toString());
}
}
@@ -447,7 +447,7 @@ class BuilderTests {
}
@Test
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
void buildWhenStackIdDoesNotMatchLogsWarning() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
@@ -458,9 +458,11 @@ class BuilderTests {
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
assertThatIllegalStateException().isThrownBy(() -> builder.build(request))
.withMessage(
"Run image stack 'org.cloudfoundry.stacks.cfwindowsfs3' does not match builder stack 'io.buildpacks.stacks.bionic'");
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'");
}
@Test