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

kalix.javasdk.testkit.KalixTestKit Maven / Gradle / Ivy

/*
 * Copyright 2021 Lightbend Inc.
 *
 * 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 kalix.javasdk.testkit;

import akka.actor.ActorSystem;
import akka.grpc.GrpcClientSettings;
import akka.http.javadsl.Http;
import akka.http.javadsl.model.HttpRequest;
import akka.pattern.Patterns;
import akka.stream.Materializer;
import akka.stream.SystemMaterializer;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import kalix.javasdk.Kalix;
import kalix.javasdk.KalixRunner;
import kalix.javasdk.Principal;
import kalix.javasdk.impl.GrpcClients;
import kalix.javasdk.impl.ProxyInfoHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.ServerSocket;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Testkit for running Kalix services locally.
 *
 * 

Requires Docker for starting a local instance of the Kalix proxy. * *

Create a KalixTestkit with an {@link Kalix} service descriptor, and then {@link #start} the * testkit before testing the service with gRPC or HTTP clients. Call {@link #stop} after tests are * complete. */ public class KalixTestKit { /** Settings for KalixTestkit. */ public static class Settings { /** Default stop timeout (10 seconds). */ public static Duration DEFAULT_STOP_TIMEOUT = Duration.ofSeconds(10); /** Default settings for KalixTestkit. */ public static Settings DEFAULT = new Settings(DEFAULT_STOP_TIMEOUT); /** Timeout setting for stopping the local Kalix test instance. */ public final Duration stopTimeout; /** The name of this service when deployed. */ public final String serviceName; /** Whether ACL checking is enabled. */ public final boolean aclEnabled; /** Whether advanced View features are enabled. */ public final boolean advancedViews; /** To override workflow tick interval for integration tests */ public final Optional workflowTickInterval; /** Service port mappings from serviceName to host:port */ public final Map servicePortMappings; /** * Create new settings for KalixTestkit. * * @param stopTimeout timeout to use when waiting for Kalix to stop * @deprecated Use Settings.DEFAULT.withStopTimeout() instead. */ @Deprecated public Settings(final Duration stopTimeout) { this(stopTimeout, "self", false, false, Optional.empty(), Collections.emptyMap()); } private Settings( final Duration stopTimeout, final String serviceName, final boolean aclEnabled, final boolean advancedViews, final Optional workflowTickInterval, final Map servicePortMappings) { this.stopTimeout = stopTimeout; this.serviceName = serviceName; this.aclEnabled = aclEnabled; this.advancedViews = advancedViews; this.workflowTickInterval = workflowTickInterval; this.servicePortMappings = servicePortMappings; } /** * Set a custom stop timeout, for stopping the local Kalix test instance. * * @param stopTimeout timeout to use when waiting for Kalix to stop * @return updated Settings */ public Settings withStopTimeout(final Duration stopTimeout) { return new Settings(stopTimeout, serviceName, aclEnabled, advancedViews, workflowTickInterval, servicePortMappings); } /** * Set the name of this service. This will be used by the service when making calls on other * services run by the testkit to authenticate itself, allowing those services to apply ACLs * based on that name. * * @param serviceName The name of this service. * @return The updated settings. */ public Settings withServiceName(final String serviceName) { return new Settings(stopTimeout, serviceName, aclEnabled, advancedViews, workflowTickInterval, servicePortMappings); } /** * Disable ACL checking in this service. * * @return The updated settings. */ public Settings withAclDisabled() { return new Settings(stopTimeout, serviceName, false, advancedViews, workflowTickInterval, servicePortMappings); } /** * Enable ACL checking in this service. * * @return The updated settings. */ public Settings withAclEnabled() { return new Settings(stopTimeout, serviceName, true, advancedViews, workflowTickInterval, servicePortMappings); } /** * Enable advanced View features for this service. * * @return The updated settings. */ public Settings withAdvancedViews() { return new Settings(stopTimeout, serviceName, aclEnabled, true, workflowTickInterval, servicePortMappings); } /** * Overrides workflow tick interval * * @return The updated settings. */ public Settings withWorkflowTickInterval(Duration tickInterval) { return new Settings(stopTimeout, serviceName, aclEnabled, true, Optional.of(tickInterval), servicePortMappings); } /** * Add a service port mapping from serviceName to host:port. * @return The updated settings. */ public Settings withServicePortMapping(String serviceName, String host, int port) { var updatedMappings = new HashMap<>(servicePortMappings); updatedMappings.put(serviceName, host + ":" + port); return new Settings(stopTimeout, serviceName, aclEnabled, advancedViews, workflowTickInterval, Map.copyOf(updatedMappings)); } @Override public String toString() { var portMappingsRendered = servicePortMappings.entrySet().stream() .map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList()); return "Settings(" + "stopTimeout=" + stopTimeout + ", serviceName='" + serviceName + '\'' + ", aclEnabled=" + aclEnabled + ", advancedViews=" + advancedViews + ", workflowTickInterval=" + workflowTickInterval + ", servicePortMappings=[" + String.join(", ", portMappingsRendered) + "]" + ')'; } } private static final Logger log = LoggerFactory.getLogger(KalixTestKit.class); private final Kalix kalix; private final Settings settings; private boolean started = false; private String proxyHost; private int proxyPort; private Optional proxyContainer; private KalixRunner runner; private ActorSystem testSystem; /** * Create a new testkit for a Kalix service descriptor. * * @param kalix Kalix service descriptor */ public KalixTestKit(final Kalix kalix) { this(kalix, Settings.DEFAULT); } /** * Create a new testkit for a Kalix service descriptor with custom settings. * * @param kalix Kalix service descriptor * @param settings custom testkit settings */ public KalixTestKit(final Kalix kalix, final Settings settings) { this.kalix = kalix; this.settings = settings; } /** * Start this testkit with default configuration (loaded from {@code application.conf}). * * @return this KalixTestkit */ public KalixTestKit start() { return start(ConfigFactory.load()); } /** * Start this testkit with custom configuration (overrides {@code application.conf}). * * @param config custom test configuration for the KalixRunner * @return this KalixTestkit */ public KalixTestKit start(final Config config) { if (started) throw new IllegalStateException("KalixTestkit already started"); Boolean useTestContainers = Optional.ofNullable(System.getenv("KALIX_TESTKIT_USE_TEST_CONTAINERS")).map(Boolean::valueOf).orElse(true); int port = userFunctionPort(useTestContainers); Map conf = new HashMap<>(); conf.put("kalix.user-function-port", port); // don't kill the test JVM when terminating the KalixRunner conf.put("kalix.system.akka.coordinated-shutdown.exit-jvm", "off"); // dev-mode should be false when running integration tests conf.put("kalix.dev-mode.enabled", false); Config testConfig = ConfigFactory.parseMap(conf); runner = kalix.createRunner(testConfig.withFallback(config)); runner.run(); testSystem = ActorSystem.create("KalixTestkit"); runProxy(useTestContainers, port); started = true; if (log.isDebugEnabled()) log.debug("TestKit using [{}:{}] for calls to proxy from service", proxyHost, proxyPort); return this; } private void runProxy(Boolean useTestContainers, int port) { if (useTestContainers) { var proxyContainer = new KalixProxyContainer(port); this.proxyContainer = Optional.of(proxyContainer); proxyContainer.addEnv("SERVICE_NAME", settings.serviceName); proxyContainer.addEnv("ACL_ENABLED", Boolean.toString(settings.aclEnabled)); proxyContainer.addEnv("VIEW_FEATURES_ALL", Boolean.toString(settings.advancedViews)); List javaOptions = new ArrayList<>(); javaOptions.add("-Dlogback.configurationFile=logback-dev-mode.xml"); settings.servicePortMappings.forEach((serviceName, hostPort) -> { javaOptions.add("-Dkalix.dev-mode.service-port-mappings." + serviceName + "=" + hostPort); }); proxyContainer.addEnv("JAVA_TOOL_OPTIONS", String.join(" ", javaOptions)); settings.workflowTickInterval.ifPresent(tickInterval -> proxyContainer.addEnv("WORKFLOW_TICK_INTERVAL", tickInterval.toMillis() + ".millis")); proxyContainer.start(); proxyPort = proxyContainer.getProxyPort(); proxyHost = proxyContainer.getHost(); } else { proxyPort = 9000; proxyHost = "localhost"; Http http = Http.get(testSystem); log.info("Checking kalix-proxy status"); CompletionStage checkingProxyStatus = Patterns.retry(() -> http.singleRequest(HttpRequest.GET("http://localhost:8558/ready")).thenCompose(response -> { int responseCode = response.status().intValue(); if (responseCode == 200) { log.info("Kalix-proxy started"); return CompletableFuture.completedStage("Ok"); } else { log.info("Waiting for kalix-proxy, current response code is {}", responseCode); return CompletableFuture.failedFuture(new IllegalStateException("Proxy not started.")); } }), 10, Duration.ofSeconds(1), testSystem); try { checkingProxyStatus.toCompletableFuture().get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } // the proxy will announce its host and default port, but to communicate with it, // we need to use the port and host that testcontainers will expose // therefore, we set a port override in ProxyInfoHolder to allow for inter-component communication ProxyInfoHolder holder = ProxyInfoHolder.get(runner.system()); holder.overridePort(proxyPort); holder.overrideProxyHost(proxyHost); } private int userFunctionPort(Boolean useTestContainers) { if (useTestContainers) { return availableLocalPort(); } else { return KalixProxyContainer.DEFAULT_USER_FUNCTION_PORT; } } /** * Get the host name/IP address where the Kalix service is available. This is relevant in certain * Continuous Integration environments. * * @return Kalix host */ public String getHost() { if (!started) throw new IllegalStateException("Need to start KalixTestkit before accessing the host name"); return proxyHost; } /** * Get the local port where the Kalix service is available. * * @return local Kalix port */ public int getPort() { if (!started) throw new IllegalStateException("Need to start KalixTestkit before accessing the port"); return proxyPort; } /** * Get an Akka gRPC client for the given service name. The same client instance is shared for the * test. The lifecycle of the client is managed by the SDK and it should not be stopped by user * code. * * @param The "service" interface generated for the service by Akka gRPC * @param clientClass The class of a gRPC service generated by Akka gRPC */ public T getGrpcClient(Class clientClass) { return GrpcClients.get(getActorSystem()).getGrpcClient(clientClass, getHost(), getPort()); } /** * Get an Akka gRPC client for the given service name, authenticating using the given principal. * The same client instance is shared for the test. The lifecycle of the client is managed by the * SDK and it should not be stopped by user code. * * @param The "service" interface generated for the service by Akka gRPC * @param clientClass The class of a gRPC service generated by Akka gRPC * @param principal The principal to authenticate calls to the service as. */ public T getGrpcClientForPrincipal(Class clientClass, Principal principal) { String serviceName = null; if (principal == Principal.SELF) { serviceName = settings.serviceName; } else if (principal instanceof Principal.LocalService) { serviceName = ((Principal.LocalService) principal).getName(); } if (serviceName != null) { return GrpcClients.get(getActorSystem()) .getGrpcClient(clientClass, getHost(), getPort(), serviceName); } else { return GrpcClients.get(getActorSystem()).getGrpcClient(clientClass, getHost(), getPort()); } } /** * An Akka Stream materializer to use for running streams. Needed for example in a command handler * which accepts streaming elements but returns a single async reply once all streamed elements * has been consumed. */ public Materializer getMaterializer() { return SystemMaterializer.get(getActorSystem()).materializer(); } /** * Get an {@link ActorSystem} for creating Akka HTTP clients. * * @return test actor system */ public ActorSystem getActorSystem() { if (!started) throw new IllegalStateException("Need to start KalixTestkit before accessing actor system"); return testSystem; } /** * Get {@link GrpcClientSettings} for creating Akka gRPC clients. * * @return test gRPC client settings * @deprecated Use getGrpcClient instead. */ @Deprecated(since = "0.8.1", forRemoval = true) public GrpcClientSettings getGrpcClientSettings() { if (!started) throw new IllegalStateException( "Need to start KalixTestkit before accessing gRPC client settings"); return GrpcClientSettings.connectToServiceAt(getHost(), getPort(), testSystem).withTls(false); } /** Stop the testkit and local Kalix. */ public void stop() { try { proxyContainer.ifPresent(container -> container.stop()); } catch (Exception e) { log.error("KalixTestkit proxy container failed to stop", e); } try { testSystem.terminate(); testSystem .getWhenTerminated() .toCompletableFuture() .get(settings.stopTimeout.toMillis(), TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("KalixTestkit ActorSystem failed to terminate", e); } try { runner .terminate() .toCompletableFuture() .get(settings.stopTimeout.toMillis(), TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("KalixTestkit KalixRunner failed to terminate", e); } started = false; } /** * Get an available local port for testing. * * @return available local port */ public static int availableLocalPort() { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); } catch (IOException e) { throw new RuntimeException("Couldn't get available local port", e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy