All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkiverse.zeebe.devservices.ZeebeDevServiceProcessor Maven / Gradle / Ivy

package io.quarkiverse.zeebe.devservices;

import static io.quarkiverse.zeebe.ZeebeProcessor.FEATURE_NAME;
import static io.quarkus.runtime.LaunchMode.DEVELOPMENT;

import java.io.Closeable;
import java.io.IOException;
import java.net.ServerSocket;
import java.time.Duration;
import java.util.*;
import java.util.function.Supplier;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
import org.testcontainers.containers.Network;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;

import io.camunda.zeebe.client.ZeebeClient;
import io.quarkiverse.zeebe.ZeebeDevServiceBuildTimeConfig;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.*;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService;
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;
import io.zeebe.containers.*;
import io.zeebe.containers.util.HostPortForwarder;

public class ZeebeDevServiceProcessor {

    private static final String DEFAULT_ZEEBE_CONTAINER_IMAGE = "camunda/zeebe";

    private static final String DEFAULT_ZEEBE_VERSION = ZeebeClient.class.getPackage().getImplementationVersion();

    private static final DockerImageName ZEEBE_IMAGE_NAME = DockerImageName.parse(DEFAULT_ZEEBE_CONTAINER_IMAGE)
            .withTag(DEFAULT_ZEEBE_VERSION);

    private static final Logger log = Logger.getLogger(ZeebeDevServiceProcessor.class);
    static final String PROP_ZEEBE_GATEWAY_ADDRESS = "quarkus.zeebe.client.broker.gateway-address";
    static final String PROP_ZEEBE_REST_ADDRESS = "quarkus.zeebe.client.broker.rest-address";
    private static final String DEV_SERVICE_LABEL = "quarkus-dev-service-zeebe";
    public static final int DEFAULT_ZEEBE_PORT = ZeebePort.GATEWAY.getPort();
    public static final int DEFAULT_ZEEBE_REST_PORT = 8080;
    private static final ContainerLocator zeebeContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, DEFAULT_ZEEBE_PORT);
    static volatile ZeebeRunningDevService devService;
    static volatile ZeebeDevServiceCfg cfg;
    static volatile boolean first = true;

    @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class })
    public DevServicesResultBuildItem startZeebeContainers(LaunchModeBuildItem launchMode,
            List devServicesSharedNetworkBuildItem,
            ZeebeDevServiceBuildTimeConfig buildTimeConfig,
            Optional consoleInstalledBuildItem,
            CuratedApplicationShutdownBuildItem closeBuildItem,
            DockerStatusBuildItem dockerStatusBuildItem,
            BuildProducer startResultProducer,
            LoggingSetupBuildItem loggingSetupBuildItem, GlobalDevServicesConfig devServicesConfig) {

        ZeebeDevServiceCfg configuration = getConfiguration(buildTimeConfig);

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

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

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

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

        if (devService.isOwner()) {
            String tmp = devService.getConfig().get(PROP_ZEEBE_GATEWAY_ADDRESS);
            log.infof("The zeebe broker is ready to accept connections on %s (http://%s)",
                    tmp, tmp);
        }

        return devService.toBuildItem();
    }

    public static class ZeebeRunningDevService extends RunningDevService {

        public ZeebeRunningDevService(String name, String containerId, Closeable closeable, Map config,
                String zeebeInternalUrl) {
            super(name, containerId, closeable, config);
        }
    }

    private ZeebeRunningDevService startZeebe(DockerStatusBuildItem dockerStatusBuildItem,
            ZeebeDevServiceCfg config,
            LaunchModeBuildItem launchMode, boolean useSharedNetwork, Optional timeout) {

        if (!config.devServicesEnabled) {
            // explicitly disabled
            log.debug("Not starting dev services for Zeebe as it has been disabled in the config");
            return null;
        }

        if (ConfigUtils.isPropertyPresent(PROP_ZEEBE_GATEWAY_ADDRESS)) {
            log.debug("Not starting dev services for Zeebe as '" + PROP_ZEEBE_GATEWAY_ADDRESS + "' have been provided");
            return null;
        }

        if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) {
            log.warn(
                    "Docker isn't working, please configure the zeebe broker servers gateway property ("
                            + PROP_ZEEBE_GATEWAY_ADDRESS + ").");
            return null;
        }

        final Optional maybeContainerAddress = zeebeContainerLocator.locateContainer(config.serviceName,
                config.shared,
                launchMode.getLaunchMode());

        // Starting the broker
        final Supplier defaultZeebeBrokerSupplier = () -> {

            DockerImageName image = ZEEBE_IMAGE_NAME;
            if (config.imageName != null) {
                image = DockerImageName.parse(config.imageName);
            }

            int testDebugExportPort = config.testDebugExportPort;
            if (launchMode.isTest() && config.testExporter) {
                if (config.testDebugExportPort == 0) {
                    try (ServerSocket serverSocket = new ServerSocket(0)) {
                        testDebugExportPort = serverSocket.getLocalPort();
                    } catch (IOException e) {
                        log.error("Port for debug exporter receiver is not available");
                    }
                }
            }

            QuarkusZeebeContainer container = new QuarkusZeebeContainer(
                    image,
                    config.fixedExposedPort,
                    launchMode.getLaunchMode() == DEVELOPMENT ? config.serviceName : null,
                    useSharedNetwork,
                    launchMode.isTest(),
                    testDebugExportPort,
                    config.devDebugExporter,
                    config.debugReceiverPort,
                    config.fixedExposedRestPort);
            timeout.ifPresent(container::withStartupTimeout);

            // enable test-container reuse
            if (config.reuse) {
                container.withReuse(true);
            }

            container.start();

            String gateway = String.format("%s:%d", container.getZeebeHost(), container.getGrpcPort());
            String baseUrl = String.format("http://%s:%d", container.getZeebeHost(), container.getRestPort());
            String zeebeInternalUrl = container.getInternalAddress(DEFAULT_ZEEBE_PORT);
            String testClient = container.getExternalAddress(DEFAULT_ZEEBE_PORT);
            String testClientRest = container.getExternalAddress(DEFAULT_ZEEBE_REST_PORT);

            return new ZeebeRunningDevService(FEATURE_NAME,
                    container.getContainerId(),
                    new ContainerShutdownCloseable(container, FEATURE_NAME),
                    configMap(gateway, baseUrl, launchMode.isTest(), testClient, testClientRest, testDebugExportPort,
                            config.testExporter),
                    zeebeInternalUrl);
        };

        return maybeContainerAddress
                .map(containerAddress -> new ZeebeRunningDevService(FEATURE_NAME,
                        containerAddress.getId(),
                        null, configMap(containerAddress.getUrl(), containerAddress.getUrl(), false, null, null, null, false),
                        null))
                .orElseGet(defaultZeebeBrokerSupplier);
    }

    private static Map configMap(String gateway, String baseUrl, boolean test, String testClient,
            String testClientRest,
            Integer testDebugExportPort,
            boolean testExporter) {
        Map config = new HashMap<>();
        config.put(PROP_ZEEBE_GATEWAY_ADDRESS, gateway);
        config.put(PROP_ZEEBE_REST_ADDRESS, baseUrl);
        if (test && testExporter) {
            if (testDebugExportPort != null) {
                config.put("quarkiverse.zeebe.devservices.test.receiver-port", "" + testDebugExportPort);
            }
            if (testClient != null) {
                config.put("quarkiverse.zeebe.devservices.test.gateway-address", testClient);
                config.put("quarkiverse.zeebe.devservices.test.rest-address", testClientRest);
            }
        }
        return config;
    }

    private void stopZeebe() {
        if (devService != null) {
            try {
                devService.close();
            } catch (Throwable e) {
                log.error("Failed to stop the Zeebe broker", e);
            } finally {
                devService = null;
            }
        }
    }

    private ZeebeDevServiceCfg getConfiguration(ZeebeDevServiceBuildTimeConfig cfg) {
        ZeebeDevServicesConfig devServicesConfig = cfg.devService();
        return new ZeebeDevServiceCfg(devServicesConfig);
    }

    private static final class ZeebeDevServiceCfg {
        private final boolean devServicesEnabled;
        private final String imageName;
        private final Integer fixedExposedPort;
        private final Integer fixedExposedRestPort;
        private final boolean shared;
        private final String serviceName;

        private final boolean testExporter;
        private final int testDebugExportPort;

        private final boolean devDebugExporter;

        private final int debugReceiverPort;

        private final boolean reuse;

        public ZeebeDevServiceCfg(ZeebeDevServicesConfig config) {
            this.devServicesEnabled = config.enabled();
            this.imageName = config.imageName().orElse(null);
            this.fixedExposedPort = config.port().orElse(0);
            this.fixedExposedRestPort = config.restPort().orElse(0);
            this.shared = config.shared();
            this.serviceName = config.serviceName();
            this.testExporter = config.test().exporter();
            this.testDebugExportPort = config.test().receiverPort().orElse(0);
            this.devDebugExporter = config.devExporter().enabled();
            this.debugReceiverPort = getPort();
            this.reuse = config.reuse();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ZeebeDevServiceCfg that = (ZeebeDevServiceCfg) 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);
        }
    }

    private static class QuarkusZeebeContainer extends ZeebeContainer {

        private final int fixedExposedPort;
        private final int fixedExposedRestPort;
        private final boolean useSharedNetwork;

        private String hostName = null;

        public QuarkusZeebeContainer(DockerImageName image, int fixedExposedPort, String serviceName,
                boolean useSharedNetwork, boolean test, int testDebugExportPort, boolean devDebugExporter,
                int debugExporterPort, int fixedExposedRestPort) {
            super(image);
            log.debugf("Zeebe broker docker image %s", image);
            this.fixedExposedPort = fixedExposedPort;
            this.fixedExposedRestPort = fixedExposedRestPort;
            this.useSharedNetwork = useSharedNetwork;

            if (serviceName != null) {
                withLabel(DEV_SERVICE_LABEL, serviceName);
            }
            if (test) {
                // create random port
                withDebugExporter(testDebugExportPort);
            } else {
                if (devDebugExporter) {
                    debugExporter(debugExporterPort);
                }
            }
        }

        public void debugExporter(final int port) {
            final int containerPort = HostPortForwarder.forwardHostPort(port, 5);
            var receiver = "http://host.testcontainers.internal:" + containerPort + "/q/zeebe/records";
            //noinspection resource
            withCopyToContainer(
                    MountableFile.forClasspathResource("debug-exporter.jar"), "/tmp/debug-exporter.jar")
                    .withEnv("ZEEBE_BROKER_EXPORTERS_DEBUG_JARPATH", "/tmp/debug-exporter.jar")
                    .withEnv(
                            "ZEEBE_BROKER_EXPORTERS_DEBUG_CLASSNAME", "io.zeebe.containers.exporter.DebugExporter")
                    .withEnv(
                            "ZEEBE_BROKER_EXPORTERS_DEBUG_ARGS_URL", receiver);
        }

        @Override
        protected void configure() {
            super.configure();

            if (useSharedNetwork) {
                hostName = ConfigureUtil.configureSharedNetwork(this, "zeebe");
                addExposedPort(DEFAULT_ZEEBE_REST_PORT);
                withEnv("ZEEBE_BROKER_NETWORK_ADVERTISEDHOST", hostName);
                return;
            } else {
                withNetwork(Network.SHARED);
            }

            if (fixedExposedPort > 0) {
                addFixedExposedPort(fixedExposedPort, DEFAULT_ZEEBE_PORT);
            } else {
                addExposedPort(DEFAULT_ZEEBE_PORT);
            }
            if (fixedExposedRestPort > 0) {
                addFixedExposedPort(fixedExposedRestPort, DEFAULT_ZEEBE_REST_PORT);
            } else {
                addExposedPort(DEFAULT_ZEEBE_REST_PORT);
            }
        }

        public int getGrpcPort() {
            if (useSharedNetwork) {
                return DEFAULT_ZEEBE_PORT;
            }
            if (fixedExposedPort > 0) {
                return fixedExposedPort;
            }
            return super.getFirstMappedPort();
        }

        public int getRestPort() {
            if (useSharedNetwork) {
                return DEFAULT_ZEEBE_REST_PORT;
            }
            if (fixedExposedPort > 0) {
                return fixedExposedRestPort;
            }
            return super.getFirstMappedPort();
        }

        public String getZeebeHost() {
            return useSharedNetwork ? hostName : super.getHost();
        }
    }

    private static int getPort() {
        Config config = ConfigProvider.getConfig();
        return config
                .getOptionalValue("quarkus.http.port", Integer.class)
                .orElse(8080);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy