Skip to content

Commit f0ee05b

Browse files
committed
feat: implement shared container feature for Dapr Dev Services with configuration options
1 parent 4f2bce0 commit f0ee05b

6 files changed

Lines changed: 258 additions & 12 deletions

File tree

deployment/src/main/java/io/quarkiverse/dapr/deployment/devservices/DaprDevServiceBuildTimeConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,13 @@ interface Dashboard {
5757
@WithDefault("true")
5858
Optional<Boolean> enabled();
5959

60+
/**
61+
* The value of the {@code quarkus-dev-service-dapr-dashboard} label attached to the started container.
62+
* <p>
63+
* This is used to discover and re-use an existing shared Dapr Dashboard container.
64+
*/
65+
@WithDefault("dapr-dashboard")
66+
String serviceName();
67+
6068
}
6169
}

deployment/src/main/java/io/quarkiverse/dapr/deployment/devservices/DashboardContainerStartable.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static io.quarkiverse.dapr.deployment.devservices.StateStoreContainerStartable.PGSQL_NETWORK_ALIAS;
44
import static io.quarkiverse.dapr.deployment.devservices.StateStoreContainerStartable.PGSQL_STATE_STORE;
5+
import static io.quarkus.devservices.common.ConfigureUtil.configureSharedServiceLabel;
56

67
import java.util.HashMap;
78
import java.util.Map;
@@ -13,17 +14,19 @@
1314
import io.dapr.testcontainers.DaprContainerConstants;
1415
import io.dapr.testcontainers.WorkflowDashboardContainer;
1516
import io.quarkus.deployment.builditem.Startable;
17+
import io.quarkus.runtime.LaunchMode;
1618

1719
public class DashboardContainerStartable extends WorkflowDashboardContainer implements Startable {
1820

1921
public static final int INTERNAL_DAPR_DASHBOARD_WORKFLOW_PORT = 8080;
2022

2123
private static final Map<String, String> POSTGRE_SQL_DETAILS = new HashMap<>();
2224

23-
public DashboardContainerStartable(Network network) {
25+
public DashboardContainerStartable(Network network, LaunchMode launchMode, String serviceName) {
2426
super(DockerImageName.parse(DaprContainerConstants.DAPR_WORKFLOWS_DASHBOARD));
2527
super.withExposedPorts(INTERNAL_DAPR_DASHBOARD_WORKFLOW_PORT)
2628
.withNetwork(network);
29+
configureSharedServiceLabel(this, launchMode, DevServicesDaprProcessor.DASHBOARD_WORKFLOW_LABEL, serviceName);
2730
}
2831

2932
@Override

deployment/src/main/java/io/quarkiverse/dapr/deployment/devservices/DevServicesDaprProcessor.java

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static io.quarkus.devservices.common.ContainerLocator.locateContainerWithLabels;
99

1010
import java.util.ArrayList;
11+
import java.util.Collections;
1112
import java.util.HashMap;
1213
import java.util.List;
1314
import java.util.Map;
@@ -37,10 +38,15 @@ public class DevServicesDaprProcessor {
3738

3839
private static final Logger LOGGER = LoggerFactory.getLogger(DevServicesDaprProcessor.class);
3940
static final String DEV_SERVICE_LABEL = "quarkus-dev-service-dapr";
41+
static final String DASHBOARD_WORKFLOW_LABEL = "quarkus-dev-service-dapr-dashboard";
42+
4043
private static final int DAPR_INTERNAL_HTTP_PORT = 3500;
4144
private static final int DAPR_INTERNAL_GRPC_PORT = 50001;
4245
private static final ContainerLocator DAPR_CONTAINER_LOCATOR = locateContainerWithLabels(DAPR_INTERNAL_HTTP_PORT,
4346
DEV_SERVICE_LABEL);
47+
private static final ContainerLocator DASHBOARD_CONTAINER_LOCATOR = locateContainerWithLabels(
48+
DashboardContainerStartable.INTERNAL_DAPR_DASHBOARD_WORKFLOW_PORT,
49+
DASHBOARD_WORKFLOW_LABEL);
4450

4551
private static final String QUARKUS_DAPR_SERVICE_NAME_PREFIX = "quarkus-dev-service-";
4652
private static final String DASHBOARD_WORKFLOW = QUARKUS_DAPR_SERVICE_NAME_PREFIX + "dashboard-workflow";
@@ -79,17 +85,35 @@ List<DevServicesResultBuildItem> devServices(
7985
DevServicesResultBuildItem discoveredDapr = discoverDaprContainer(config, launchMode);
8086
if (discoveredDapr != null) {
8187
containers.add(discoveredDapr);
88+
// Even when reusing a shared Dapr container, we still need to start the
89+
// dashboard and state-store containers for this application (or discover them).
90+
if (config.dashboard().enabled().get()) {
91+
DevServicesResultBuildItem discoveredDashboard = discoverDashboardContainer(config, launchMode);
92+
if (discoveredDashboard != null) {
93+
containers.add(discoveredDashboard);
94+
} else {
95+
DevServicesResultBuildItem pgsql = configurePgsqlContainer(network, launchMode);
96+
DevServicesResultBuildItem dashboard = configureDashboardWorkflowContainer(config, network, launchMode);
97+
containers.add(pgsql);
98+
containers.add(dashboard);
99+
}
100+
}
82101
return containers;
83102
}
84103

85104
DevServicesResultBuildItem dapr = configureDaprContainer(config, launchMode, network);
86105
containers.add(dapr);
87106

88107
if (config.dashboard().enabled().get()) {
89-
DevServicesResultBuildItem pgsql = configurePgsqlContainer(network);
90-
DevServicesResultBuildItem dashboard = configureDashboardWorkflowContainer(network);
91-
containers.add(dashboard);
92-
containers.add(pgsql);
108+
DevServicesResultBuildItem discoveredDashboard = discoverDashboardContainer(config, launchMode);
109+
if (discoveredDashboard != null) {
110+
containers.add(discoveredDashboard);
111+
} else {
112+
DevServicesResultBuildItem pgsql = configurePgsqlContainer(network, launchMode);
113+
DevServicesResultBuildItem dashboard = configureDashboardWorkflowContainer(config, network, launchMode);
114+
containers.add(pgsql);
115+
containers.add(dashboard);
116+
}
93117
}
94118

95119
return containers;
@@ -114,12 +138,21 @@ private static DevServicesResultBuildItem discoverDaprContainer(DaprDevServiceBu
114138
return null;
115139
}
116140

117-
configureDaprPorts(grpcAddress.getPort(), httpAddress.getPort());
141+
int grpcPort = grpcAddress.getPort();
142+
int httpPort = httpAddress.getPort();
143+
configureDaprPorts(grpcPort, httpPort);
118144
LOGGER.info("Re-using shared Dapr container {} listening on HTTP {} and gRPC {}",
119-
containerId.get(), httpAddress.getPort(), grpcAddress.getPort());
145+
containerId.get(), httpPort, grpcPort);
146+
147+
// Always supply a non-null config map so DevServicesConfigBuildStep#setup won't NPE
148+
Map<String, String> portConfig = new HashMap<>();
149+
portConfig.put(Properties.HTTP_PORT.getName(), Integer.toString(httpPort));
150+
portConfig.put(Properties.GRPC_PORT.getName(), Integer.toString(grpcPort));
151+
120152
return DevServicesResultBuildItem.discovered()
121153
.name(FEATURE)
122154
.containerId(containerId.get())
155+
.config(Collections.unmodifiableMap(portConfig))
123156
.build();
124157
}
125158

@@ -150,19 +183,54 @@ public Startable get() {
150183
.build();
151184
}
152185

186+
private static DevServicesResultBuildItem discoverDashboardContainer(DaprDevServiceBuildTimeConfig config,
187+
LaunchModeBuildItem launchModeBuildItem) {
188+
Map<Integer, ContainerAddress> mappedPorts = new HashMap<>();
189+
Optional<String> containerId = DASHBOARD_CONTAINER_LOCATOR.locateContainer(
190+
config.dashboard().serviceName(),
191+
config.shared().get(),
192+
launchModeBuildItem.getLaunchMode(),
193+
mappedPorts::put);
194+
195+
if (containerId.isEmpty()) {
196+
return null;
197+
}
198+
199+
ContainerAddress dashboardAddress = mappedPorts.get(DashboardContainerStartable.INTERNAL_DAPR_DASHBOARD_WORKFLOW_PORT);
200+
if (dashboardAddress == null) {
201+
LOGGER.warn("Found shared Dapr Dashboard container {} but missing mapped port. Creating a new one instead.",
202+
containerId.get());
203+
return null;
204+
}
205+
206+
String dashboardUrl = "http://127.0.0.1:" + dashboardAddress.getPort();
207+
LOGGER.info("Re-using shared Dapr Dashboard container {} at {}", containerId.get(), dashboardUrl);
208+
209+
Map<String, String> dashboardConfig = new HashMap<>();
210+
dashboardConfig.put(DAPR_DASHBOARD_WORKFLOW_URL, dashboardUrl);
211+
212+
return DevServicesResultBuildItem.discovered()
213+
.name(FEATURE)
214+
.containerId(containerId.get())
215+
.config(Collections.unmodifiableMap(dashboardConfig))
216+
.build();
217+
}
218+
153219
private static void configureDaprPorts(int grpcPort, int httpPort) {
154220
System.setProperty(Properties.GRPC_PORT.getName(), Integer.toString(grpcPort));
155221
System.setProperty(Properties.HTTP_PORT.getName(), Integer.toString(httpPort));
156222
}
157223

158-
private static DevServicesResultBuildItem configureDashboardWorkflowContainer(Network network) {
224+
private static DevServicesResultBuildItem configureDashboardWorkflowContainer(DaprDevServiceBuildTimeConfig config,
225+
Network network, LaunchModeBuildItem launchMode) {
159226
DevServicesResultBuildItem dashboard = DevServicesResultBuildItem.owned()
160227
.serviceName(DASHBOARD_WORKFLOW)
161228
.feature(FEATURE)
162229
.startable(new Supplier<Startable>() {
163230
@Override
164231
public Startable get() {
165-
return new DashboardContainerStartable(network);
232+
return new DashboardContainerStartable(network, launchMode.getLaunchMode(),
233+
config.dashboard().serviceName());
166234
}
167235
})
168236
.dependsOnConfig(POSTGRESQL_PORT_PROPERTY, (Startable startable, String value) -> {
@@ -178,7 +246,8 @@ public Startable get() {
178246
return dashboard;
179247
}
180248

181-
private static DevServicesResultBuildItem configurePgsqlContainer(Network network) {
249+
private static DevServicesResultBuildItem configurePgsqlContainer(Network network,
250+
LaunchModeBuildItem launchMode) {
182251
return DevServicesResultBuildItem.owned()
183252
.serviceName(STATESTORE_PG)
184253
.feature(FEATURE)
@@ -189,7 +258,7 @@ private static DevServicesResultBuildItem configurePgsqlContainer(Network networ
189258
.startable(new Supplier<Startable>() {
190259
@Override
191260
public Startable get() {
192-
return new StateStoreContainerStartable(network);
261+
return new StateStoreContainerStartable(network, launchMode.getLaunchMode());
193262
}
194263
})
195264
.build();

deployment/src/main/java/io/quarkiverse/dapr/deployment/devservices/StateStoreContainerStartable.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package io.quarkiverse.dapr.deployment.devservices;
22

3+
import static io.quarkus.devservices.common.ConfigureUtil.configureSharedServiceLabel;
4+
35
import org.testcontainers.containers.Network;
46
import org.testcontainers.postgresql.PostgreSQLContainer;
57
import org.testcontainers.utility.DockerImageName;
68

79
import io.quarkiverse.dapr.deployment.DaprProcessor;
810
import io.quarkus.deployment.builditem.Startable;
11+
import io.quarkus.runtime.LaunchMode;
912

1013
public class StateStoreContainerStartable extends PostgreSQLContainer implements Startable {
1114

@@ -15,14 +18,16 @@ public class StateStoreContainerStartable extends PostgreSQLContainer implements
1518
public static String USERNAME = POSTGRES;
1619
public static String PASSWORD = POSTGRES;
1720
public static String PGSQL_STATE_STORE = "kvstore";
21+
static final String DEV_SERVICE_LABEL = "quarkus-dev-service-dapr-pgsql";
1822

19-
public StateStoreContainerStartable(Network network) {
23+
public StateStoreContainerStartable(Network network, LaunchMode launchMode) {
2024
super(DockerImageName.parse("postgres"));
2125
super.withNetwork(network)
2226
.withNetworkAliases(PGSQL_NETWORK_ALIAS)
2327
.withDatabaseName(DaprProcessor.FEATURE)
2428
.withUsername(USERNAME)
2529
.withPassword(PASSWORD);
30+
configureSharedServiceLabel(this, launchMode, DEV_SERVICE_LABEL, "dapr-pgsql");
2631
}
2732

2833
@Override
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package io.quarkiverse.dapr.test;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatCode;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
import org.junit.jupiter.api.Test;
10+
11+
import io.dapr.config.Properties;
12+
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
13+
14+
/**
15+
* Regression tests for the shared-container feature.
16+
*
17+
* <h2>Background</h2>
18+
* When a second application starts in dev-mode and finds an already-running Dapr container
19+
* via {@code DAPR_CONTAINER_LOCATOR}, the processor used to call
20+
* {@code DevServicesResultBuildItem.discovered().name(...).containerId(...).build()} without
21+
* providing a config map.
22+
* <p>
23+
* The deprecated constructor that backs {@code DiscoveredServiceBuilder.build()} stores
24+
* {@code this.config = config} as-is, so the {@code getConfig()} accessor returned
25+
* {@code null}. Quarkus core's {@code DevServicesConfigBuildStep#setup} then called
26+
* {@code newProperties.putAll(resultBuildItem.getConfig())} which threw:
27+
*
28+
* <pre>
29+
* java.lang.NullPointerException: Cannot invoke "java.util.Map.size()" because "m" is null
30+
* at java.util.HashMap.putMapEntries
31+
* at io.quarkus.deployment.steps.DevServicesConfigBuildStep.setup(DevServicesConfigBuildStep.java:32)
32+
* </pre>
33+
*
34+
* <h2>Fix</h2>
35+
* The fix is to always call {@code .config(nonNullMap)} on the {@code DiscoveredServiceBuilder},
36+
* populating it with the resolved Dapr HTTP- and gRPC-port values so the second application
37+
* also receives them as runtime configuration.
38+
*/
39+
class DaprDevServicesSharedContainerTest {
40+
41+
/**
42+
* Simulates what the processor does when it discovers a shared container.
43+
* The produced {@code DevServicesResultBuildItem} must never expose a null config map,
44+
* otherwise {@code DevServicesConfigBuildStep} will NPE.
45+
*/
46+
@Test
47+
void discoveredItemMustHaveNonNullConfigMap() {
48+
int httpPort = 3500;
49+
int grpcPort = 50001;
50+
51+
Map<String, String> portConfig = new HashMap<>();
52+
portConfig.put(Properties.HTTP_PORT.getName(), Integer.toString(httpPort));
53+
portConfig.put(Properties.GRPC_PORT.getName(), Integer.toString(grpcPort));
54+
55+
DevServicesResultBuildItem item = DevServicesResultBuildItem.discovered()
56+
.name("dapr")
57+
.containerId("abc123")
58+
.config(java.util.Collections.unmodifiableMap(portConfig))
59+
.build();
60+
61+
// getConfig() must never return null – putAll(null) would throw NPE
62+
assertThat(item.getConfig())
63+
.as("DevServicesResultBuildItem.getConfig() must not be null")
64+
.isNotNull();
65+
}
66+
67+
/**
68+
* Verifies that the Dapr HTTP port is stored in the config map under the property name
69+
* used by the Dapr SDK ({@link Properties#HTTP_PORT}).
70+
*/
71+
@Test
72+
void discoveredItemContainsHttpPortProperty() {
73+
int httpPort = 13500;
74+
int grpcPort = 60001;
75+
76+
Map<String, String> portConfig = new HashMap<>();
77+
portConfig.put(Properties.HTTP_PORT.getName(), Integer.toString(httpPort));
78+
portConfig.put(Properties.GRPC_PORT.getName(), Integer.toString(grpcPort));
79+
80+
DevServicesResultBuildItem item = DevServicesResultBuildItem.discovered()
81+
.name("dapr")
82+
.containerId("container-42")
83+
.config(java.util.Collections.unmodifiableMap(portConfig))
84+
.build();
85+
86+
assertThat(item.getConfig())
87+
.containsEntry(Properties.HTTP_PORT.getName(), "13500")
88+
.containsEntry(Properties.GRPC_PORT.getName(), "60001");
89+
}
90+
91+
/**
92+
* Emulates the exact line in {@code DevServicesConfigBuildStep#setup} that was crashing,
93+
* to make the failure mode explicit and guarantee the fix prevents a regression.
94+
*/
95+
@Test
96+
void putAllOnDiscoveredItemConfigDoesNotThrowNpe() {
97+
Map<String, String> portConfig = new HashMap<>();
98+
portConfig.put(Properties.HTTP_PORT.getName(), "3500");
99+
portConfig.put(Properties.GRPC_PORT.getName(), "50001");
100+
101+
DevServicesResultBuildItem item = DevServicesResultBuildItem.discovered()
102+
.name("dapr")
103+
.containerId("container-shared")
104+
.config(java.util.Collections.unmodifiableMap(portConfig))
105+
.build();
106+
107+
Map<String, String> aggregated = new HashMap<>();
108+
// This is the exact operation that was NPE-ing in DevServicesConfigBuildStep:
109+
assertThatCode(() -> aggregated.putAll(item.getConfig()))
110+
.as("putAll(getConfig()) must not throw NullPointerException")
111+
.doesNotThrowAnyException();
112+
113+
assertThat(aggregated).isNotEmpty();
114+
}
115+
}

0 commit comments

Comments
 (0)