Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #54 to add DevServices for Artemis JMS #55

Merged
merged 5 commits into from
Jun 29, 2022
Merged
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
16 changes: 16 additions & 0 deletions core/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-deployment</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonp-deployment</artifactId>
Expand All @@ -40,6 +45,17 @@
<groupId>io.quarkiverse.artemis</groupId>
<artifactId>quarkus-artemis-core</artifactId>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ public class ArtemisBuildTimeConfig {
*/
@ConfigItem(name = "xa.enabled", defaultValue = "false")
public boolean xaEnabled;

/**
* Configuration for DevServices. DevServices allows Quarkus to automatically start ActiveMQ Artemis in dev and test mode.
*/
@ConfigItem
public ArtemisDevServicesBuildTimeConfig devservices;
middagj marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.quarkus.artemis.core.deployment;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;

@ConfigGroup
public class ArtemisDevServicesBuildTimeConfig {

/**
* Enable or disable Dev Services explicitly. Dev Services are automatically enabled unless {@code quarkus.artemis.url} is
* set.
*/
@ConfigItem
public Optional<Boolean> enabled = Optional.empty();

/**
* Optional fixed port the dev service will listen to.
* <p>
* If not defined, the port will be chosen randomly.
*/
@ConfigItem
public Optional<Integer> port;

/**
* The ActiveMQ Artemis container image to use.
*/
@ConfigItem(defaultValue = "quay.io/artemiscloud/activemq-artemis-broker:1.0.5")
public String imageName;

/**
* Indicates if the ActiveMQ Artemis broker managed by Quarkus Dev Services is shared.
* When shared, Quarkus looks for running containers using label-based service discovery.
* If a matching container is found, it is used, and so a second one is not started.
* Otherwise, Dev Services for ActiveMQ Artemis starts a new container.
* <p>
* The discovery uses the {@code quarkus-dev-service-artemis} label.
* The value is configured using the {@code service-name} property.
* <p>
* Container sharing is only used in dev mode.
*/
@ConfigItem(defaultValue = "true")
public boolean shared;

/**
* The value of the {@code quarkus-dev-service-artemis} label attached to the started container.
* This property is used when {@code shared} is set to {@code true}.
* In this case, before starting a container, Dev Services for ActiveMQ Artemis looks for a container with the
* {@code quarkus-dev-service-artemis} label
* set to the configured value. If found, it will use this container instead of starting a new one. Otherwise it
* starts a new container with the {@code quarkus-dev-service-artemis} label set to the specified value.
* <p>
* This property is used when you need multiple shared ActiveMQ Artemis brokers.
*/
@ConfigItem(defaultValue = "artemis")
public String serviceName;

/**
* User to start artemis broker
*/
@ConfigItem(defaultValue = "admin")
public String user;

/**
* Password to start artemis broker
*/
@ConfigItem(defaultValue = "admin")
public String password;

/**
* Queues to create on starting
*/
@ConfigItem(defaultValue = "[]")
public List<String> queues = Collections.emptyList();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package io.quarkus.artemis.core.deployment;

import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;

import org.jboss.logging.Logger;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService;
import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem;
import io.quarkus.deployment.builditem.DockerStatusBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.console.ConsoleInstalledBuildItem;
import io.quarkus.deployment.console.StartupLogCompressor;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.deployment.logging.LoggingSetupBuildItem;
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerAddress;
import io.quarkus.devservices.common.ContainerLocator;
import io.quarkus.runtime.configuration.ConfigUtils;

/**
* Start a ActiveMQ Artemis broker if needed
*/
public class DevServicesArtemisProcessor {
private static final Logger LOGGER = Logger.getLogger(DevServicesArtemisProcessor.class);
private static final String QUARKUS_ARTEMIS_URL = "quarkus.artemis.url";

/**
* Label to add to shared Dev Service for ActiveMQ Artemis running in containers.
* This allows other applications to discover the running service and use it instead of starting a new instance.
*/
static final String DEV_SERVICE_LABEL = "quarkus-dev-service-artemis";
static final int ARTEMIS_PORT = 61616;

private static final ContainerLocator artemisContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, ARTEMIS_PORT);

static volatile RunningDevService devService;
static volatile ArtemisDevServiceCfg cfg;
static volatile boolean first = true;

@BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class)
public DevServicesResultBuildItem startArtemisDevService(
DockerStatusBuildItem dockerStatusBuildItem,
LaunchModeBuildItem launchMode,
ArtemisBuildTimeConfig artemisBuildTimeConfig,
List<DevServicesSharedNetworkBuildItem> devServicesSharedNetworkBuildItem,
Optional<ConsoleInstalledBuildItem> consoleInstalledBuildItem,
CuratedApplicationShutdownBuildItem closeBuildItem,
LoggingSetupBuildItem loggingSetupBuildItem, GlobalDevServicesConfig devServicesConfig) {

ArtemisDevServiceCfg configuration = getConfiguration(artemisBuildTimeConfig);

if (devService != null) {
boolean shouldShutdownTheBroker = !configuration.equals(cfg);
if (!shouldShutdownTheBroker) {
return devService.toBuildItem();
}
shutdownBroker();
cfg = null;
}

StartupLogCompressor compressor = new StartupLogCompressor(
(launchMode.isTest() ? "(test) " : "") + "ActiveMQ Artemis Dev Services Starting:",
consoleInstalledBuildItem, loggingSetupBuildItem);
try {
devService = startArtemis(dockerStatusBuildItem, configuration, launchMode,
!devServicesSharedNetworkBuildItem.isEmpty(),
devServicesConfig.timeout);
if (devService == null) {
compressor.closeAndDumpCaptured();
} else {
compressor.close();
}
} catch (Throwable t) {
compressor.closeAndDumpCaptured();
throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
}

if (devService == null) {
return null;
}

// Configure the watch dog
if (first) {
first = false;
Runnable closeTask = () -> {
if (devService != null) {
shutdownBroker();
}
first = true;
devService = null;
cfg = null;
};
closeBuildItem.addCloseTask(closeTask, true);
}
cfg = configuration;

if (devService.isOwner()) {
LOGGER.infof("Dev Services for ActiveMQ Artemis started on %s", getArtemisUrl());
}

return devService.toBuildItem();
}

public static String getArtemisUrl() {
return devService.getConfig().get(QUARKUS_ARTEMIS_URL);
}

private void shutdownBroker() {
if (devService != null) {
try {
devService.close();
} catch (Throwable e) {
LOGGER.error("Failed to stop the ActiveMQ Artemis broker", e);
} finally {
devService = null;
}
}
}

private RunningDevService startArtemis(DockerStatusBuildItem dockerStatusBuildItem, ArtemisDevServiceCfg config,
LaunchModeBuildItem launchMode, boolean useSharedNetwork, Optional<Duration> timeout) {
if (!config.devServicesEnabled) {
// explicitly disabled
LOGGER.debug("Not starting dev services for ActiveMQ Artemis, as it has been disabled in the config.");
return null;
}

// Check if quarkus.artemis.url is set
if (ConfigUtils.isPropertyPresent(QUARKUS_ARTEMIS_URL)) {
LOGGER.debug("Not starting dev services for ActiveMQ Artemis, the quarkus.artemis.url is configured.");
return null;
}

if (!dockerStatusBuildItem.isDockerAvailable()) {
LOGGER.warn(
"Docker isn't working, please configure the ActiveMQ Artemis Url property (quarkus.artemis.url).");
return null;
}

final Optional<ContainerAddress> maybeContainerAddress = artemisContainerLocator.locateContainer(config.serviceName,
config.shared,
launchMode.getLaunchMode());

// Starting the broker
final Supplier<RunningDevService> defaultArtemisBrokerSupplier = () -> {
GenericContainer<?> container = new GenericContainer<>(config.imageName)
.withExposedPorts(ARTEMIS_PORT)
.withEnv("AMQ_USER", config.user)
.withEnv("AMQ_PASSWORD", config.password)
.withEnv("AMQ_EXTRA_ARGS", "--queues " + String.join(", ", config.queues))
.waitingFor(Wait.forLogMessage(".* Apache ActiveMQ Artemis Message Broker .*", 1));

ConfigureUtil.configureSharedNetwork(container, "artemis");

if (config.serviceName != null) {
container.withLabel(DevServicesArtemisProcessor.DEV_SERVICE_LABEL, config.serviceName);
}

timeout.ifPresent(container::withStartupTimeout);

container.start();
return new RunningDevService("ActiveMQ-Artemis",
container.getContainerId(),
container::close,
QUARKUS_ARTEMIS_URL,
String.format("tcp://%s:%d", container.getHost(), container.getMappedPort(ARTEMIS_PORT)));
};

return maybeContainerAddress
.map(containerAddress -> new RunningDevService("ActiveMQ-Artemis",
containerAddress.getId(),
null,
QUARKUS_ARTEMIS_URL, containerAddress.getUrl()))
.orElseGet(defaultArtemisBrokerSupplier);
}

private ArtemisDevServiceCfg getConfiguration(ArtemisBuildTimeConfig cfg) {
ArtemisDevServicesBuildTimeConfig devServicesConfig = cfg.devservices;
return new ArtemisDevServiceCfg(devServicesConfig);
}

private static final class ArtemisDevServiceCfg {
private final boolean devServicesEnabled;
private final String imageName;
private final Integer fixedExposedPort;
private final boolean shared;
private final String serviceName;
private final String user;
private final String password;
private final List<String> queues;

public ArtemisDevServiceCfg(ArtemisDevServicesBuildTimeConfig config) {
this.devServicesEnabled = config.enabled.orElse(true);
this.imageName = config.imageName;
this.fixedExposedPort = config.port.orElse(0);
this.shared = config.shared;
this.serviceName = config.serviceName;
this.user = config.user;
this.password = config.password;
this.queues = config.queues;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ArtemisDevServiceCfg that = (ArtemisDevServiceCfg) o;
return devServicesEnabled == that.devServicesEnabled && Objects.equals(imageName, that.imageName)
&& Objects.equals(fixedExposedPort, that.fixedExposedPort);
}

@Override
public int hashCode() {
return Objects.hash(devServicesEnabled, imageName, fixedExposedPort);
}
}
}
Loading