Merge branch '4.0.x'

Closes gh-48701
This commit is contained in:
Andy Wilkinson
2026-01-08 12:14:50 +00:00
212 changed files with 2601 additions and 356 deletions
@@ -26,6 +26,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.util.TimeValue;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -128,7 +129,7 @@ abstract class AbstractDeploymentTests {
}
WarDeploymentContainer(String baseImage, String deploymentLocation, int port,
Consumer<DockerfileBuilder> dockerfileCustomizer) {
@Nullable Consumer<DockerfileBuilder> dockerfileCustomizer) {
super(new ImageFromDockerfile().withFileFromFile("spring-boot.war", findWarToDeploy())
.withDockerfileFromBuilder((builder) -> {
builder.from(baseImage).add("spring-boot.war", deploymentLocation + "/spring-boot.war");
@@ -0,0 +1,20 @@
/*
* 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.
*/
@NullMarked
package org.springframework.boot.deployment;
import org.jspecify.annotations.NullMarked;
@@ -30,6 +30,7 @@ import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.ListAssert;
import org.assertj.core.api.ObjectAssert;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.test.json.JsonContentAssert;
import org.springframework.lang.CheckReturnValue;
@@ -81,7 +82,7 @@ public class ContainerConfigAssert extends AbstractAssert<ContainerConfigAssert,
*/
public static class LabelsAssert extends AbstractMapAssert<LabelsAssert, Map<String, String>, String, String> {
protected LabelsAssert(Map<String, String> labels) {
protected LabelsAssert(@Nullable Map<String, String> labels) {
super(labels, LabelsAssert.class);
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
@NullMarked
package org.springframework.boot.image.assertions;
import org.jspecify.annotations.NullMarked;
@@ -49,6 +49,7 @@ public class GradleBuildInjectionExtension implements BeforeEachCallback {
.gradleVersion(GradleVersions.minimumCompatible())
.bootVersion(this.bootVersion);
Field field = ReflectionUtils.findField(context.getRequiredTestClass(), "gradleBuild");
Assert.notNull(field, "No gradleBuild field found in " + context.getRequiredTestClass().getName());
field.setAccessible(true);
field.set(context.getRequiredTestInstance(), gradleBuild);
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
@NullMarked
package org.springframework.boot.image.junit;
import org.jspecify.annotations.NullMarked;
@@ -31,7 +31,9 @@ import java.util.jar.JarFile;
import com.github.dockerjava.api.model.ContainerConfig;
import org.assertj.core.api.Condition;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.BuildTask;
import org.gradle.testkit.runner.TaskOutcome;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -62,6 +64,7 @@ import static org.assertj.core.api.Assertions.entry;
@ExtendWith({ GradleBuildInjectionExtension.class, GradleBuildExtension.class })
class PaketoBuilderTests {
@SuppressWarnings("NullAway.Init")
GradleBuild gradleBuild;
@BeforeEach
@@ -81,7 +84,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -113,7 +116,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withCommand("--server.port=9090");
@@ -131,7 +134,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -140,7 +143,7 @@ class PaketoBuilderTests {
}
this.gradleBuild.expectDeprecationMessages("BOM table is deprecated");
result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
container.waitingFor(Wait.forHttp("/test")).start();
@@ -158,7 +161,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + projectName;
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName, "assemble", "bootDistZip");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -195,7 +198,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + projectName;
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName, "assemble", "bootDistZip");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -233,7 +236,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -266,7 +269,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImageWithRetry(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -316,7 +319,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -346,7 +349,7 @@ class PaketoBuilderTests {
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":bootBuildImage")).extracting(BuildTask::getOutcome).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Running creator");
try (GenericContainer<?> container = new GenericContainer<>(imageName)) {
container.withExposedPorts(8080);
@@ -569,7 +572,7 @@ class PaketoBuilderTests {
private static class DigestCapturingCondition extends Condition<Object> {
private static String digest;
private static @Nullable String digest;
DigestCapturingCondition() {
super(predicate(), "a value starting with 'sha256:'");
@@ -583,6 +586,7 @@ class PaketoBuilderTests {
}
String getDigest() {
assertThat(digest).isNotNull();
return digest;
}
@@ -590,14 +594,15 @@ class PaketoBuilderTests {
private static class DigestsCapturingCondition extends Condition<Object> {
private static List<String> digests;
private static @Nullable List<String> digests;
DigestsCapturingCondition() {
super(predicate(), "a value starting with 'sha256:'");
}
private static Predicate<Object> predicate() {
digests = new ArrayList<>();
List<String> digests = new ArrayList<>();
DigestsCapturingCondition.digests = digests;
return (sha) -> {
digests.add(sha.toString());
return sha.toString().startsWith("sha256:");
@@ -605,6 +610,7 @@ class PaketoBuilderTests {
}
String getDigest(int index) {
assertThat(digests).isNotNull();
return digests.get(index);
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
@NullMarked
package org.springframework.boot.image.paketo;
import org.jspecify.annotations.NullMarked;