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:
Moritz Halbritter
2025-02-11 10:15:58 +01:00
parent 528b7e9ad9
commit b62a0c1ae0
20 changed files with 776 additions and 53 deletions

View File

@@ -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();

View File

@@ -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);

View File

@@ -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 "";
}

View File

@@ -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 "";
}

View File

@@ -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 "";
}

View File

@@ -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 "";
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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) {

View File

@@ -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 "";
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}