From 1f1b6fe8d036617357ce54e67715176e8585c4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Andr=C3=A9?= Date: Thu, 3 Sep 2026 12:27:04 -0300 Subject: [PATCH] feat: add mTLS configuration support to the Dapr SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matheus André --- .../io/dapr/testcontainers/Configuration.java | 25 ++- .../io/dapr/testcontainers/DaprContainer.java | 127 +++++++++++- .../DaprContainerConstants.java | 1 + .../DaprPlacementContainer.java | 87 +++++++- .../DaprSchedulerContainer.java | 87 +++++++- .../testcontainers/DaprSentryContainer.java | 194 ++++++++++++++++++ .../MtlsConfigurationSettings.java | 117 +++++++++++ .../testcontainers/MtlsTokenValidator.java | 66 ++++++ .../converter/ConfigurationYamlConverter.java | 36 ++++ .../io/dapr/testcontainers/DaprMtlsTest.java | 150 ++++++++++++++ .../DaprPlacementContainerTest.java | 82 ++++++++ .../DaprSchedulerContainerTest.java | 82 ++++++++ .../DaprSentryContainerTest.java | 87 ++++++++ .../ConfigurationYamlConverterTest.java | 155 ++++++++++++++ 14 files changed, 1292 insertions(+), 4 deletions(-) create mode 100644 testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSentryContainer.java create mode 100644 testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsConfigurationSettings.java create mode 100644 testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsTokenValidator.java create mode 100644 testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprMtlsTest.java create mode 100644 testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprPlacementContainerTest.java create mode 100644 testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSchedulerContainerTest.java create mode 100644 testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSentryContainerTest.java diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/Configuration.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/Configuration.java index 4a431400c4..66ab4c7d67 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/Configuration.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/Configuration.java @@ -20,6 +20,7 @@ public class Configuration { private final String name; private final TracingConfigurationSettings tracing; private final AppHttpPipeline appHttpPipeline; + private final MtlsConfigurationSettings mtls; // @TODO: add secrets https://github.com/dapr/java-sdk/issues/1280 // @TODO: add metrics https://github.com/dapr/java-sdk/issues/1281 @@ -27,7 +28,6 @@ public class Configuration { // @TODO: add middleware httpPipeline https://github.com/dapr/java-sdk/issues/1283 // @TODO: add nameResolution https://github.com/dapr/java-sdk/issues/1284 // @TODO: add disallow components https://github.com/dapr/java-sdk/issues/1285 - // @TODO: add mtls https://github.com/dapr/java-sdk/issues/1286 /** * Creates a new configuration. @@ -38,9 +38,28 @@ public class Configuration { * @param appHttpPipeline AppHttpPipeline middleware configuration. */ public Configuration(String name, TracingConfigurationSettings tracing, AppHttpPipeline appHttpPipeline) { + this(name, tracing, appHttpPipeline, null); + } + + /** + * Creates a new configuration. + * + * @param name Configuration name. + * @param tracing TracingConfigParameters tracing configuration + * parameters. + * @param appHttpPipeline AppHttpPipeline middleware configuration. + * @param mtls MtlsConfigurationSettings mTLS configuration. + */ + public Configuration( + String name, + TracingConfigurationSettings tracing, + AppHttpPipeline appHttpPipeline, + MtlsConfigurationSettings mtls + ) { this.name = name; this.tracing = tracing; this.appHttpPipeline = appHttpPipeline; + this.mtls = mtls; } public String getName() { @@ -54,4 +73,8 @@ public TracingConfigurationSettings getTracing() { public AppHttpPipeline getAppHttpPipeline() { return appHttpPipeline; } + + public MtlsConfigurationSettings getMtls() { + return mtls; + } } diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 823f6f61f7..afd24ed73a 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -43,6 +43,7 @@ import static io.dapr.testcontainers.DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG; import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; import static io.dapr.testcontainers.DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG; +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_SENTRY_IMAGE_TAG; public class DaprContainer extends GenericContainer { private static final Logger LOGGER = LoggerFactory.getLogger(DaprContainer.class); @@ -67,12 +68,15 @@ public class DaprContainer extends GenericContainer { private String appChannelAddress = "localhost"; private String placementService = "placement"; private String schedulerService = "scheduler"; + private String sentryService = "sentry"; private DockerImageName placementDockerImageName = DockerImageName.parse(DAPR_PLACEMENT_IMAGE_TAG); private DockerImageName schedulerDockerImageName = DockerImageName.parse(DAPR_SCHEDULER_IMAGE_TAG); + private DockerImageName sentryDockerImageName = DockerImageName.parse(DAPR_SENTRY_IMAGE_TAG); private Configuration configuration; private DaprPlacementContainer placementContainer; private DaprSchedulerContainer schedulerContainer; + private DaprSentryContainer sentryContainer; private String appName; private Integer appPort; private DaprProtocol appProtocol = DaprProtocol.HTTP; // default from docs @@ -82,6 +86,7 @@ public class DaprContainer extends GenericContainer { private Integer appHealthCheckThreshold = 3; //default from docs private boolean shouldReusePlacement; private boolean shouldReuseScheduler; + private boolean shouldReuseSentry; /** * Creates a new Dapr container. @@ -171,6 +176,11 @@ public DaprContainer withSchedulerService(String schedulerService) { return this; } + public DaprContainer withSentryService(String sentryService) { + this.sentryService = sentryService; + return this; + } + public DaprContainer withAppName(String appName) { this.appName = appName; return this; @@ -221,6 +231,21 @@ public DaprContainer withReusableScheduler(boolean shouldReuseScheduler) { return this; } + public DaprContainer withSentryImage(DockerImageName sentryDockerImageName) { + this.sentryDockerImageName = sentryDockerImageName; + return this; + } + + public DaprContainer withSentryImage(String sentryDockerImageName) { + this.sentryDockerImageName = DockerImageName.parse(sentryDockerImageName); + return this; + } + + public DaprContainer withReusableSentry(boolean shouldReuseSentry) { + this.shouldReuseSentry = shouldReuseSentry; + return this; + } + public DaprContainer withPlacementContainer(DaprPlacementContainer placementContainer) { this.placementContainer = placementContainer; return this; @@ -231,6 +256,29 @@ public DaprContainer withSchedulerContainer(DaprSchedulerContainer schedulerCont return this; } + /** + * Sets the Sentry container used as certificate authority when mTLS is enabled in the {@link Configuration}. + * When not set, a Sentry container is created automatically with the same configuration. + * @param sentryContainer Sentry container. + * @return this container. + */ + public DaprContainer withSentryContainer(DaprSentryContainer sentryContainer) { + this.sentryContainer = sentryContainer; + return this; + } + + /** + * Returns true when the {@link Configuration} enables mTLS. In that case a Sentry container is used as + * certificate authority, the placement and scheduler services are started with TLS enabled, and daprd is + * started with {@code --enable-mtls}. + * @return whether mTLS is enabled. + */ + public boolean isMtlsEnabled() { + return configuration != null + && configuration.getMtls() != null + && Boolean.TRUE.equals(configuration.getMtls().getEnabled()); + } + public DaprContainer withComponent(Component component) { components.add(component); return this; @@ -292,11 +340,50 @@ protected void configure() { withNetwork(Network.newNetwork()); } + boolean mtlsEnabled = isMtlsEnabled(); + String sentryAddress = null; + String trustDomain = null; + String trustAnchors = null; + + if (mtlsEnabled) { + MtlsConfigurationSettings mtls = configuration.getMtls(); + trustDomain = mtls.getControlPlaneTrustDomain() != null + ? mtls.getControlPlaneTrustDomain() + : DaprSentryContainer.DEFAULT_TRUST_DOMAIN; + + if (this.sentryContainer == null) { + this.sentryContainer = new DaprSentryContainer(this.sentryDockerImageName) + .withNetwork(getNetwork()) + .withNetworkAliases(sentryService) + .withConfiguration(configuration) + .withTrustDomain(trustDomain) + .withDaprLogLevel(daprLogLevel) + .withReuse(this.shouldReuseSentry); + } + + // Sentry must be running before the other services start, as they need the trust anchors it issues. + this.sentryContainer.start(); + + sentryAddress = mtls.getSentryAddress() != null + ? mtls.getSentryAddress() + : sentryService + ":" + this.sentryContainer.getPort(); + trustAnchors = this.sentryContainer.getTrustAnchors(); + } + if (this.placementContainer == null) { this.placementContainer = new DaprPlacementContainer(this.placementDockerImageName) .withNetwork(getNetwork()) .withNetworkAliases(placementService) .withReuse(this.shouldReusePlacement); + + if (mtlsEnabled) { + this.placementContainer + .withTlsEnabled(true) + .withSentryAddress(sentryAddress) + .withTrustDomain(trustDomain) + .withTrustAnchors(trustAnchors); + } + this.placementContainer.start(); } @@ -305,6 +392,15 @@ protected void configure() { .withNetwork(getNetwork()) .withNetworkAliases(schedulerService) .withReuse(this.shouldReuseScheduler); + + if (mtlsEnabled) { + this.schedulerContainer + .withTlsEnabled(true) + .withSentryAddress(sentryAddress) + .withTrustDomain(trustDomain) + .withTrustAnchors(trustAnchors); + } + this.schedulerContainer.start(); } @@ -354,6 +450,15 @@ protected void configure() { cmds.add("/dapr-resources/" + configuration.getName() + ".yaml"); } + if (mtlsEnabled) { + cmds.add("--enable-mtls"); + cmds.add("--sentry-address"); + cmds.add(sentryAddress); + cmds.add("--control-plane-trust-domain"); + cmds.add(trustDomain); + withEnv("DAPR_TRUST_ANCHORS", trustAnchors); + } + cmds.add("--log-level"); cmds.add(daprLogLevel.toString()); cmds.add("--resources-path"); @@ -410,7 +515,11 @@ protected void configure() { withCopyToContainer(Transferable.of(endpointYaml), "/dapr-resources/" + endpoint.getName() + ".yaml"); } - dependsOn(placementContainer, schedulerContainer); + if (sentryContainer != null) { + dependsOn(placementContainer, schedulerContainer, sentryContainer); + } else { + dependsOn(placementContainer, schedulerContainer); + } } public String getAppName() { @@ -449,6 +558,18 @@ public String getPlacementService() { return placementService; } + public String getSchedulerService() { + return schedulerService; + } + + public String getSentryService() { + return sentryService; + } + + public DaprSentryContainer getSentryContainer() { + return sentryContainer; + } + public static DockerImageName getDefaultImageName() { return DEFAULT_IMAGE_NAME; } @@ -461,6 +582,10 @@ public DockerImageName getSchedulerDockerImageName() { return schedulerDockerImageName; } + public DockerImageName getSentryDockerImageName() { + return sentryDockerImageName; + } + // Required by spotbugs plugin @Override public boolean equals(Object o) { diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainerConstants.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainerConstants.java index 1d36b7c2c7..a46ae8ceeb 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainerConstants.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainerConstants.java @@ -19,5 +19,6 @@ public interface DaprContainerConstants { String DAPR_RUNTIME_IMAGE_TAG = "daprio/daprd:" + DAPR_VERSION; String DAPR_PLACEMENT_IMAGE_TAG = "daprio/placement:" + DAPR_VERSION; String DAPR_SCHEDULER_IMAGE_TAG = "daprio/scheduler:" + DAPR_VERSION; + String DAPR_SENTRY_IMAGE_TAG = "daprio/sentry:" + DAPR_VERSION; String DAPR_WORKFLOWS_DASHBOARD = "ghcr.io/diagridio/diagrid-dashboard:" + DAPR_WORKFLOWS_DASHBOARD_VERSION; } diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprPlacementContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprPlacementContainer.java index 1144f3c080..49a204b2d3 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprPlacementContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprPlacementContainer.java @@ -14,15 +14,24 @@ package io.dapr.testcontainers; import org.testcontainers.containers.GenericContainer; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; +import java.util.ArrayList; +import java.util.List; + /** * Test container for Dapr placement service. */ public class DaprPlacementContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("daprio/placement"); + private static final String TRUST_ANCHORS_FILE = "/var/run/secrets/dapr.io/tls/ca.crt"; private int placementPort = 50005; + private boolean tlsEnabled; + private String sentryAddress; + private String trustDomain; + private String trustAnchors; /** * Creates a new Dapr placement container. @@ -46,7 +55,41 @@ public DaprPlacementContainer(String image) { @Override protected void configure() { super.configure(); - withCommand("./placement", "-port", Integer.toString(placementPort)); + List cmds = new ArrayList<>(); + cmds.add("./placement"); + cmds.add("-port"); + cmds.add(Integer.toString(placementPort)); + cmds.addAll(tlsCommandArguments()); + + withCommand(cmds.toArray(new String[]{})); + } + + private List tlsCommandArguments() { + List cmds = new ArrayList<>(); + + if (!tlsEnabled) { + return cmds; + } + + cmds.add("--tls-enabled"); + + if (sentryAddress != null) { + cmds.add("--sentry-address"); + cmds.add(sentryAddress); + } + + if (trustDomain != null) { + cmds.add("--trust-domain"); + cmds.add(trustDomain); + } + + if (trustAnchors != null) { + withCopyToContainer(Transferable.of(trustAnchors), TRUST_ANCHORS_FILE); + cmds.add("--trust-anchors-file"); + cmds.add(TRUST_ANCHORS_FILE); + } + + return cmds; } public static DockerImageName getDefaultImageName() { @@ -62,6 +105,48 @@ public int getPort() { return placementPort; } + /** + * Enables TLS on the placement gRPC server. Requires a Sentry address and the trust anchors issued by Sentry. + * @param tlsEnabled whether TLS is enabled. + * @return this container. + */ + public DaprPlacementContainer withTlsEnabled(boolean tlsEnabled) { + this.tlsEnabled = tlsEnabled; + return this; + } + + public DaprPlacementContainer withSentryAddress(String sentryAddress) { + this.sentryAddress = sentryAddress; + return this; + } + + public DaprPlacementContainer withTrustDomain(String trustDomain) { + this.trustDomain = trustDomain; + return this; + } + + /** + * Sets the PEM encoded trust anchors (root CA certificate) issued by Sentry. + * @param trustAnchors PEM encoded trust anchors. + * @return this container. + */ + public DaprPlacementContainer withTrustAnchors(String trustAnchors) { + this.trustAnchors = trustAnchors; + return this; + } + + public boolean isTlsEnabled() { + return tlsEnabled; + } + + public String getSentryAddress() { + return sentryAddress; + } + + public String getTrustDomain() { + return trustDomain; + } + // Required by spotbugs plugin @Override public boolean equals(Object o) { diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSchedulerContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSchedulerContainer.java index 96f530adc7..d2b65f91b4 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSchedulerContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSchedulerContainer.java @@ -20,6 +20,8 @@ import org.testcontainers.utility.MountableFile; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * Test container for Dapr scheduler service. @@ -27,7 +29,12 @@ public class DaprSchedulerContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("daprio/scheduler"); + private static final String TRUST_ANCHORS_FILE = "/var/run/secrets/dapr.io/tls/ca.crt"; private int schedulerPort = 51005; + private boolean tlsEnabled; + private String sentryAddress; + private String trustDomain; + private String trustAnchors; /** * Creates a new Dapr scheduler container. @@ -53,7 +60,43 @@ protected void configure() { withCopyToContainer(Transferable.of("", 0777), "./default-dapr-scheduler-server-0/dapr-0.1/"); withCopyToContainer(Transferable.of("", 0777), "./dapr-scheduler-existing-cluster/"); - withCommand("./scheduler", "--port", Integer.toString(schedulerPort), "--etcd-data-dir", "."); + List cmds = new ArrayList<>(); + cmds.add("./scheduler"); + cmds.add("--port"); + cmds.add(Integer.toString(schedulerPort)); + cmds.add("--etcd-data-dir"); + cmds.add("."); + cmds.addAll(tlsCommandArguments()); + + withCommand(cmds.toArray(new String[]{})); + } + + private List tlsCommandArguments() { + List cmds = new ArrayList<>(); + + if (!tlsEnabled) { + return cmds; + } + + cmds.add("--tls-enabled"); + + if (sentryAddress != null) { + cmds.add("--sentry-address"); + cmds.add(sentryAddress); + } + + if (trustDomain != null) { + cmds.add("--trust-domain"); + cmds.add(trustDomain); + } + + if (trustAnchors != null) { + withCopyToContainer(Transferable.of(trustAnchors), TRUST_ANCHORS_FILE); + cmds.add("--trust-anchors-file"); + cmds.add(TRUST_ANCHORS_FILE); + } + + return cmds; } public static DockerImageName getDefaultImageName() { @@ -69,6 +112,48 @@ public int getPort() { return schedulerPort; } + /** + * Enables TLS on the scheduler gRPC server. Requires a Sentry address and the trust anchors issued by Sentry. + * @param tlsEnabled whether TLS is enabled. + * @return this container. + */ + public DaprSchedulerContainer withTlsEnabled(boolean tlsEnabled) { + this.tlsEnabled = tlsEnabled; + return this; + } + + public DaprSchedulerContainer withSentryAddress(String sentryAddress) { + this.sentryAddress = sentryAddress; + return this; + } + + public DaprSchedulerContainer withTrustDomain(String trustDomain) { + this.trustDomain = trustDomain; + return this; + } + + /** + * Sets the PEM encoded trust anchors (root CA certificate) issued by Sentry. + * @param trustAnchors PEM encoded trust anchors. + * @return this container. + */ + public DaprSchedulerContainer withTrustAnchors(String trustAnchors) { + this.trustAnchors = trustAnchors; + return this; + } + + public boolean isTlsEnabled() { + return tlsEnabled; + } + + public String getSentryAddress() { + return sentryAddress; + } + + public String getTrustDomain() { + return trustDomain; + } + // Required by spotbugs plugin @Override public boolean equals(Object o) { diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSentryContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSentryContainer.java new file mode 100644 index 0000000000..628975753a --- /dev/null +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprSentryContainer.java @@ -0,0 +1,194 @@ +/* + * Copyright 2026 The Dapr 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 + * http://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 io.dapr.testcontainers; + +import io.dapr.testcontainers.converter.ConfigurationYamlConverter; +import io.dapr.testcontainers.converter.YamlConverter; +import io.dapr.testcontainers.converter.YamlMapperFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Dapr Sentry container. Sentry is the certificate authority of the Dapr control plane and is required to + * enable mTLS. The mTLS settings are read from the {@code spec.mtls} section of the {@link Configuration} + * passed to this container. + */ +public class DaprSentryContainer extends GenericContainer { + + public static final int DEFAULT_SENTRY_PORT = 50001; + public static final int DEFAULT_HEALTHZ_PORT = 8080; + public static final String DEFAULT_TRUST_DOMAIN = "localhost"; + public static final String DEFAULT_ISSUER_CREDENTIALS_PATH = "/var/run/secrets/dapr.io/credentials"; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("daprio/sentry"); + private static final String CONFIGURATION_PATH = "/dapr-resources/"; + private static final String TRUST_ANCHORS_FILENAME = "ca.crt"; + private static final YamlConverter CONFIGURATION_CONVERTER = + new ConfigurationYamlConverter(YamlMapperFactory.create()); + + private int sentryPort = DEFAULT_SENTRY_PORT; + private int healthzPort = DEFAULT_HEALTHZ_PORT; + private String trustDomain = DEFAULT_TRUST_DOMAIN; + private String issuerCredentialsPath = DEFAULT_ISSUER_CREDENTIALS_PATH; + private DaprLogLevel daprLogLevel = DaprLogLevel.INFO; + private Configuration configuration; + + /** + * Creates a new Dapr sentry container. + * @param dockerImageName Docker image name. + */ + public DaprSentryContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + } + + /** + * Creates a new Dapr sentry container. + * @param image Docker image name. + */ + public DaprSentryContainer(String image) { + this(DockerImageName.parse(image)); + } + + @Override + protected void configure() { + super.configure(); + + withExposedPorts(sentryPort, healthzPort); + + // Sentry generates a self-signed CA on first start when no issuer credentials are found. It runs as a + // non-root user, so the credentials directory must exist and be writable. After generating the + // credentials it reloads itself, so we wait until it reports the credentials were loaded from the store. + withCopyToContainer(Transferable.of("", 0777), issuerCredentialsPath + "/"); + setWaitStrategy(new WaitAllStrategy() + .withStrategy(Wait.forLogMessage(".*Root and issuer certs found: using credentials from store.*", 1)) + .withStrategy(Wait.forHttp("/healthz").forPort(healthzPort))); + + List cmds = new ArrayList<>(); + cmds.add("./sentry"); + cmds.add("--port"); + cmds.add(Integer.toString(sentryPort)); + cmds.add("--healthz-port"); + cmds.add(Integer.toString(healthzPort)); + cmds.add("--trust-domain"); + cmds.add(trustDomain); + cmds.add("--issuer-credentials"); + cmds.add(issuerCredentialsPath); + cmds.add("--log-level"); + cmds.add(daprLogLevel.toString()); + + if (configuration != null) { + String configurationYaml = CONFIGURATION_CONVERTER.convert(configuration); + String configurationFile = CONFIGURATION_PATH + configuration.getName() + ".yaml"; + + withCopyToContainer(Transferable.of(configurationYaml), configurationFile); + cmds.add("--config"); + cmds.add(configurationFile); + } + + withCommand(cmds.toArray(new String[]{})); + } + + public static DockerImageName getDefaultImageName() { + return DEFAULT_IMAGE_NAME; + } + + /** + * Sets the Dapr {@link Configuration} used by sentry. Its {@code spec.mtls} section configures the + * certificate authority (workload certificate TTL, allowed clock skew, token validators, etc.). + * @param configuration Dapr configuration. + * @return this container. + */ + public DaprSentryContainer withConfiguration(Configuration configuration) { + this.configuration = configuration; + return this; + } + + public DaprSentryContainer withPort(Integer port) { + this.sentryPort = port; + return this; + } + + public DaprSentryContainer withHealthzPort(Integer healthzPort) { + this.healthzPort = healthzPort; + return this; + } + + public DaprSentryContainer withTrustDomain(String trustDomain) { + this.trustDomain = trustDomain; + return this; + } + + public DaprSentryContainer withIssuerCredentialsPath(String issuerCredentialsPath) { + this.issuerCredentialsPath = issuerCredentialsPath; + return this; + } + + public DaprSentryContainer withDaprLogLevel(DaprLogLevel daprLogLevel) { + this.daprLogLevel = daprLogLevel; + return this; + } + + public Configuration getConfiguration() { + return configuration; + } + + public int getPort() { + return sentryPort; + } + + public int getHealthzPort() { + return healthzPort; + } + + public String getTrustDomain() { + return trustDomain; + } + + public String getIssuerCredentialsPath() { + return issuerCredentialsPath; + } + + /** + * Returns the PEM encoded root CA certificate (trust anchors) issued by this sentry instance. + * The container must be running. + * @return PEM encoded trust anchors. + */ + public String getTrustAnchors() { + if (!isRunning()) { + throw new IllegalStateException("Sentry container must be running to read the trust anchors"); + } + + return copyFileFromContainer(issuerCredentialsPath + "/" + TRUST_ANCHORS_FILENAME, + inputStream -> new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); + } + + // Required by spotbugs plugin + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsConfigurationSettings.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsConfigurationSettings.java new file mode 100644 index 0000000000..b7e5e0aec3 --- /dev/null +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsConfigurationSettings.java @@ -0,0 +1,117 @@ +/* + * Copyright 2026 The Dapr 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 + * http://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 io.dapr.testcontainers; + +import java.util.Collections; +import java.util.List; + +/** + * Configuration settings for mTLS (mutual TLS). + * + * @see + * Dapr mTLS configuration + */ +public class MtlsConfigurationSettings implements ConfigurationSettings { + private final Boolean enabled; + private final String workloadCertTtl; + private final String allowedClockSkew; + private final String sentryAddress; + private final String controlPlaneTrustDomain; + private final List tokenValidators; + + /** + * Creates a new mTLS configuration. + * + * @param enabled if true, enables mTLS for communication between services and apps. + * @param workloadCertTtl how long a TLS certificate issued by Dapr is valid for (Go duration, e.g. "24h"). + * @param allowedClockSkew allowed tolerance when checking certificate expiration (Go duration, e.g. "15m"). + */ + public MtlsConfigurationSettings(Boolean enabled, String workloadCertTtl, String allowedClockSkew) { + this(enabled, workloadCertTtl, allowedClockSkew, null, null, null); + } + + /** + * Creates a new mTLS configuration. + * + * @param enabled if true, enables mTLS for communication between services and apps. + * @param workloadCertTtl how long a TLS certificate issued by Dapr is valid for + * (Go duration, e.g. "24h"). + * @param allowedClockSkew allowed tolerance when checking certificate expiration + * (Go duration, e.g. "15m"). + * @param sentryAddress hostname port address for connecting to the Sentry server. + * @param controlPlaneTrustDomain trust domain for the control plane. + */ + public MtlsConfigurationSettings( + Boolean enabled, + String workloadCertTtl, + String allowedClockSkew, + String sentryAddress, + String controlPlaneTrustDomain + ) { + this(enabled, workloadCertTtl, allowedClockSkew, sentryAddress, controlPlaneTrustDomain, null); + } + + /** + * Creates a new mTLS configuration. + * + * @param enabled if true, enables mTLS for communication between services and apps. + * @param workloadCertTtl how long a TLS certificate issued by Dapr is valid for + * (Go duration, e.g. "24h"). + * @param allowedClockSkew allowed tolerance when checking certificate expiration + * (Go duration, e.g. "15m"). + * @param sentryAddress hostname port address for connecting to the Sentry server. + * @param controlPlaneTrustDomain trust domain for the control plane. + * @param tokenValidators additional Sentry token validators used to authenticate + * certificate requests. + */ + public MtlsConfigurationSettings( + Boolean enabled, + String workloadCertTtl, + String allowedClockSkew, + String sentryAddress, + String controlPlaneTrustDomain, + List tokenValidators + ) { + this.enabled = enabled; + this.workloadCertTtl = workloadCertTtl; + this.allowedClockSkew = allowedClockSkew; + this.sentryAddress = sentryAddress; + this.controlPlaneTrustDomain = controlPlaneTrustDomain; + this.tokenValidators = tokenValidators != null ? Collections.unmodifiableList(tokenValidators) : null; + } + + public Boolean getEnabled() { + return enabled; + } + + public String getWorkloadCertTtl() { + return workloadCertTtl; + } + + public String getAllowedClockSkew() { + return allowedClockSkew; + } + + public String getSentryAddress() { + return sentryAddress; + } + + public String getControlPlaneTrustDomain() { + return controlPlaneTrustDomain; + } + + public List getTokenValidators() { + return tokenValidators; + } +} diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsTokenValidator.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsTokenValidator.java new file mode 100644 index 0000000000..d4b2d45f7e --- /dev/null +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/MtlsTokenValidator.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 The Dapr 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 + * http://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 io.dapr.testcontainers; + +import java.util.Collections; +import java.util.Map; + +/** + * Additional Sentry token validator used to authenticate certificate requests (mTLS {@code tokenValidators} entry). + * + *

Only the {@code jwks} validator can be configured manually. The built-in {@code kubernetes} and + * {@code insecure} validators are enabled automatically by Sentry depending on the runtime mode and are + * rejected when listed in {@code tokenValidators}. In self-hosted mode, configuring a validator disables the + * built-in {@code insecure} validator. + */ +public class MtlsTokenValidator { + /** + * Name of the JWKS validator, the only validator that can be configured manually. + */ + public static final String JWKS = "jwks"; + + private final String name; + private final Map options; + + /** + * Creates a token validator. + * + * @param name validator name. Only {@link #JWKS} is accepted by Sentry. + * @param options validator options, if any (e.g. {@code source}, {@code minRefreshInterval}, + * {@code requestTimeout}, {@code caCertificate} for the JWKS validator). + */ + public MtlsTokenValidator(String name, Map options) { + this.name = name; + this.options = options != null ? Collections.unmodifiableMap(options) : null; + } + + /** + * Creates a JWKS token validator. + * + * @param options JWKS validator options (e.g. {@code source}, {@code minRefreshInterval}, + * {@code requestTimeout}, {@code caCertificate}). + * @return a JWKS token validator. + */ + public static MtlsTokenValidator jwks(Map options) { + return new MtlsTokenValidator(JWKS, options); + } + + public String getName() { + return name; + } + + public Map getOptions() { + return options; + } +} diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/ConfigurationYamlConverter.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/ConfigurationYamlConverter.java index 005f0b50f9..7cd502c256 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/ConfigurationYamlConverter.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/converter/ConfigurationYamlConverter.java @@ -16,11 +16,14 @@ import io.dapr.testcontainers.AppHttpPipeline; import io.dapr.testcontainers.Configuration; import io.dapr.testcontainers.ListEntry; +import io.dapr.testcontainers.MtlsConfigurationSettings; +import io.dapr.testcontainers.MtlsTokenValidator; import io.dapr.testcontainers.OtelTracingConfigurationSettings; import io.dapr.testcontainers.TracingConfigurationSettings; import io.dapr.testcontainers.ZipkinTracingConfigurationSettings; import org.yaml.snakeyaml.Yaml; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -87,8 +90,41 @@ public String convert(Configuration configuration) { } + MtlsConfigurationSettings mtls = configuration.getMtls(); + if (mtls != null) { + Map mtlsMap = new LinkedHashMap<>(); + + putIfNotNull(mtlsMap, "enabled", mtls.getEnabled()); + putIfNotNull(mtlsMap, "workloadCertTTL", mtls.getWorkloadCertTtl()); + putIfNotNull(mtlsMap, "allowedClockSkew", mtls.getAllowedClockSkew()); + putIfNotNull(mtlsMap, "sentryAddress", mtls.getSentryAddress()); + putIfNotNull(mtlsMap, "controlPlaneTrustDomain", mtls.getControlPlaneTrustDomain()); + + List tokenValidators = mtls.getTokenValidators(); + if (tokenValidators != null && !tokenValidators.isEmpty()) { + List> tokenValidatorsList = new ArrayList<>(); + + for (MtlsTokenValidator tokenValidator : tokenValidators) { + Map tokenValidatorMap = new LinkedHashMap<>(); + tokenValidatorMap.put("name", tokenValidator.getName()); + putIfNotNull(tokenValidatorMap, "options", tokenValidator.getOptions()); + tokenValidatorsList.add(tokenValidatorMap); + } + + mtlsMap.put("tokenValidators", tokenValidatorsList); + } + + configurationSpec.put("mtls", mtlsMap); + } + configurationProps.put("spec", configurationSpec); return mapper.dumpAsMap(configurationProps); } + + private static void putIfNotNull(Map map, String key, Object value) { + if (value != null) { + map.put(key, value); + } + } } diff --git a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprMtlsTest.java b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprMtlsTest.java new file mode 100644 index 0000000000..31193256ff --- /dev/null +++ b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprMtlsTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2026 The Dapr 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 + * http://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 io.dapr.testcontainers; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Network; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DaprMtlsTest { + + @Test + public void mtlsDisabledByDefaultTest() { + try (DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG).withAppName("dapr-app")) { + assertFalse(dapr.isMtlsEnabled()); + } + + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings(false, "24h", "15m"); + try (DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("dapr-app") + .withConfiguration(new Configuration("my-config", null, null, mtls))) { + assertFalse(dapr.isMtlsEnabled()); + dapr.configure(); + + assertNull(dapr.getSentryContainer()); + assertFalse(List.of(dapr.getCommandParts()).contains("--enable-mtls")); + } + } + + @Test + public void sentrySettingsTest() { + try (DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("dapr-app") + .withConfiguration(new Configuration("my-config", null, null))) { + assertFalse(dapr.isMtlsEnabled()); + assertEquals("sentry", dapr.getSentryService()); + assertEquals("scheduler", dapr.getSchedulerService()); + assertEquals(DaprContainerConstants.DAPR_SENTRY_IMAGE_TAG, + dapr.getSentryDockerImageName().asCanonicalNameString()); + } + + try (DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("dapr-app") + .withSentryService("my-sentry") + .withSentryImage("daprio/sentry:" + DAPR_VERSION) + .withReusableSentry(true)) { + assertEquals("my-sentry", dapr.getSentryService()); + assertEquals("daprio/sentry:" + DAPR_VERSION, dapr.getSentryDockerImageName().asCanonicalNameString()); + } + + try (DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("dapr-app") + .withSentryImage(DockerImageName.parse("custom/sentry:" + DAPR_VERSION) + .asCompatibleSubstituteFor("daprio/sentry:" + DAPR_VERSION))) { + assertEquals("custom/sentry:" + DAPR_VERSION, dapr.getSentryDockerImageName().asCanonicalNameString()); + } + } + + @Test + public void mtlsEnabledStartsSentryAndSecuresControlPlaneTest() throws IOException, InterruptedException { + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings(true, "24h", "15m"); + Configuration configuration = new Configuration("daprsystem", null, null, mtls); + + try (DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("dapr-mtls-app") + .withConfiguration(configuration) + .withDaprLogLevel(DaprLogLevel.DEBUG)) { + assertTrue(dapr.isMtlsEnabled()); + + dapr.start(); + + DaprSentryContainer sentry = dapr.getSentryContainer(); + assertNotNull(sentry); + assertTrue(sentry.isRunning()); + assertEquals(configuration, sentry.getConfiguration()); + + String trustAnchors = sentry.getTrustAnchors(); + assertTrue(trustAnchors.startsWith("-----BEGIN CERTIFICATE-----")); + assertEquals(trustAnchors, dapr.getEnvMap().get("DAPR_TRUST_ANCHORS")); + + List command = List.of(dapr.getCommandParts()); + assertTrue(command.contains("--enable-mtls")); + assertTrue(command.containsAll(List.of("--sentry-address", "sentry:50001"))); + assertTrue(command.containsAll(List.of("--control-plane-trust-domain", "localhost"))); + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(dapr.getHttpEndpoint() + "/v1.0/metadata")) + .GET() + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + assertTrue(response.body().contains("dapr-mtls-app")); + } + } + + @Test + public void mtlsWithCustomSentryContainerTest() { + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings( + true, "24h", "15m", "my-sentry:50001", "cluster.local"); + Configuration configuration = new Configuration("daprsystem", null, null, mtls); + + try (Network network = Network.newNetwork(); + DaprSentryContainer sentry = new DaprSentryContainer(DaprContainerConstants.DAPR_SENTRY_IMAGE_TAG) + .withNetwork(network) + .withNetworkAliases("my-sentry") + .withConfiguration(configuration) + .withTrustDomain("cluster.local"); + DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withNetwork(network) + .withAppName("dapr-mtls-app") + .withConfiguration(configuration) + .withSentryContainer(sentry)) { + dapr.start(); + + assertTrue(sentry.isRunning()); + assertEquals(sentry, dapr.getSentryContainer()); + + List command = List.of(dapr.getCommandParts()); + assertTrue(command.containsAll(List.of("--sentry-address", "my-sentry:50001"))); + assertTrue(command.containsAll(List.of("--control-plane-trust-domain", "cluster.local"))); + } + } +} diff --git a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprPlacementContainerTest.java b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprPlacementContainerTest.java new file mode 100644 index 0000000000..0bf3061388 --- /dev/null +++ b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprPlacementContainerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2026 The Dapr 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 + * http://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 io.dapr.testcontainers; + +import org.junit.jupiter.api.Test; + +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +public class DaprPlacementContainerTest { + + private static final String TRUST_ANCHORS = "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n"; + + @Test + public void tlsDisabledByDefaultTest() { + try (DaprPlacementContainer container = new DaprPlacementContainer(DAPR_PLACEMENT_IMAGE_TAG)) { + container.configure(); + + assertFalse(container.isTlsEnabled()); + assertNull(container.getSentryAddress()); + assertNull(container.getTrustDomain()); + assertEquals(50005, container.getPort()); + + List command = List.of(container.getCommandParts()); + assertEquals("./placement", command.get(0)); + assertFalse(command.contains("--tls-enabled")); + assertFalse(command.contains("--sentry-address")); + assertFalse(command.contains("--trust-anchors-file")); + } + } + + @Test + public void tlsEnabledTest() { + try (DaprPlacementContainer container = new DaprPlacementContainer(DAPR_PLACEMENT_IMAGE_TAG) + .withTlsEnabled(true) + .withSentryAddress("sentry:50001") + .withTrustDomain("cluster.local") + .withTrustAnchors(TRUST_ANCHORS)) { + container.configure(); + + assertTrue(container.isTlsEnabled()); + assertEquals("sentry:50001", container.getSentryAddress()); + assertEquals("cluster.local", container.getTrustDomain()); + + List command = List.of(container.getCommandParts()); + assertTrue(command.contains("--tls-enabled")); + assertTrue(command.containsAll(List.of("--sentry-address", "sentry:50001"))); + assertTrue(command.containsAll(List.of("--trust-domain", "cluster.local"))); + assertTrue(command.containsAll(List.of("--trust-anchors-file", "/var/run/secrets/dapr.io/tls/ca.crt"))); + } + } + + @Test + public void tlsEnabledWithoutOptionalSettingsTest() { + try (DaprPlacementContainer container = new DaprPlacementContainer(DaprPlacementContainer.getDefaultImageName().withTag(DaprContainerConstants.DAPR_VERSION)) + .withTlsEnabled(true)) { + container.configure(); + + List command = List.of(container.getCommandParts()); + assertTrue(command.contains("--tls-enabled")); + assertFalse(command.contains("--sentry-address")); + assertFalse(command.contains("--trust-domain")); + assertFalse(command.contains("--trust-anchors-file")); + } + } +} diff --git a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSchedulerContainerTest.java b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSchedulerContainerTest.java new file mode 100644 index 0000000000..5e7cc30e77 --- /dev/null +++ b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSchedulerContainerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2026 The Dapr 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 + * http://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 io.dapr.testcontainers; + +import org.junit.jupiter.api.Test; + +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +public class DaprSchedulerContainerTest { + + private static final String TRUST_ANCHORS = "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n"; + + @Test + public void tlsDisabledByDefaultTest() { + try (DaprSchedulerContainer container = new DaprSchedulerContainer(DAPR_SCHEDULER_IMAGE_TAG)) { + container.configure(); + + assertFalse(container.isTlsEnabled()); + assertNull(container.getSentryAddress()); + assertNull(container.getTrustDomain()); + assertEquals(51005, container.getPort()); + + List command = List.of(container.getCommandParts()); + assertEquals("./scheduler", command.get(0)); + assertFalse(command.contains("--tls-enabled")); + assertFalse(command.contains("--sentry-address")); + assertFalse(command.contains("--trust-anchors-file")); + } + } + + @Test + public void tlsEnabledTest() { + try (DaprSchedulerContainer container = new DaprSchedulerContainer(DAPR_SCHEDULER_IMAGE_TAG) + .withTlsEnabled(true) + .withSentryAddress("sentry:50001") + .withTrustDomain("cluster.local") + .withTrustAnchors(TRUST_ANCHORS)) { + container.configure(); + + assertTrue(container.isTlsEnabled()); + assertEquals("sentry:50001", container.getSentryAddress()); + assertEquals("cluster.local", container.getTrustDomain()); + + List command = List.of(container.getCommandParts()); + assertTrue(command.contains("--tls-enabled")); + assertTrue(command.containsAll(List.of("--sentry-address", "sentry:50001"))); + assertTrue(command.containsAll(List.of("--trust-domain", "cluster.local"))); + assertTrue(command.containsAll(List.of("--trust-anchors-file", "/var/run/secrets/dapr.io/tls/ca.crt"))); + } + } + + @Test + public void tlsEnabledWithoutOptionalSettingsTest() { + try (DaprSchedulerContainer container = new DaprSchedulerContainer(DaprSchedulerContainer.getDefaultImageName().withTag(DaprContainerConstants.DAPR_VERSION)) + .withTlsEnabled(true)) { + container.configure(); + + List command = List.of(container.getCommandParts()); + assertTrue(command.contains("--tls-enabled")); + assertFalse(command.contains("--sentry-address")); + assertFalse(command.contains("--trust-domain")); + assertFalse(command.contains("--trust-anchors-file")); + } + } +} diff --git a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSentryContainerTest.java b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSentryContainerTest.java new file mode 100644 index 0000000000..b43eb3dd3e --- /dev/null +++ b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/DaprSentryContainerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2026 The Dapr 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 + * http://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 io.dapr.testcontainers; + +import org.junit.jupiter.api.Test; + +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_SENTRY_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +public class DaprSentryContainerTest { + + @Test + public void sentryDefaultsTest() { + try (DaprSentryContainer sentry = new DaprSentryContainer(DAPR_SENTRY_IMAGE_TAG)) { + sentry.configure(); + + assertEquals(DaprSentryContainer.DEFAULT_SENTRY_PORT, sentry.getPort()); + assertEquals(DaprSentryContainer.DEFAULT_HEALTHZ_PORT, sentry.getHealthzPort()); + assertEquals(DaprSentryContainer.DEFAULT_TRUST_DOMAIN, sentry.getTrustDomain()); + assertEquals(DaprSentryContainer.DEFAULT_ISSUER_CREDENTIALS_PATH, sentry.getIssuerCredentialsPath()); + assertNull(sentry.getConfiguration()); + assertEquals("daprio/sentry", DaprSentryContainer.getDefaultImageName().getUnversionedPart()); + + List command = List.of(sentry.getCommandParts()); + assertEquals("./sentry", command.get(0)); + assertTrue(command.containsAll(List.of("--port", "50001", "--healthz-port", "8080", "--trust-domain", "localhost"))); + assertFalse(command.contains("--config")); + assertTrue(sentry.getExposedPorts().containsAll(List.of(50001, 8080))); + } + } + + @Test + public void sentryWithConfigurationTest() { + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings(true, "24h", "15m"); + Configuration configuration = new Configuration("daprsystem", null, null, mtls); + + try (DaprSentryContainer sentry = new DaprSentryContainer(DAPR_SENTRY_IMAGE_TAG) + .withConfiguration(configuration) + .withPort(50002) + .withHealthzPort(8081) + .withTrustDomain("cluster.local") + .withIssuerCredentialsPath("/tmp/dapr-credentials") + .withDaprLogLevel(DaprLogLevel.DEBUG)) { + sentry.configure(); + + assertNotNull(sentry.getConfiguration()); + assertEquals(50002, sentry.getPort()); + assertEquals(8081, sentry.getHealthzPort()); + assertEquals("cluster.local", sentry.getTrustDomain()); + assertEquals("/tmp/dapr-credentials", sentry.getIssuerCredentialsPath()); + + List command = List.of(sentry.getCommandParts()); + assertTrue(command.containsAll(List.of( + "--port", "50002", + "--healthz-port", "8081", + "--trust-domain", "cluster.local", + "--issuer-credentials", "/tmp/dapr-credentials", + "--log-level", "DEBUG", + "--config", "/dapr-resources/daprsystem.yaml"))); + } + } + + @Test + public void trustAnchorsRequireRunningContainerTest() { + try (DaprSentryContainer sentry = new DaprSentryContainer(DAPR_SENTRY_IMAGE_TAG)) { + assertThrows(IllegalStateException.class, sentry::getTrustAnchors); + } + } +} diff --git a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/ConfigurationYamlConverterTest.java b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/ConfigurationYamlConverterTest.java index fe798611b5..f8ca8fa303 100644 --- a/testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/ConfigurationYamlConverterTest.java +++ b/testcontainers-dapr/src/test/java/io/dapr/testcontainers/converter/ConfigurationYamlConverterTest.java @@ -17,6 +17,8 @@ import io.dapr.testcontainers.Configuration; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.ListEntry; +import io.dapr.testcontainers.MtlsConfigurationSettings; +import io.dapr.testcontainers.MtlsTokenValidator; import io.dapr.testcontainers.OtelTracingConfigurationSettings; import io.dapr.testcontainers.TracingConfigurationSettings; import org.junit.jupiter.api.Test; @@ -27,7 +29,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; class ConfigurationYamlConverterTest { private final Yaml MAPPER = YamlMapperFactory.create(); @@ -83,4 +87,155 @@ public void testConfigurationToYaml() { assertEquals(expectedConfigurationYaml, configurationYaml); } + + @Test + public void testConfigurationWithMtlsToYaml() { + Map jwksOptions = new LinkedHashMap<>(); + jwksOptions.put("minRefreshInterval", "2m"); + jwksOptions.put("requestTimeout", "1m"); + jwksOptions.put("source", "https://localhost:1234/"); + + List tokenValidators = new ArrayList<>(); + tokenValidators.add(MtlsTokenValidator.jwks(jwksOptions)); + + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings( + true, + "24h", + "15m", + "localhost:50001", + "cluster.local", + tokenValidators + ); + + DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("dapr-app") + .withAppPort(8081) + .withConfiguration(new Configuration("my-config", null, null, mtls)) + .withAppChannelAddress("host.testcontainers.internal"); + + Configuration configuration = dapr.getConfiguration(); + assertNotNull(configuration); + + String configurationYaml = converter.convert(configuration); + String expectedConfigurationYaml = + "apiVersion: dapr.io/v1alpha1\n" + + "kind: Configuration\n" + + "metadata:\n" + + " name: my-config\n" + + "spec:\n" + + " mtls:\n" + + " enabled: true\n" + + " workloadCertTTL: 24h\n" + + " allowedClockSkew: 15m\n" + + " sentryAddress: localhost:50001\n" + + " controlPlaneTrustDomain: cluster.local\n" + + " tokenValidators:\n" + + " - name: jwks\n" + + " options:\n" + + " minRefreshInterval: 2m\n" + + " requestTimeout: 1m\n" + + " source: https://localhost:1234/\n"; + + assertEquals(expectedConfigurationYaml, configurationYaml); + } + + @Test + public void testConfigurationWithMinimalMtlsToYaml() { + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings(true, "24h", "15m"); + + Configuration configuration = new Configuration("my-config", null, null, mtls); + + String configurationYaml = converter.convert(configuration); + String expectedConfigurationYaml = + "apiVersion: dapr.io/v1alpha1\n" + + "kind: Configuration\n" + + "metadata:\n" + + " name: my-config\n" + + "spec:\n" + + " mtls:\n" + + " enabled: true\n" + + " workloadCertTTL: 24h\n" + + " allowedClockSkew: 15m\n"; + + assertEquals(expectedConfigurationYaml, configurationYaml); + } + + @Test + public void testConfigurationWithMtlsWithoutTokenValidatorsToYaml() { + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings( + true, + "24h", + "15m", + "localhost:50001", + "cluster.local" + ); + + Configuration configuration = new Configuration("my-config", null, null, mtls); + + String configurationYaml = converter.convert(configuration); + String expectedConfigurationYaml = + "apiVersion: dapr.io/v1alpha1\n" + + "kind: Configuration\n" + + "metadata:\n" + + " name: my-config\n" + + "spec:\n" + + " mtls:\n" + + " enabled: true\n" + + " workloadCertTTL: 24h\n" + + " allowedClockSkew: 15m\n" + + " sentryAddress: localhost:50001\n" + + " controlPlaneTrustDomain: cluster.local\n"; + + assertEquals(expectedConfigurationYaml, configurationYaml); + } + + @Test + public void testConfigurationWithMtlsTokenValidatorWithoutOptionsToYaml() { + List tokenValidators = new ArrayList<>(); + tokenValidators.add(new MtlsTokenValidator(MtlsTokenValidator.JWKS, null)); + + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings(true, null, null, null, null, tokenValidators); + + Configuration configuration = new Configuration("my-config", null, null, mtls); + + String configurationYaml = converter.convert(configuration); + String expectedConfigurationYaml = + "apiVersion: dapr.io/v1alpha1\n" + + "kind: Configuration\n" + + "metadata:\n" + + " name: my-config\n" + + "spec:\n" + + " mtls:\n" + + " enabled: true\n" + + " tokenValidators:\n" + + " - name: jwks\n"; + + assertEquals(expectedConfigurationYaml, configurationYaml); + } + + @Test + public void testConfigurationWithEmptyMtlsTokenValidatorsToYaml() { + MtlsConfigurationSettings mtls = new MtlsConfigurationSettings( + false, + null, + null, + null, + null, + new ArrayList<>() + ); + + Configuration configuration = new Configuration("my-config", null, null, mtls); + + String configurationYaml = converter.convert(configuration); + String expectedConfigurationYaml = + "apiVersion: dapr.io/v1alpha1\n" + + "kind: Configuration\n" + + "metadata:\n" + + " name: my-config\n" + + "spec:\n" + + " mtls:\n" + + " enabled: false\n"; + + assertEquals(expectedConfigurationYaml, configurationYaml); + } }