From b62a0c1ae055de3bd387922b3db9acf9648a10e9 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 11 Feb 2025 10:15:58 +0100 Subject: [PATCH] Add SSL support to Docker Compose and Testcontainers infrastructure See gh-41137 Co-authored-by: Phillip Webb --- .../compose/core/DefaultRunningService.java | 10 +- .../docker/compose/core/RunningService.java | 11 +- ...DockerComposeConnectionDetailsFactory.java | 125 ++++++++++++++ .../ContainerConnectionDetailsFactory.java | 31 ++++ .../connection/ContainerConnectionSource.java | 29 +++- .../service/connection/JksKeyStore.java | 71 ++++++++ .../service/connection/JksTrustStore.java | 71 ++++++++ .../service/connection/PemKeyStore.java | 71 ++++++++ .../service/connection/PemTrustStore.java | 71 ++++++++ .../service/connection/ServiceConnection.java | 11 +- ...eConnectionAutoConfigurationRegistrar.java | 35 ++-- .../ServiceConnectionContextCustomizer.java | 8 +- ...iceConnectionContextCustomizerFactory.java | 10 +- .../service/connection/Ssl.java | 78 +++++++++ .../service/connection/SslBundleSource.java | 152 ++++++++++++++++++ .../ConnectionDetailsRegistrarTests.java | 4 +- ...ontainerConnectionDetailsFactoryTests.java | 7 +- .../ContainerConnectionSourceTests.java | 8 +- ...rviceConnectionContextCustomizerTests.java | 22 +-- .../TestContainerConnectionSource.java | 4 +- 20 files changed, 776 insertions(+), 53 deletions(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksKeyStore.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksTrustStore.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemKeyStore.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemTrustStore.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/Ssl.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/SslBundleSource.java diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java index 700ec8e76a..2509d82c81 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -45,6 +45,8 @@ class DefaultRunningService implements RunningService, OriginProvider { private final DockerEnv env; + private final DockerComposeFile composeFile; + DefaultRunningService(DockerHost host, DockerComposeFile composeFile, DockerCliComposePsResponse composePsResponse, DockerCliInspectResponse inspectResponse) { this.origin = new DockerComposeOrigin(composeFile, composePsResponse.name()); @@ -55,6 +57,7 @@ class DefaultRunningService implements RunningService, OriginProvider { this.ports = new DefaultConnectionPorts(inspectResponse); this.env = new DockerEnv(inspectResponse.config().env()); this.labels = Collections.unmodifiableMap(inspectResponse.config().labels()); + this.composeFile = composeFile; } @Override @@ -97,4 +100,9 @@ class DefaultRunningService implements RunningService, OriginProvider { return this.name; } + @Override + public DockerComposeFile composeFile() { + return this.composeFile; + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java index eefc8caffe..80e0ee0b85 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -64,4 +64,13 @@ public interface RunningService { */ Map labels(); + /** + * Return the Docker Compose file for the service. + * @return the Docker Compose file + * @since 3.5.0 + */ + default DockerComposeFile composeFile() { + return null; + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java index 0224c443cc..7fae097629 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java @@ -16,17 +16,33 @@ package org.springframework.boot.docker.compose.service.connection; +import java.nio.file.Path; import java.util.Arrays; +import java.util.Set; import java.util.function.Predicate; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory; +import org.springframework.boot.docker.compose.core.DockerComposeFile; import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginProvider; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStore; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Base class for {@link ConnectionDetailsFactory} implementations that provide @@ -106,6 +122,8 @@ public abstract class DockerComposeConnectionDetailsFactory ciphersSet = null; + if (StringUtils.hasLength(ciphers)) { + ciphersSet = StringUtils.commaDelimitedListToSet(ciphers); + } + Set enabledProtocolsSet = null; + if (StringUtils.hasLength(enabledProtocols)) { + enabledProtocolsSet = StringUtils.commaDelimitedListToSet(enabledProtocols); + } + return SslOptions.of(ciphersSet, enabledProtocolsSet); + } + + private SslBundle getPemSslBundle(RunningService service) { + PemSslStoreDetails keyStoreDetails = getPemSslStoreDetails(service, "keystore"); + PemSslStoreDetails trustStoreDetails = getPemSslStoreDetails(service, "truststore"); + if (keyStoreDetails == null && trustStoreDetails == null) { + return null; + } + SslBundleKey key = SslBundleKey.of(service.labels().get("org.springframework.boot.sslbundle.pem.key.alias"), + service.labels().get("org.springframework.boot.sslbundle.pem.key.password")); + SslOptions options = createSslOptions( + service.labels().get("org.springframework.boot.sslbundle.pem.options.ciphers"), + service.labels().get("org.springframework.boot.sslbundle.pem.options.enabled-protocols")); + String protocol = service.labels().get("org.springframework.boot.sslbundle.pem.protocol"); + Path workingDirectory = getWorkingDirectory(service); + ResourceLoader resourceLoader = getResourceLoader(workingDirectory); + return SslBundle.of(new PemSslStoreBundle(PemSslStore.load(keyStoreDetails, resourceLoader), + PemSslStore.load(trustStoreDetails, resourceLoader)), key, options, protocol); + } + + private PemSslStoreDetails getPemSslStoreDetails(RunningService service, String storeType) { + String type = service.labels().get("org.springframework.boot.sslbundle.pem.%s.type".formatted(storeType)); + String certificate = service.labels() + .get("org.springframework.boot.sslbundle.pem.%s.certificate".formatted(storeType)); + String privateKey = service.labels() + .get("org.springframework.boot.sslbundle.pem.%s.private-key".formatted(storeType)); + String privateKeyPassword = service.labels() + .get("org.springframework.boot.sslbundle.pem.%s.private-key-password".formatted(storeType)); + if (certificate == null && privateKey == null) { + return null; + } + return new PemSslStoreDetails(type, certificate, privateKey, privateKeyPassword); + } + } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java index ccf6bedd74..6ad17a87c5 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java @@ -16,6 +16,7 @@ package org.springframework.boot.testcontainers.service.connection; +import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; @@ -33,6 +34,7 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetai import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory; import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginProvider; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.lifecycle.TestcontainersStartup; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -166,6 +168,8 @@ public abstract class ContainerConnectionDetailsFactory, private volatile C container; + private volatile SslBundle sslBundle; + /** * Create a new {@link ContainerConnectionDetails} instance. * @param source the source {@link ContainerConnectionSource} @@ -194,6 +198,33 @@ public abstract class ContainerConnectionDetailsFactory, return this.container; } + /** + * Return the {@link SslBundle} to use with this connection or {@code null}. + * @return the ssl bundle or {@code null} + * @since 3.5.0 + */ + protected SslBundle getSslBundle() { + if (this.source.getSslBundleSource() == null) { + return null; + } + SslBundle sslBundle = this.sslBundle; + if (sslBundle == null) { + sslBundle = this.source.getSslBundleSource().getSslBundle(); + this.sslBundle = sslBundle; + } + return sslBundle; + } + + /** + * Whether the field or bean is annotated with the given annotation. + * @param annotationType the annotation to check + * @return whether the field or bean is annotated with the annotation + * @since 3.5.0 + */ + protected boolean hasAnnotation(Class annotationType) { + return this.source.hasAnnotation(annotationType); + } + @Override public Origin getOrigin() { return this.source.getOrigin(); diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java index d589b4274d..598271fd62 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -16,6 +16,7 @@ package org.springframework.boot.testcontainers.service.connection; +import java.lang.annotation.Annotation; import java.util.Set; import java.util.function.Supplier; @@ -27,6 +28,7 @@ import org.testcontainers.utility.DockerImageName; import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginProvider; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.log.LogMessage; import org.springframework.util.StringUtils; @@ -60,8 +62,13 @@ public final class ContainerConnectionSource> implements private final Supplier containerSupplier; + private final SslBundleSource sslBundleSource; + + private final MergedAnnotations annotations; + ContainerConnectionSource(String beanNameSuffix, Origin origin, Class containerType, String containerImageName, - MergedAnnotation annotation, Supplier containerSupplier) { + MergedAnnotation annotation, Supplier containerSupplier, + SslBundleSource sslBundleSource, MergedAnnotations annotations) { this.beanNameSuffix = beanNameSuffix; this.origin = origin; this.containerType = containerType; @@ -69,10 +76,13 @@ public final class ContainerConnectionSource> implements this.connectionName = getOrDeduceConnectionName(annotation.getString("name"), containerImageName); this.connectionDetailsTypes = Set.of(annotation.getClassArray("type")); this.containerSupplier = containerSupplier; + this.sslBundleSource = sslBundleSource; + this.annotations = annotations; } ContainerConnectionSource(String beanNameSuffix, Origin origin, Class containerType, String containerImageName, - ServiceConnection annotation, Supplier containerSupplier) { + ServiceConnection annotation, Supplier containerSupplier, SslBundleSource sslBundleSource, + MergedAnnotations annotations) { this.beanNameSuffix = beanNameSuffix; this.origin = origin; this.containerType = containerType; @@ -80,6 +90,8 @@ public final class ContainerConnectionSource> implements this.connectionName = getOrDeduceConnectionName(annotation.name(), containerImageName); this.connectionDetailsTypes = Set.of(annotation.type()); this.containerSupplier = containerSupplier; + this.sslBundleSource = sslBundleSource; + this.annotations = annotations; } private static String getOrDeduceConnectionName(String connectionName, String containerImageName) { @@ -156,6 +168,17 @@ public final class ContainerConnectionSource> implements return this.connectionDetailsTypes; } + SslBundleSource getSslBundleSource() { + return this.sslBundleSource; + } + + boolean hasAnnotation(Class annotationType) { + if (this.annotations == null) { + return false; + } + return this.annotations.isPresent(annotationType); + } + @Override public String toString() { return "@ServiceConnection source for %s".formatted(this.origin); diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksKeyStore.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksKeyStore.java new file mode 100644 index 0000000000..6973794509 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksKeyStore.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2025 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.testcontainers.service.connection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.core.annotation.AliasFor; + +/** + * Configures the {@link JksSslStoreBundle} key store to use with an {@link SslBundle SSL} + * supported {@link ServiceConnection @ServiceConnection}. + * + * @author Phillip Webb + * @since 3.5.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface JksKeyStore { + + /** + * Alias for {@link #location()}. + * @return the store location + */ + @AliasFor("location") + String value() default ""; + + /** + * The location of the resource containing the store content. + * @return the store location + */ + @AliasFor("value") + String location() default ""; + + /** + * The password used to access the store. + * @return the store password + */ + String password() default ""; + + /** + * The type of the store to create, e.g. JKS. + * @return store type + */ + String type() default ""; + + /** + * The provider for the store. + * @return the store provider + */ + String provider() default ""; + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksTrustStore.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksTrustStore.java new file mode 100644 index 0000000000..2c8fd42bcd --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/JksTrustStore.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2025 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.testcontainers.service.connection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.core.annotation.AliasFor; + +/** + * Configures the {@link JksSslStoreBundle} trust store to use with an {@link SslBundle + * SSL} supported {@link ServiceConnection @ServiceConnection}. + * + * @author Phillip Webb + * @since 3.5.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface JksTrustStore { + + /** + * Alias for {@link #location()}. + * @return the store location + */ + @AliasFor("location") + String value() default ""; + + /** + * The location of the resource containing the store content. + * @return the store location + */ + @AliasFor("value") + String location() default ""; + + /** + * The password used to access the store. + * @return the store password + */ + String password() default ""; + + /** + * The type of the store to create, e.g. JKS. + * @return store type + */ + String type() default ""; + + /** + * The provider for the store. + * @return the store provider + */ + String provider() default ""; + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemKeyStore.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemKeyStore.java new file mode 100644 index 0000000000..a612dc25ec --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemKeyStore.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2025 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.testcontainers.service.connection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.core.annotation.AliasFor; + +/** + * Configures the {@link PemSslStoreBundle} key store to use with an {@link SslBundle SSL} + * supported {@link ServiceConnection @ServiceConnection}. + * + * @author Phillip Webb + * @since 3.5.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface PemKeyStore { + + /** + * Alias for {@link #certificate()}. + * @return the store certificate + */ + @AliasFor("certificate") + String value() default ""; + + /** + * The location or content of the certificate or certificate chain in PEM format. + * @return the store certificate location or content + */ + @AliasFor("value") + String certificate() default ""; + + /** + * The location or content of the private key in PEM format. + * @return the store private key location or content + */ + String privateKey() default ""; + + /** + * The password used to decrypt an encrypted private key. + * @return the store private key password + */ + String privateKeyPassword() default ""; + + /** + * The type of the store to create, e.g. JKS. + * @return the store type + */ + String type() default ""; + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemTrustStore.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemTrustStore.java new file mode 100644 index 0000000000..187ad4944d --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/PemTrustStore.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2025 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.testcontainers.service.connection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.core.annotation.AliasFor; + +/** + * Configures the {@link PemSslStoreBundle} trust store to use with an {@link SslBundle + * SSL} supported {@link ServiceConnection @ServiceConnection}. + * + * @author Phillip Webb + * @since 3.5.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface PemTrustStore { + + /** + * Alias for {@link #certificate()}. + * @return the store certificate + */ + @AliasFor("certificate") + String value() default ""; + + /** + * The location or content of the certificate or certificate chain in PEM format. + * @return the store certificate location or content + */ + @AliasFor("value") + String certificate() default ""; + + /** + * The location or content of the private key in PEM format. + * @return the store private key location or content + */ + String privateKey() default ""; + + /** + * The password used to decrypt an encrypted private key. + * @return the store private key password + */ + String privateKeyPassword() default ""; + + /** + * The type of the store to create, e.g. JKS. + * @return the store type + */ + String type() default ""; + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnection.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnection.java index a6827dfd2c..248a67cae6 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnection.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -29,8 +29,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.AliasFor; /** - * Annotation used to indicate that a field or method is a - * {@link ContainerConnectionSource} which provides a service that can be connected to. + * Indicates that a field or method is a {@link ContainerConnectionSource} which provides + * a service that can be connected to. + *

+ * If the underling connection supports SSL, the {@link PemKeyStore @PemKeyStore}, + * {@link PemTrustStore @PemTrustStore}, {@link JksKeyStore @JksKeyStore}, + * {@link JksTrustStore @JksTrustStore}, {@link Ssl @Ssl} annotations may be used to + * provide additional configuration. * * @author Moritz Halbritter * @author Andy Wilkinson diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java index ae6c9b10ea..144d2fa677 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -31,6 +31,7 @@ import org.springframework.boot.origin.Origin; import org.springframework.boot.testcontainers.beans.TestcontainerBeanDefinition; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; /** @@ -59,24 +60,27 @@ class ServiceConnectionAutoConfigurationRegistrar implements ImportBeanDefinitio new ConnectionDetailsFactories()); for (String beanName : beanFactory.getBeanNamesForType(Container.class)) { BeanDefinition beanDefinition = getBeanDefinition(beanFactory, beanName); - for (ServiceConnection annotation : getAnnotations(beanFactory, beanName, beanDefinition)) { - ContainerConnectionSource source = createSource(beanFactory, beanName, beanDefinition, annotation); + MergedAnnotations annotations = (beanDefinition instanceof TestcontainerBeanDefinition testcontainerBeanDefinition) + ? testcontainerBeanDefinition.getAnnotations() : null; + for (ServiceConnection serviceConnection : getServiceConnections(beanFactory, beanName, annotations)) { + ContainerConnectionSource source = createSource(beanFactory, beanName, beanDefinition, annotations, + serviceConnection); registrar.registerBeanDefinitions(registry, source); } } } - private Set getAnnotations(ConfigurableListableBeanFactory beanFactory, String beanName, - BeanDefinition beanDefinition) { - Set annotations = new LinkedHashSet<>( - beanFactory.findAllAnnotationsOnBean(beanName, ServiceConnection.class, false)); - if (beanDefinition instanceof TestcontainerBeanDefinition testcontainerBeanDefinition) { - testcontainerBeanDefinition.getAnnotations() - .stream(ServiceConnection.class) + private Set getServiceConnections(ConfigurableListableBeanFactory beanFactory, String beanName, + MergedAnnotations annotations) { + Set serviceConnections = beanFactory.findAllAnnotationsOnBean(beanName, + ServiceConnection.class, false); + if (annotations != null) { + serviceConnections = new LinkedHashSet<>(serviceConnections); + annotations.stream(ServiceConnection.class) .map(MergedAnnotation::synthesize) - .forEach(annotations::add); + .forEach(serviceConnections::add); } - return annotations; + return serviceConnections; } private BeanDefinition getBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) { @@ -91,13 +95,14 @@ class ServiceConnectionAutoConfigurationRegistrar implements ImportBeanDefinitio @SuppressWarnings("unchecked") private > ContainerConnectionSource createSource( ConfigurableListableBeanFactory beanFactory, String beanName, BeanDefinition beanDefinition, - ServiceConnection annotation) { + MergedAnnotations annotations, ServiceConnection serviceConnection) { Origin origin = new BeanOrigin(beanName, beanDefinition); Class containerType = (Class) beanFactory.getType(beanName, false); String containerImageName = (beanDefinition instanceof TestcontainerBeanDefinition testcontainerBeanDefinition) ? testcontainerBeanDefinition.getContainerImageName() : null; - return new ContainerConnectionSource<>(beanName, origin, containerType, containerImageName, annotation, - () -> beanFactory.getBean(beanName, containerType)); + return new ContainerConnectionSource<>(beanName, origin, containerType, containerImageName, serviceConnection, + () -> beanFactory.getBean(beanName, containerType), + SslBundleSource.get(beanFactory, beanName, annotations), annotations); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java index 9d20af2ac2..e5ade0d084 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -91,10 +91,12 @@ class ServiceConnectionContextCustomizer implements ContextCustomizer { * Relevant details from {@link ContainerConnectionSource} used as a * MergedContextConfiguration cache key. */ - private record CacheKey(String connectionName, Set> connectionDetailsTypes, Container container) { + private record CacheKey(String connectionName, Set> connectionDetailsTypes, Container container, + SslBundleSource sslBundleSource) { CacheKey(ContainerConnectionSource source) { - this(source.getConnectionName(), source.getConnectionDetailsTypes(), source.getContainerSupplier().get()); + this(source.getConnectionName(), source.getConnectionDetailsTypes(), source.getContainerSupplier().get(), + source.getSslBundleSource()); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java index be70abf452..bbf6ac61b3 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -61,7 +61,7 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact ReflectionUtils.doWithLocalFields(candidate, (field) -> { MergedAnnotations annotations = MergedAnnotations.from(field); annotations.stream(ServiceConnection.class) - .forEach((annotation) -> sources.add(createSource(field, annotation))); + .forEach((serviceConnection) -> sources.add(createSource(field, annotations, serviceConnection))); }); if (TestContextAnnotationUtils.searchEnclosingClass(candidate)) { collectSources(candidate.getEnclosingClass(), sources); @@ -74,7 +74,7 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact @SuppressWarnings("unchecked") private > ContainerConnectionSource createSource(Field field, - MergedAnnotation annotation) { + MergedAnnotations annotations, MergedAnnotation serviceConnection) { Assert.state(Modifier.isStatic(field.getModifiers()), () -> "@ServiceConnection field '%s' must be static".formatted(field.getName())); Origin origin = new FieldOrigin(field); @@ -87,8 +87,8 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact // When running tests that doesn't matter, but running AOT processing should be // possible without a Docker environment String dockerImageName = isAotProcessingInProgress() ? null : container.getDockerImageName(); - return new ContainerConnectionSource<>("test", origin, containerType, dockerImageName, annotation, - () -> container); + return new ContainerConnectionSource<>("test", origin, containerType, dockerImageName, serviceConnection, + () -> container, SslBundleSource.get(annotations), annotations); } private Object getFieldValue(Field field) { diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/Ssl.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/Ssl.java new file mode 100644 index 0000000000..5139556957 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/Ssl.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2025 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.testcontainers.service.connection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslOptions; + +/** + * Configures the {@link SslOptions}, {@link SslBundleKey @SslBundleKey} and + * {@link SslBundle#getProtocol() protocol} to use with an {@link SslBundle SSL} supported + * {@link ServiceConnection @ServiceConnection}. + *

+ * Also serves as a signal to enable automatic {@link SslBundle} extraction from supported + * containers. + * + * @author Phillip Webb + * @since 3.5.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface Ssl { + + /** + * The protocol to use for the SSL connection. + * @return the SSL protocol + * @see SslBundle#getProtocol() + */ + String protocol() default SslBundle.DEFAULT_PROTOCOL; + + /** + * The ciphers that can be used for the SSL connection. + * @return the SSL ciphers + * @see SslOptions#getCiphers() + */ + String[] ciphers() default {}; + + /** + * The protocols that are enabled for the SSL connection. + * @return the enabled SSL protocols + * @see SslOptions#getEnabledProtocols() + */ + String[] enabledProtocols() default {}; + + /** + * The password that should be used to access the key. + * @return the key password + * @see SslBundleKey#getPassword() + */ + String keyPassword() default ""; + + /** + * The alias that should be used to access the key. + * @return the key alias + * @see SslBundleKey#getAlias() + */ + String keyAlias() default ""; + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/SslBundleSource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/SslBundleSource.java new file mode 100644 index 0000000000..2b32a7af75 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/SslBundleSource.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2025 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.testcontainers.service.connection; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link SslBundle} source created from annotations. Used as a cache key and as a + * {@link SslBundle} factory. + * + * @param ssl the {@link Ssl @Ssl} annotation + * @param pemKeyStore the {@link PemKeyStore @PemKeyStore} annotation + * @param pemTrustStore the {@link PemTrustStore @PemTrustStore} annotation + * @param jksKeyStore the {@link JksKeyStore @JksKeyStore} annotation + * @param jksTrustStore the {@link JksTrustStore @JksTrustStore} annotation + * @author Phillip Webb + * @author Moritz Halbritter + */ +record SslBundleSource(Ssl ssl, PemKeyStore pemKeyStore, PemTrustStore pemTrustStore, JksKeyStore jksKeyStore, + JksTrustStore jksTrustStore) { + + SslBundleSource { + boolean hasPem = (pemKeyStore != null || pemTrustStore != null); + boolean hasJks = (jksKeyStore != null || jksTrustStore != null); + if (hasJks && hasPem) { + throw new IllegalStateException("PEM and JKS store annotations cannot be used together"); + } + } + + SslBundle getSslBundle() { + SslStoreBundle stores = stores(); + if (stores == null) { + return null; + } + Ssl ssl = (this.ssl != null) ? this.ssl : MergedAnnotation.of(Ssl.class).synthesize(); + SslOptions options = SslOptions.of(nullIfEmpty(ssl.ciphers()), nullIfEmpty(ssl.enabledProtocols())); + SslBundleKey key = SslBundleKey.of(nullIfEmpty(ssl.keyPassword()), nullIfEmpty(ssl.keyAlias())); + String protocol = ssl.protocol(); + return SslBundle.of(stores, key, options, protocol); + } + + private SslStoreBundle stores() { + if (this.pemKeyStore != null || this.pemTrustStore != null) { + return new PemSslStoreBundle(pemKeyStoreDetails(), pemTrustStoreDetails()); + } + if (this.jksKeyStore != null || this.jksTrustStore != null) { + return new JksSslStoreBundle(jksKeyStoreDetails(), jksTrustStoreDetails()); + } + return null; + } + + private PemSslStoreDetails pemKeyStoreDetails() { + PemKeyStore store = this.pemKeyStore; + return (store != null) ? new PemSslStoreDetails(nullIfEmpty(store.type()), nullIfEmpty(store.certificate()), + nullIfEmpty(store.privateKey()), nullIfEmpty(store.privateKeyPassword())) : null; + } + + private PemSslStoreDetails pemTrustStoreDetails() { + PemTrustStore store = this.pemTrustStore; + return (store != null) ? new PemSslStoreDetails(nullIfEmpty(store.type()), nullIfEmpty(store.certificate()), + nullIfEmpty(store.privateKey()), nullIfEmpty(store.privateKeyPassword())) : null; + } + + private JksSslStoreDetails jksKeyStoreDetails() { + JksKeyStore store = this.jksKeyStore; + return (store != null) ? new JksSslStoreDetails(nullIfEmpty(store.type()), nullIfEmpty(store.provider()), + nullIfEmpty(store.location()), nullIfEmpty(store.password())) : null; + } + + private JksSslStoreDetails jksTrustStoreDetails() { + JksTrustStore store = this.jksTrustStore; + return (store != null) ? new JksSslStoreDetails(nullIfEmpty(store.type()), nullIfEmpty(store.provider()), + nullIfEmpty(store.location()), nullIfEmpty(store.password())) : null; + } + + private String nullIfEmpty(String string) { + if (StringUtils.hasLength(string)) { + return string; + } + return null; + } + + private String[] nullIfEmpty(String[] array) { + if (array == null || array.length == 0) { + return null; + } + return array; + } + + static SslBundleSource get(MergedAnnotations annotations) { + return get(null, null, annotations); + } + + static SslBundleSource get(ListableBeanFactory beanFactory, String beanName, MergedAnnotations annotations) { + Ssl ssl = getAnnotation(beanFactory, beanName, annotations, Ssl.class); + PemKeyStore pemKeyStore = getAnnotation(beanFactory, beanName, annotations, PemKeyStore.class); + PemTrustStore pemTrustStore = getAnnotation(beanFactory, beanName, annotations, PemTrustStore.class); + JksKeyStore jksKeyStore = getAnnotation(beanFactory, beanName, annotations, JksKeyStore.class); + JksTrustStore jksTrustStore = getAnnotation(beanFactory, beanName, annotations, JksTrustStore.class); + if (ssl == null && pemKeyStore == null && pemTrustStore == null && jksKeyStore == null + && jksTrustStore == null) { + return null; + } + return new SslBundleSource(ssl, pemKeyStore, pemTrustStore, jksKeyStore, jksTrustStore); + } + + private static A getAnnotation(ListableBeanFactory beanFactory, String beanName, + MergedAnnotations annotations, Class annotationType) { + Set found = (beanFactory != null) ? beanFactory.findAllAnnotationsOnBean(beanName, annotationType, false) + : Collections.emptySet(); + if (annotations != null) { + found = new LinkedHashSet<>(found); + annotations.stream(annotationType).map(MergedAnnotation::synthesize).forEach(found::add); + } + int size = found.size(); + Assert.state(size <= 1, + () -> "Expected single %s annotation, but found %d".formatted(annotationType.getName(), size)); + return (size > 0) ? found.iterator().next() : null; + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ConnectionDetailsRegistrarTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ConnectionDetailsRegistrarTests.java index d40ce9664c..179931ed8b 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ConnectionDetailsRegistrarTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ConnectionDetailsRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -59,7 +59,7 @@ class ConnectionDetailsRegistrarTests { this.container = mock(PostgreSQLContainer.class); this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", "", "type", new Class[0])); this.source = new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, null, - this.annotation, () -> this.container); + this.annotation, () -> this.container, null, null); this.factories = mock(ConnectionDetailsFactories.class); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java index b24b25f1a8..828cec1e5e 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -65,7 +65,7 @@ class ContainerConnectionDetailsFactoryTests { this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", "myname", "type", new Class[0])); this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class, - this.container.getDockerImageName(), this.annotation, () -> this.container); + this.container.getDockerImageName(), this.annotation, () -> this.container, null, null); } @Test @@ -109,7 +109,8 @@ class ContainerConnectionDetailsFactoryTests { void getConnectionDetailsWhenContainerTypeDoesNotMatchReturnsNull() { ElasticsearchContainer container = mock(ElasticsearchContainer.class); ContainerConnectionSource source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, - ElasticsearchContainer.class, container.getDockerImageName(), this.annotation, () -> container); + ElasticsearchContainer.class, container.getDockerImageName(), this.annotation, () -> container, null, + null); TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory(); ConnectionDetails connectionDetails = getConnectionDetails(factory, source); assertThat(connectionDetails).isNull(); diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSourceTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSourceTests.java index 913b08037e..fa22ec512d 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSourceTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -60,7 +60,7 @@ class ContainerConnectionSourceTests { given(this.container.getDockerImageName()).willReturn("postgres"); this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", "", "type", new Class[0])); this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class, - this.container.getDockerImageName(), this.annotation, () -> this.container); + this.container.getDockerImageName(), this.annotation, () -> this.container, null, null); } @Test @@ -180,14 +180,14 @@ class ContainerConnectionSourceTests { private void setupSourceAnnotatedWithName(String name) { this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", name, "type", new Class[0])); this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class, - this.container.getDockerImageName(), this.annotation, () -> this.container); + this.container.getDockerImageName(), this.annotation, () -> this.container, null, null); } private void setupSourceAnnotatedWithType(Class type) { this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", "", "type", new Class[] { type })); this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class, - this.container.getDockerImageName(), this.annotation, () -> this.container); + this.container.getDockerImageName(), this.annotation, () -> this.container, null, null); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerTests.java index 9ce9c30f9b..04a084f244 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -66,7 +66,7 @@ class ServiceConnectionContextCustomizerTests { this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", "myname", "type", new Class[0])); this.source = new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, - this.container.getDockerImageName(), this.annotation, () -> this.container); + this.container.getDockerImageName(), this.annotation, () -> this.container, null, null); this.factories = mock(ConnectionDetailsFactories.class); } @@ -103,37 +103,37 @@ class ServiceConnectionContextCustomizerTests { // Connection Names ServiceConnectionContextCustomizer n1 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation1, () -> container1))); + annotation1, () -> container1, null, null))); ServiceConnectionContextCustomizer n2 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation1, () -> container1))); + annotation1, () -> container1, null, null))); ServiceConnectionContextCustomizer n3 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "namex", - annotation1, () -> container1))); + annotation1, () -> container1, null, null))); assertThat(n1.hashCode()).isEqualTo(n2.hashCode()).isNotEqualTo(n3.hashCode()); assertThat(n1).isEqualTo(n2).isNotEqualTo(n3); // Connection Details Types ServiceConnectionContextCustomizer t1 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation1, () -> container1))); + annotation1, () -> container1, null, null))); ServiceConnectionContextCustomizer t2 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation2, () -> container1))); + annotation2, () -> container1, null, null))); ServiceConnectionContextCustomizer t3 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation3, () -> container1))); + annotation3, () -> container1, null, null))); assertThat(t1.hashCode()).isEqualTo(t2.hashCode()).isNotEqualTo(t3.hashCode()); assertThat(t1).isEqualTo(t2).isNotEqualTo(t3); // Container ServiceConnectionContextCustomizer c1 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation1, () -> container1))); + annotation1, () -> container1, null, null))); ServiceConnectionContextCustomizer c2 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation1, () -> container1))); + annotation1, () -> container1, null, null))); ServiceConnectionContextCustomizer c3 = new ServiceConnectionContextCustomizer( List.of(new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, "name", - annotation1, () -> container2))); + annotation1, () -> container2, null, null))); assertThat(c1.hashCode()).isEqualTo(c2.hashCode()).isNotEqualTo(c3.hashCode()); assertThat(c1).isEqualTo(c2).isNotEqualTo(c3); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/TestContainerConnectionSource.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/TestContainerConnectionSource.java index dda8088ece..55515a134c 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/TestContainerConnectionSource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/TestContainerConnectionSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -37,7 +37,7 @@ public final class TestContainerConnectionSource { Class containerType, String containerImageName, MergedAnnotation annotation, Supplier containerSupplier) { return new ContainerConnectionSource<>(beanNameSuffix, origin, containerType, containerImageName, annotation, - containerSupplier); + containerSupplier, null, null); } }