Add SSL support to Docker Compose and Testcontainers infrastructure
See gh-41137 Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
This commit is contained in:
parent
528b7e9ad9
commit
b62a0c1ae0
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String> labels();
|
||||
|
||||
/**
|
||||
* Return the Docker Compose file for the service.
|
||||
* @return the Docker Compose file
|
||||
* @since 3.5.0
|
||||
*/
|
||||
default DockerComposeFile composeFile() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<D extends Connection
|
|||
|
||||
private final Origin origin;
|
||||
|
||||
private volatile SslBundle sslBundle;
|
||||
|
||||
/**
|
||||
* Create a new {@link DockerComposeConnectionDetails} instance.
|
||||
* @param runningService the source {@link RunningService}
|
||||
|
|
@ -120,6 +138,113 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
|
|||
return this.origin;
|
||||
}
|
||||
|
||||
protected SslBundle getSslBundle(RunningService service) {
|
||||
if (this.sslBundle != null) {
|
||||
return this.sslBundle;
|
||||
}
|
||||
SslBundle jksSslBundle = getJksSslBundle(service);
|
||||
SslBundle pemSslBundle = getPemSslBundle(service);
|
||||
if (jksSslBundle == null && pemSslBundle == null) {
|
||||
return null;
|
||||
}
|
||||
if (jksSslBundle != null && pemSslBundle != null) {
|
||||
throw new IllegalStateException("Mutually exclusive JKS and PEM ssl bundles have been configured");
|
||||
}
|
||||
SslBundle sslBundle = (jksSslBundle != null) ? jksSslBundle : pemSslBundle;
|
||||
this.sslBundle = sslBundle;
|
||||
return sslBundle;
|
||||
}
|
||||
|
||||
private SslBundle getJksSslBundle(RunningService service) {
|
||||
JksSslStoreDetails keyStoreDetails = getJksSslStoreDetails(service, "keystore");
|
||||
JksSslStoreDetails trustStoreDetails = getJksSslStoreDetails(service, "truststore");
|
||||
if (keyStoreDetails == null && trustStoreDetails == null) {
|
||||
return null;
|
||||
}
|
||||
SslBundleKey key = SslBundleKey.of(service.labels().get("org.springframework.boot.sslbundle.jks.key.alias"),
|
||||
service.labels().get("org.springframework.boot.sslbundle.jks.key.password"));
|
||||
SslOptions options = createSslOptions(
|
||||
service.labels().get("org.springframework.boot.sslbundle.jks.options.ciphers"),
|
||||
service.labels().get("org.springframework.boot.sslbundle.jks.options.enabled-protocols"));
|
||||
String protocol = service.labels().get("org.springframework.boot.sslbundle.jks.protocol");
|
||||
Path workingDirectory = getWorkingDirectory(service);
|
||||
return SslBundle.of(
|
||||
new JksSslStoreBundle(keyStoreDetails, trustStoreDetails, getResourceLoader(workingDirectory)), key,
|
||||
options, protocol);
|
||||
}
|
||||
|
||||
private ResourceLoader getResourceLoader(Path workingDirectory) {
|
||||
ClassLoader classLoader = ApplicationResourceLoader.get().getClassLoader();
|
||||
return ApplicationResourceLoader.get(classLoader,
|
||||
SpringFactoriesLoader.forDefaultResourceLocation(classLoader), workingDirectory);
|
||||
}
|
||||
|
||||
private JksSslStoreDetails getJksSslStoreDetails(RunningService service, String storeType) {
|
||||
String type = service.labels().get("org.springframework.boot.sslbundle.jks.%s.type".formatted(storeType));
|
||||
String provider = service.labels()
|
||||
.get("org.springframework.boot.sslbundle.jks.%s.provider".formatted(storeType));
|
||||
String location = service.labels()
|
||||
.get("org.springframework.boot.sslbundle.jks.%s.location".formatted(storeType));
|
||||
String password = service.labels()
|
||||
.get("org.springframework.boot.sslbundle.jks.%s.password".formatted(storeType));
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
return new JksSslStoreDetails(type, provider, location, password);
|
||||
}
|
||||
|
||||
private Path getWorkingDirectory(RunningService runningService) {
|
||||
DockerComposeFile composeFile = runningService.composeFile();
|
||||
if (composeFile == null || CollectionUtils.isEmpty(composeFile.getFiles())) {
|
||||
return Path.of(".");
|
||||
}
|
||||
return composeFile.getFiles().get(0).toPath().getParent();
|
||||
}
|
||||
|
||||
private SslOptions createSslOptions(String ciphers, String enabledProtocols) {
|
||||
Set<String> ciphersSet = null;
|
||||
if (StringUtils.hasLength(ciphers)) {
|
||||
ciphersSet = StringUtils.commaDelimitedListToSet(ciphers);
|
||||
}
|
||||
Set<String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<C extends Container<?>,
|
|||
|
||||
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<C extends Container<?>,
|
|||
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<? extends Annotation> annotationType) {
|
||||
return this.source.hasAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Origin getOrigin() {
|
||||
return this.source.getOrigin();
|
||||
|
|
|
|||
|
|
@ -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<C extends Container<?>> implements
|
|||
|
||||
private final Supplier<C> containerSupplier;
|
||||
|
||||
private final SslBundleSource sslBundleSource;
|
||||
|
||||
private final MergedAnnotations annotations;
|
||||
|
||||
ContainerConnectionSource(String beanNameSuffix, Origin origin, Class<C> containerType, String containerImageName,
|
||||
MergedAnnotation<ServiceConnection> annotation, Supplier<C> containerSupplier) {
|
||||
MergedAnnotation<ServiceConnection> annotation, Supplier<C> containerSupplier,
|
||||
SslBundleSource sslBundleSource, MergedAnnotations annotations) {
|
||||
this.beanNameSuffix = beanNameSuffix;
|
||||
this.origin = origin;
|
||||
this.containerType = containerType;
|
||||
|
|
@ -69,10 +76,13 @@ public final class ContainerConnectionSource<C extends Container<?>> 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<C> containerType, String containerImageName,
|
||||
ServiceConnection annotation, Supplier<C> containerSupplier) {
|
||||
ServiceConnection annotation, Supplier<C> containerSupplier, SslBundleSource sslBundleSource,
|
||||
MergedAnnotations annotations) {
|
||||
this.beanNameSuffix = beanNameSuffix;
|
||||
this.origin = origin;
|
||||
this.containerType = containerType;
|
||||
|
|
@ -80,6 +90,8 @@ public final class ContainerConnectionSource<C extends Container<?>> 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<C extends Container<?>> implements
|
|||
return this.connectionDetailsTypes;
|
||||
}
|
||||
|
||||
SslBundleSource getSslBundleSource() {
|
||||
return this.sslBundleSource;
|
||||
}
|
||||
|
||||
boolean hasAnnotation(Class<? extends Annotation> annotationType) {
|
||||
if (this.annotations == null) {
|
||||
return false;
|
||||
}
|
||||
return this.annotations.isPresent(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "@ServiceConnection source for %s".formatted(this.origin);
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -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<ServiceConnection> getAnnotations(ConfigurableListableBeanFactory beanFactory, String beanName,
|
||||
BeanDefinition beanDefinition) {
|
||||
Set<ServiceConnection> annotations = new LinkedHashSet<>(
|
||||
beanFactory.findAllAnnotationsOnBean(beanName, ServiceConnection.class, false));
|
||||
if (beanDefinition instanceof TestcontainerBeanDefinition testcontainerBeanDefinition) {
|
||||
testcontainerBeanDefinition.getAnnotations()
|
||||
.stream(ServiceConnection.class)
|
||||
private Set<ServiceConnection> getServiceConnections(ConfigurableListableBeanFactory beanFactory, String beanName,
|
||||
MergedAnnotations annotations) {
|
||||
Set<ServiceConnection> 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 <C extends Container<?>> ContainerConnectionSource<C> createSource(
|
||||
ConfigurableListableBeanFactory beanFactory, String beanName, BeanDefinition beanDefinition,
|
||||
ServiceConnection annotation) {
|
||||
MergedAnnotations annotations, ServiceConnection serviceConnection) {
|
||||
Origin origin = new BeanOrigin(beanName, beanDefinition);
|
||||
Class<C> containerType = (Class<C>) 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Class<?>> connectionDetailsTypes, Container<?> container) {
|
||||
private record CacheKey(String connectionName, Set<Class<?>> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <C extends Container<?>> ContainerConnectionSource<?> createSource(Field field,
|
||||
MergedAnnotation<ServiceConnection> annotation) {
|
||||
MergedAnnotations annotations, MergedAnnotation<ServiceConnection> 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) {
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
* <p>
|
||||
* 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 "";
|
||||
|
||||
}
|
||||
|
|
@ -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 extends Annotation> A getAnnotation(ListableBeanFactory beanFactory, String beanName,
|
||||
MergedAnnotations annotations, Class<A> annotationType) {
|
||||
Set<A> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<C> containerType, String containerImageName, MergedAnnotation<ServiceConnection> annotation,
|
||||
Supplier<C> containerSupplier) {
|
||||
return new ContainerConnectionSource<>(beanNameSuffix, origin, containerType, containerImageName, annotation,
|
||||
containerSupplier);
|
||||
containerSupplier, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue