Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
private AutoCloseable postgresContainer;
private AutoCloseable kafkaContainer;
private AutoCloseable frontendContainer;
private boolean isContainerReuseEnabled;

@Override
public void contextInitialized(final ServletContextEvent event) {
Expand All @@ -72,6 +73,8 @@
throw new IllegalStateException("Dev services are not available for production builds");
}

isContainerReuseEnabled = config.getValue("dev.services.container-reuse-enabled", boolean.class);

// Infer database port and name from the JDBC URL of the primary data source.
final URI defaultDataSourceUri = URI.create(
config.getValue("dt.datasource.default.url", String.class).replaceFirst("^jdbc:", "").split("\\?", 2)[0]);
Expand Down Expand Up @@ -103,6 +106,8 @@
postgresContainerClass.getMethod("withPassword", String.class).invoke(postgresContainer, postgresPassword);
postgresContainerClass.getMethod("withDatabaseName", String.class).invoke(postgresContainer, postgresDatabase);
postgresContainerClass.getMethod("withUrlParam", String.class, String.class).invoke(postgresContainer, "reWriteBatchedInserts", "true");
postgresContainerClass.getMethod("withLabel", String.class, String.class).invoke(postgresContainer, "owner", "hyades-apiserver-dev");
postgresContainerClass.getMethod("withReuse", boolean.class).invoke(postgresContainer, isContainerReuseEnabled);
addFixedExposedPortMethod.invoke(postgresContainer, /* hostPort */ postgresPort, /* containerPort */ 5432);

// TODO: Detect when Apache Kafka is requested vs. when Kafka is requested,
Expand All @@ -114,12 +119,16 @@
// * https://github.com/testcontainers/testcontainers-java/issues/9506#issuecomment-2463504967
// * https://issues.apache.org/jira/browse/KAFKA-18281
kafkaContainerClass.getMethod("withEnv", String.class, String.class).invoke(kafkaContainer, "KAFKA_LISTENERS", "PLAINTEXT://:9092,BROKER://:9093,CONTROLLER://:9094");
kafkaContainerClass.getMethod("withLabel", String.class, String.class).invoke(kafkaContainer, "owner", "hyades-apiserver-dev");
kafkaContainerClass.getMethod("withReuse", boolean.class).invoke(kafkaContainer, isContainerReuseEnabled);
addFixedExposedPortMethod.invoke(kafkaContainer, /* hostPort */ kafkaPort, /* containerPort */ 9092);

final Constructor<?> genericContainerConstructor = genericContainerClass.getDeclaredConstructor(String.class);
frontendContainer = (AutoCloseable) genericContainerConstructor.newInstance(config.getValue(DEV_SERVICES_IMAGE_FRONTEND.getPropertyName(), String.class));
genericContainerClass.getMethod("withEnv", String.class, String.class).invoke(frontendContainer, "API_BASE_URL", "http://localhost:8080");
genericContainerClass.getMethod("withExposedPorts", Integer[].class).invoke(frontendContainer, (Object) new Integer[]{8080});
genericContainerClass.getMethod("withLabel", String.class, String.class).invoke(frontendContainer, "owner", "hyades-apiserver-dev");
genericContainerClass.getMethod("withReuse", boolean.class).invoke(frontendContainer, isContainerReuseEnabled);
addFixedExposedPortMethod.invoke(frontendContainer, /* hostPort */ frontendPort, /* containerPort */ 8080);
if (config.getValue(DEV_SERVICES_IMAGE_FRONTEND.getPropertyName(), String.class).endsWith(":snapshot")) {
genericContainerClass.getMethod("withImagePullPolicy", imagePullPolicyClass).invoke(frontendContainer, alwaysPullPolicy);
Expand Down Expand Up @@ -161,7 +170,10 @@
LOGGER.info("Creating topics: %s".formatted(topicsToCreate));
adminClient.createTopics(topicsToCreate).all().get();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Failed to create topics", e);
if (e.getCause() == null
|| !"TopicExistsException".equals(e.getCause().getClass().getSimpleName())) {
throw new RuntimeException("Failed to create topics", e);

Check warning on line 175 in apiserver/src/main/java/org/dependencytrack/dev/DevServicesInitializer.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

apiserver/src/main/java/org/dependencytrack/dev/DevServicesInitializer.java#L175

Avoid throwing raw exception types.
}
Comment on lines 172 to +176
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TopicExists handling is based on comparing e.getCause().getClass().getSimpleName() to a string. This is brittle and can misclassify unrelated exceptions with the same simple name; it also doesn’t preserve the thread interrupt status when an InterruptedException is caught. Prefer checking the actual Kafka exception type (e.g. org.apache.kafka.common.errors.TopicExistsException, accounting for potential wrapping) and, for InterruptedException, re-interrupt the thread before propagating/handling it.

Copilot uses AI. Check for mistakes.
}

LOGGER.info("PostgreSQL is listening at localhost:%d".formatted(postgresPort));
Expand All @@ -171,23 +183,23 @@

@Override
public void contextDestroyed(final ServletContextEvent event) {
if (postgresContainer != null) {
if (postgresContainer != null && !isContainerReuseEnabled) {
LOGGER.info("Stopping postgres container");
try {
postgresContainer.close();
} catch (Exception e) {
LOGGER.error("Failed to stop PostgreSQL container", e);
}
}
if (kafkaContainer != null) {
if (kafkaContainer != null && !isContainerReuseEnabled) {
LOGGER.info("Stopping Kafka container");
try {
kafkaContainer.close();
} catch (Exception e) {
LOGGER.error("Failed to stop Kafka container", e);
}
}
if (frontendContainer != null) {
if (frontendContainer != null && !isContainerReuseEnabled) {
LOGGER.info("Stopping frontend container");
Comment on lines 185 to 203
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contextDestroyed() skips closing containers solely based on dev.services.container-reuse-enabled. In Testcontainers, withReuse(true) has no effect unless reuse is enabled globally (e.g. testcontainers.reuse.enable=true), so this can leave non-reusable containers running (and keep host ports bound) when the webapp is stopped/redeployed without a JVM exit. Consider only skipping close() when Testcontainers reports that the environment supports reuse, otherwise always close containers on shutdown.

Copilot uses AI. Check for mistakes.
try {
frontendContainer.close();
Expand Down
8 changes: 8 additions & 0 deletions apiserver/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,14 @@ init.and.exit=false
# @type: boolean
dev.services.enabled=false

# Defines whether dev services containers should be reused.
# <br/><br/>
# Refer to <https://java.testcontainers.org/features/reuse/> for details.
#
# @category: Development
# @type: boolean
dev.services.container-reuse-enabled=true
Comment on lines +1398 to +1404
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new dev.services.container-reuse-enabled setting is documented, but it doesn’t mention two important behavioral constraints of Testcontainers reuse: (1) reuse is ignored unless TESTCONTAINERS_REUSE_ENABLE=true or testcontainers.reuse.enable=true is set, and (2) when reuse is enabled the containers are intentionally not disposed on shutdown. As written, this also conflicts with the earlier dev-services description that says containers are disposed when Dependency-Track stops. Please update the property docs (and/or the dev-services section above) to reflect the prerequisite and the changed shutdown behavior so users don’t end up with unexpected lingering containers.

Copilot uses AI. Check for mistakes.

# The image to use for the frontend dev services container.
#
# @category: Development
Expand Down
Loading