Skip to content

Commit

Permalink
feat: #21 Add AsyncAPI support with Kafka
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Broudoux <[email protected]>
  • Loading branch information
lbroudoux committed Feb 16, 2024
1 parent c42a0fc commit 7cbce9c
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public interface DevServicesConfig {
*/
Optional<ArtifactsConfiguration> artifacts();

/**
* The Ensemble configuration for optional additional features.
*/
default Optional<EnsembleConfiguration> ensemble() {
return Optional.empty();
}

/**
* Configuration for Artifacts to load within Microcks container.
Expand All @@ -98,4 +104,49 @@ public interface ArtifactsConfiguration {
*/
Optional<List<String>> secondaries();
}

/**
* Configuration for optional Ensemble features to load within Microcks DevService.
*/
@ConfigGroup
public interface EnsembleConfiguration {

/**
* Whether we should enable AsyncAPI related features. This will result in the creation and
* management of a {@code MicrocksContainerEnsemble} with the async features.
*/
@WithDefault("false")
boolean asyncEnabled();

/**
* The container image name to use for the Microcks Async Minion component.
* Use an image based on or derived from: {@code quay.io/microcks/microcks-uber-async-minion:latest}.
*/
default Optional<String> asyncImageName() {
return Optional.empty();
}

/**
* Whether we should enable Postman testing related features. This will result in the creation and
* management of a {@code MicrocksContainerEnsemble} with the Postman features.
*/
@WithDefault("false")
boolean postmanEnabled();

/**
* The container image name to use for the Microcks Postman component.
* Use an image based on or derived from: {@code quay.io/microcks/microcks-postman-runner:latest}.
*/
default Optional<String> postmanImageName() {
return Optional.empty();
}

/**
*
* @return
*/
default boolean enabled() {
return asyncEnabled() || postmanEnabled();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,23 @@

import io.github.microcks.quarkus.deployment.DevServicesConfig.ArtifactsConfiguration;
import io.github.microcks.quarkus.deployment.MicrocksBuildTimeConfig.DevServiceConfiguration;
import io.github.microcks.quarkus.runtime.MicrocksRecorder;
import io.github.microcks.testcontainers.MicrocksAsyncMinionContainer;
import io.github.microcks.testcontainers.MicrocksContainer;
import io.github.microcks.testcontainers.connection.KafkaConnection;

import io.quarkus.bootstrap.workspace.SourceDir;
import io.quarkus.builder.item.EmptyBuildItem;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem;
import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService;
import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem;
Expand All @@ -44,6 +53,9 @@
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.jboss.logging.Logger;
import org.testcontainers.Testcontainers;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.utility.Base58;
import org.testcontainers.utility.DockerImageName;

import java.io.Closeable;
Expand Down Expand Up @@ -75,19 +87,22 @@
public class DevServicesMicrocksProcessor {

private static final Logger log = Logger.getLogger(DevServicesMicrocksProcessor.class);

private static final String MICROCKS = "microcks";
private static final String MICROCKS_UBER_LATEST = "quay.io/microcks/microcks-uber:latest";
private static final String MICROCKS_SCHEME = "http://";
private static final String MICROCKS_UBER_ASYNC_MINION_LATEST = "quay.io/microcks/microcks-uber-async-minion:latest";
private static final String HTTP_SCHEME = "http://";

/**
* Label to add to shared Dev Service for Microcks running in containers.
* This allows other applications to discover the running service and use it instead of starting a new instance.
*/
private static final String DEV_SERVICE_LABEL = "quarkus-dev-service-microcks";
private static final String DEV_SERVICE_LABEL = "quarkus-dev-service-" + MICROCKS;

private static final ContainerLocator microcksContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, MicrocksContainer.MICROCKS_HTTP_PORT);
private static final ContainerLocator microcksContainerLocatorForGRPC = new ContainerLocator(DEV_SERVICE_LABEL, MicrocksContainer.MICROCKS_GRPC_PORT);

private static final String CONFIG_PREFIX = "quarkus.microcks.";
private static final String CONFIG_PREFIX = "quarkus." + MICROCKS + ".";
private static final String HTTP_SUFFIX = ".http";
private static final String HTTP_HOST_SUFFIX = ".http.host";
private static final String HTTP_PORT_SUFFIX = ".http.port";
Expand All @@ -107,14 +122,34 @@ public class DevServicesMicrocksProcessor {
private static volatile DevServiceConfiguration capturedDevServicesConfig;
private static volatile boolean first = true;

private static volatile MicrocksContainersEnsembleHosts ensembleHosts;

/** An empty build item triggering the end of Microcks ensemble build process. */
public static final class MicrocksEnsembleBuildItem extends EmptyBuildItem {}

/**
* Prepare a Shared Network for Microcks containers and others (like Kafka) if enabled.
*/
@BuildStep
public Optional<DevServicesSharedNetworkBuildItem> prepareSharedNetwork(MicrocksBuildTimeConfig config) {
// Retrieve DevServices config. Only manage a default one at the moment.
DevServiceConfiguration devServicesConfiguration = config.defaultDevService();

if (!devServicesConfiguration.devservices().enabled()) {
// Explicitly disabled
log.debug("Not preparing a shared network as Microcks devservices has been disabled in the config");
return Optional.empty();
}
return Optional.of(new DevServicesSharedNetworkBuildItem());
}

/**
* Start one (or many in the future) MicrocksContainer(s) depending on extension configuration.
* We also take care of locating an re-using existing container if configured in shared modeL
* We also take care of locating and re-using existing container if configured in shared modeL
*/
@BuildStep
public List<DevServicesResultBuildItem> startMicrocksContainers(LaunchModeBuildItem launchMode,
DockerStatusBuildItem dockerStatusBuildItem,
List<DevServicesSharedNetworkBuildItem> devServicesSharedNetworkBuildItem,
MicrocksBuildTimeConfig config,
Optional<ConsoleInstalledBuildItem> consoleInstalledBuildItem,
CuratedApplicationShutdownBuildItem closeBuildItem,
Expand Down Expand Up @@ -152,7 +187,7 @@ public List<DevServicesResultBuildItem> startMicrocksContainers(LaunchModeBuildI
loggingSetupBuildItem);
try {
RunningDevService devService = startContainer(currentDevServicesConfig.devservices(), dockerStatusBuildItem,
launchMode.getLaunchMode(), outcomeBuildItem, !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout);
launchMode.getLaunchMode(), outcomeBuildItem, devServicesConfig.timeout);

if (devService == null) {
compressor.closeAndDumpCaptured();
Expand All @@ -168,7 +203,7 @@ public List<DevServicesResultBuildItem> startMicrocksContainers(LaunchModeBuildI
throw new RuntimeException(t);
}

// Save started Dev Services and containers.
// Save started Dev Services.
devServices = newDevServices;

if (first) {
Expand All @@ -194,6 +229,81 @@ public List<DevServicesResultBuildItem> startMicrocksContainers(LaunchModeBuildI
return devServices.stream().map(RunningDevService::toBuildItem).collect(Collectors.toList());
}

/**
* Depending on other started dev services, complement the MicrocksContainer with some other forming an Ensemble.
*/
@BuildStep
@Produce(MicrocksEnsembleBuildItem.class)
public void complementMicrocksEnsemble(MicrocksBuildTimeConfig config, DevServicesLauncherConfigResultBuildItem devServicesConfigResult,
CuratedApplicationShutdownBuildItem closeBuildItem) {

String microcksHost = ensembleHosts.getMicrocksHost();

boolean aBrokerIsPresent = false;
String kafkaBootstrapServers = null;

for (Map.Entry configEntry : devServicesConfigResult.getConfig().entrySet()) {
log.debugf("DevServices config: %s=%s", configEntry.getKey(), configEntry.getValue());
if ("kafka.bootstrap.servers".equals(configEntry.getKey())) {
kafkaBootstrapServers = configEntry.getValue().toString();
aBrokerIsPresent = true;
}
}

// Get the ensemble configuration or a default one.
DevServicesConfig devServiceConfig = config.defaultDevService().devservices();
DevServicesConfig.EnsembleConfiguration ensembleConfiguration = devServiceConfig.ensemble().orElse(new DevServicesConfig.EnsembleConfiguration() {
@Override
public boolean asyncEnabled() {
return false;
}
@Override
public boolean postmanEnabled() {
return false;
}
});

if (ensembleConfiguration.asyncEnabled() || aBrokerIsPresent) {
log.debug("Starting a MicrocksAsyncMinionContainer...");

// We've got the conditions for launching a new MicrocksAsyncMinionContainer !
MicrocksAsyncMinionContainer asyncMinionContainer = new MicrocksAsyncMinionContainer(Network.SHARED,
DockerImageName.parse(ensembleConfiguration.asyncImageName().orElse(MICROCKS_UBER_ASYNC_MINION_LATEST)), microcksHost)
.withAccessToHost(true);

// Configure connection to a Kafka broker if any.
if (kafkaBootstrapServers != null) {
if (kafkaBootstrapServers.contains(",")) {
String[] kafkaAddresses = kafkaBootstrapServers.split(",");
for (String kafkaAddress : kafkaAddresses) {
if (kafkaAddress.startsWith("PLAINTEXT://")) {
kafkaBootstrapServers = kafkaAddress.replace("PLAINTEXT://", "");
}
}
}

log.debugf("Adding a KafkaConnection to '%s' for MicrocksAsyncMinionContainer", kafkaBootstrapServers);
asyncMinionContainer.withKafkaConnection(new KafkaConnection(
kafkaBootstrapServers.replace("localhost", GenericContainer.INTERNAL_HOST_HOSTNAME)));
}

asyncMinionContainer.getNetworkAliases().add(ensembleHosts.getAsyncMinionHost());
asyncMinionContainer.start();

closeBuildItem.addCloseTask(() -> asyncMinionContainer.stop(), true);
}
}

/**
* Finalize configuration by writing it to a Recorder.
*/
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
@Consume(MicrocksEnsembleBuildItem.class)
public void finalizeMicrocksEnsemble(MicrocksRecorder recorder) {
recorder.record();
}

/**
* Customize the extension card in DevUI with a link to running Microcks containers UI.
*/
Expand All @@ -216,8 +326,9 @@ public CardPageBuildItem pages(List<DevServicesResultBuildItem> devServicesResul
return cardPageBuildItem;
}


private RunningDevService startContainer(DevServicesConfig devServicesConfig, DockerStatusBuildItem dockerStatusBuildItem,
LaunchMode launchMode, CurateOutcomeBuildItem outcomeBuildItem, boolean useSharedNetwork, Optional<Duration> timeout) {
LaunchMode launchMode, CurateOutcomeBuildItem outcomeBuildItem, Optional<Duration> timeout) {
if (!devServicesConfig.enabled()) {
// explicitly disabled
log.debug("Not starting devservices for Microcks as it has been disabled in the config");
Expand Down Expand Up @@ -252,20 +363,29 @@ private RunningDevService startContainer(DevServicesConfig devServicesConfig, Do
if (launchMode == DEVELOPMENT) {
microcksContainer.withLabel(DEV_SERVICE_LABEL, devServicesConfig.serviceName());
}
String hostName = null;
if (useSharedNetwork) {
hostName = ConfigureUtil.configureSharedNetwork(microcksContainer, devServicesConfig.serviceName());
}

// Always launch microcks in a shared network to be able to access possible ensemble containers.
String microcksHost = ConfigureUtil.configureSharedNetwork(microcksContainer, devServicesConfig.serviceName());

// Build and store configuration for possible other hosts of the ensemble.
String postmanHost = String.format("%s-%s-%s-%s", MICROCKS,
devServicesConfig.serviceName(), "postman", Base58.randomString(5));
String asyncMinionHost = String.format("%s-%s-%s-%s", MICROCKS,
devServicesConfig.serviceName(), "async-minion", Base58.randomString(5));
ensembleHosts = new MicrocksContainersEnsembleHosts(microcksHost, postmanHost, asyncMinionHost);

// Set the required environment variables for running as an Ensemble.
microcksContainer.withEnv("POSTMAN_RUNNER_URL", HTTP_SCHEME + postmanHost + ":3000")
.withEnv("TEST_CALLBACK_URL", HTTP_SCHEME + microcksHost + ":" + MicrocksContainer.MICROCKS_HTTP_PORT)
.withEnv("ASYNC_MINION_URL", HTTP_SCHEME + asyncMinionHost + ":" + MicrocksAsyncMinionContainer.MICROCKS_ASYNC_MINION_HTTP_PORT);

microcksContainer.start();

// Now importing artifacts into running container.
initializeArtifacts(microcksContainer, devServicesConfig, outcomeBuildItem);

// Build the Microcks visible host to feed the exposed properties.
String visibleHost = (hostName != null ? hostName : microcksContainer.getHost());

return new RunningDevService(devServicesConfig.serviceName(), microcksContainer.getContainerId(), microcksContainer::close,
getDevServiceExposedConfig(devServicesConfig.serviceName(), visibleHost,
getDevServiceExposedConfig(devServicesConfig.serviceName(), "localhost",
microcksContainer.getMappedPort(MicrocksContainer.MICROCKS_HTTP_PORT),
microcksContainer.getMappedPort(MicrocksContainer.MICROCKS_GRPC_PORT))
);
Expand All @@ -286,10 +406,11 @@ private String getConfigPrefix(String serviceName) {
private Map<String, String> getDevServiceExposedConfig(String serviceName, String visibleHostName, Integer httpPort, Integer grpcPort) {
String configPrefix = getConfigPrefix(serviceName);

return Map.of(configPrefix + HTTP_SUFFIX, MICROCKS_SCHEME + visibleHostName + ":" + httpPort.toString(),
return Map.of(
configPrefix + HTTP_SUFFIX, HTTP_SCHEME + visibleHostName + ":" + httpPort.toString(),
configPrefix + HTTP_HOST_SUFFIX, visibleHostName,
configPrefix + HTTP_PORT_SUFFIX, httpPort.toString(),
configPrefix + GRPC_SUFFIX, MICROCKS_SCHEME + visibleHostName + ":" + grpcPort.toString(),
configPrefix + GRPC_SUFFIX, HTTP_SCHEME + visibleHostName + ":" + grpcPort.toString(),
configPrefix + GRPC_HOST_SUFFIX, visibleHostName,
configPrefix + GRPC_PORT_SUFFIX, grpcPort.toString());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright The Microcks 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.github.microcks.quarkus.deployment;

/**
* A simple bean for holding immutable configuration of Microcks ensemble components hosts.
* @author laurent
*/
public final class MicrocksContainersEnsembleHosts {

private final String microcksHost;
private final String postmanHost;
private final String asyncMinionHost;

/**
* Store the host names of the Ensemble components.
* @param microcksHost The hostname of the microcks container.
* @param postmanHost The hostname of the postman-runtime container.
* @param asyncMinionHost The hostname of the async-minion container.
*/
public MicrocksContainersEnsembleHosts(String microcksHost, String postmanHost, String asyncMinionHost) {
this.microcksHost = microcksHost;
this.postmanHost = postmanHost;
this.asyncMinionHost = asyncMinionHost;
}

public String getMicrocksHost() {
return microcksHost;
}

public String getPostmanHost() {
return postmanHost;
}

public String getAsyncMinionHost() {
return asyncMinionHost;
}
}
2 changes: 1 addition & 1 deletion runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<description>REST, GraphQL, gRPC API and SOAP mock endpoints and contract testing - directly from your specs</description>

<properties>
<microcks-testcontainers.version>0.2.1</microcks-testcontainers.version>
<microcks-testcontainers.version>0.2.3</microcks-testcontainers.version>
</properties>

<dependencies>
Expand Down
Loading

0 comments on commit 7cbce9c

Please sign in to comment.