From d89279a378d60b7be0d02a5a83165c1dad8e8603 Mon Sep 17 00:00:00 2001 From: Ivan Bodrov Date: Tue, 21 Nov 2023 10:15:13 -0500 Subject: [PATCH] testing-concord-server: add agent wrapper, simple test --- it/testing-server/pom.xml | 15 +++ .../it/testingserver/TestingConcordAgent.java | 97 +++++++++++++++ .../testingserver/TestingConcordServer.java | 116 +++++++++++------- .../it/testingserver/TestingConcordIT.java | 109 ++++++++++++++++ 4 files changed, 296 insertions(+), 41 deletions(-) create mode 100644 it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordAgent.java create mode 100644 it/testing-server/src/test/java/com/walmartlabs/concord/it/testingserver/TestingConcordIT.java diff --git a/it/testing-server/pom.xml b/it/testing-server/pom.xml index 23b03d94db..d4cba3fc79 100644 --- a/it/testing-server/pom.xml +++ b/it/testing-server/pom.xml @@ -35,6 +35,10 @@ + + com.walmartlabs.concord + concord-agent + org.testcontainers postgresql @@ -43,5 +47,16 @@ com.typesafe config + + + com.walmartlabs.concord + concord-client2 + test + + + org.junit.jupiter + junit-jupiter-api + test + diff --git a/it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordAgent.java b/it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordAgent.java new file mode 100644 index 0000000000..531378e33b --- /dev/null +++ b/it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordAgent.java @@ -0,0 +1,97 @@ +package com.walmartlabs.concord.it.testingserver; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2023 Walmart 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. + * ===== + */ + +import com.google.inject.Guice; +import com.google.inject.Module; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigParseOptions; +import com.typesafe.config.ConfigResolveOptions; +import com.walmartlabs.concord.agent.Agent; +import com.walmartlabs.concord.agent.AgentModule; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * A helper class for running concord-agent. + * The agent runs in the same JVM as TestingConcordAgent. + */ +public class TestingConcordAgent implements AutoCloseable { + + private final Map extraConfiguration; + private final List> extraModules; + private final int apiPort; + private final String agentApiKey; + + private Agent agent; + + public TestingConcordAgent(TestingConcordServer testingConcordServer) { + this(testingConcordServer, Map.of(), List.of()); + } + + public TestingConcordAgent(TestingConcordServer testingConcordServer, Map extraConfiguration, List> extraModules) { + this.apiPort = testingConcordServer.getApiPort(); + this.agentApiKey = testingConcordServer.getAgentApiKey(); + this.extraConfiguration = extraConfiguration; + this.extraModules = extraModules; + } + + public synchronized void start() { + var config = prepareConfig(); + var system = new AgentModule(config); + var allModules = Stream.concat(extraModules.stream().map(f -> f.apply(config)), Stream.of(system)).toList(); + var injector = Guice.createInjector(allModules); + agent = injector.getInstance(Agent.class); + agent.start(); + } + + public synchronized void stop() { + if (agent != null) { + agent.stop(); + agent = null; + } + } + + @Override + public void close() { + this.stop(); + } + + private Config prepareConfig() { + var extraConfig = ConfigFactory.parseMap(this.extraConfiguration); + + var testConfig = ConfigFactory.parseMap(Map.of( + "maintenanceModeListenerPort", 0, + "server.apiBaseUrl", "http://localhost:" + apiPort, + "server.websocketUrl", "ws://localhost:" + apiPort + "/websocket", + "server.apiKey", agentApiKey + )); + + var defaultConfig = ConfigFactory.load("concord-agent.conf", ConfigParseOptions.defaults(), ConfigResolveOptions.defaults().setAllowUnresolved(true)) + .getConfig("concord-agent"); + + return extraConfig.withFallback(testConfig.withFallback(defaultConfig)).resolve(); + } +} diff --git a/it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordServer.java b/it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordServer.java index bcca549988..4f1260828e 100644 --- a/it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordServer.java +++ b/it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordServer.java @@ -20,6 +20,7 @@ * ===== */ +import com.google.common.collect.ImmutableMap; import com.google.inject.Module; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; @@ -36,28 +37,39 @@ import java.util.function.Function; import java.util.stream.Stream; +import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; +/** + * A helper class for running concord-server. It runs PostgreSQL in Docker + * and the server runs in the same JVM as TestingConcordServer. + */ public class TestingConcordServer implements AutoCloseable { + private final PostgreSQLContainer db; private final Map extraConfiguration; private final List> extraModules; + private final int apiPort; + private final String adminApiKey; + private final String agentApiKey; - private PostgreSQLContainer db; private ConcordServer server; - public TestingConcordServer(Map extraConfiguration, List> extraModules) { - this.extraConfiguration = requireNonNull(extraConfiguration); - this.extraModules = requireNonNull(extraModules); + public TestingConcordServer(PostgreSQLContainer db) { + this(db, 8001, Map.of(), List.of()); } - public TestingConcordServer() { - this(Map.of(), List.of()); + public TestingConcordServer(PostgreSQLContainer db, int apiPort, Map extraConfiguration, List> extraModules) { + this.db = requireNonNull(db); + this.extraConfiguration = requireNonNull(extraConfiguration); + this.apiPort = apiPort; + this.extraModules = requireNonNull(extraModules); + this.adminApiKey = randomString(8); + this.agentApiKey = randomString(16); } public synchronized TestingConcordServer start() throws Exception { - db = new PostgreSQLContainer<>("postgres:15-alpine"); - db.start(); + checkArgument(db.isRunning(), "The database container is not running"); var config = prepareConfig(db); var system = new ConcordServerModule(config); @@ -68,21 +80,24 @@ public synchronized TestingConcordServer start() throws Exception { return this; } - @Override - public synchronized void close() throws Exception { - this.stop(); - } - - public void stop() throws Exception { + public synchronized void stop() throws Exception { if (server != null) { server.stop(); server = null; } + } - if (db != null) { - db.stop(); - db = null; - } + @Override + public void close() throws Exception { + this.stop(); + } + + public int getApiPort() { + return apiPort; + } + + public String getApiBaseUrl() { + return "http://localhost:" + apiPort; } public ConcordServer getServer() { @@ -93,20 +108,30 @@ public PostgreSQLContainer getDb() { return db; } + public String getAdminApiKey() { + return adminApiKey; + } + + public String getAgentApiKey() { + return agentApiKey; + } + private Config prepareConfig(PostgreSQLContainer db) { var extraConfig = ConfigFactory.parseMap(this.extraConfiguration); - var testConfig = ConfigFactory.parseMap(Map.of( - "db.url", db.getJdbcUrl(), - "db.appUsername", db.getUsername(), - "db.appPassword", db.getPassword(), - "db.inventoryUsername", db.getUsername(), - "db.inventoryPassword", db.getPassword(), - "db.changeLogParameters.defaultAdminToken", "foobar", - "secretStore.serverPassword", randomString(), - "secretStore.secretStoreSalt", randomString(), - "secretStore.projectSecretSalt", randomString() - )); + var testConfig = ConfigFactory.parseMap(ImmutableMap.builder() + .put("server.port", String.valueOf(apiPort)) + .put("db.url", db.getJdbcUrl()) + .put("db.appUsername", db.getUsername()) + .put("db.appPassword", db.getPassword()) + .put("db.inventoryUsername", db.getUsername()) + .put("db.inventoryPassword", db.getPassword()) + .put("db.changeLogParameters.defaultAdminToken", adminApiKey) + .put("db.changeLogParameters.defaultAgentToken", agentApiKey) + .put("secretStore.serverPassword", randomString(64)) + .put("secretStore.secretStoreSalt", randomString(64)) + .put("secretStore.projectSecretSalt", randomString(64)) + .build()); var defaultConfig = ConfigFactory.load("concord-server.conf", ConfigParseOptions.defaults(), ConfigResolveOptions.defaults().setAllowUnresolved(true)) .getConfig("concord-server"); @@ -114,8 +139,8 @@ private Config prepareConfig(PostgreSQLContainer db) { return extraConfig.withFallback(testConfig.withFallback(defaultConfig)).resolve(); } - private static String randomString() { - byte[] ab = new byte[64]; + private static String randomString(int minLength) { + byte[] ab = new byte[minLength]; new SecureRandom().nextBytes(ab); return Base64.getEncoder().encodeToString(ab); } @@ -124,20 +149,29 @@ private static String randomString() { * Just an example. */ public static void main(String[] args) throws Exception { - try (TestingConcordServer server = new TestingConcordServer(Map.of("process.watchdogPeriod", "10 seconds"), List.of())) { + try (var db = new PostgreSQLContainer<>("postgres:15-alpine"); + var server = new TestingConcordServer(db, 8001, Map.of("process.watchdogPeriod", "10 seconds"), List.of())) { + db.start(); server.start(); - - System.out.println(""" - ============================================================== + System.out.printf(""" + ============================================================== + + UI: http://localhost:8001/ + DB: + JDBC URL: %s + username: %s + password: %s - UI: http://localhost:8001/ - DB: - JDBC URL: %s - username: %s - password: %s - """.formatted(server.getDb().getJdbcUrl(), server.getDb().getUsername(), server.getDb().getPassword())); + admin API key: %s + agent API key: %s + %n""", db.getJdbcUrl(), + db.getUsername(), + db.getPassword(), + server.getAdminApiKey(), + server.getAgentApiKey()); Thread.currentThread().join(); } } } + diff --git a/it/testing-server/src/test/java/com/walmartlabs/concord/it/testingserver/TestingConcordIT.java b/it/testing-server/src/test/java/com/walmartlabs/concord/it/testingserver/TestingConcordIT.java new file mode 100644 index 0000000000..0e93548756 --- /dev/null +++ b/it/testing-server/src/test/java/com/walmartlabs/concord/it/testingserver/TestingConcordIT.java @@ -0,0 +1,109 @@ +package com.walmartlabs.concord.it.testingserver; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2023 Walmart 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. + * ===== + */ + +import com.walmartlabs.concord.client2.ApiClientConfiguration; +import com.walmartlabs.concord.client2.DefaultApiClientFactory; +import com.walmartlabs.concord.client2.ProcessApi; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.testcontainers.containers.PostgreSQLContainer; + +import java.io.IOException; +import java.net.ServerSocket; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static com.walmartlabs.concord.client2.ProcessEntry.StatusEnum.FINISHED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * A test that tests TestingConcordServer and TestingConcordAgent can run together in the same JVM. + */ +public class TestingConcordIT { + + private PostgreSQLContainer db; + private TestingConcordServer concordServer; + private TestingConcordAgent concordAgent; + + @Before + public void setUp() throws Exception { + db = new PostgreSQLContainer<>("postgres:15-alpine"); + db.start(); + + int apiPort = getFreePort(); + concordServer = new TestingConcordServer(db, apiPort, Map.of(), List.of()); + concordServer.start(); + + concordAgent = new TestingConcordAgent(concordServer); + concordAgent.start(); + } + + @After + public void tearDown() throws Exception { + if (concordAgent != null) { + concordAgent.close(); + concordAgent = null; + } + + if (concordServer != null) { + concordServer.close(); + concordServer = null; + } + + if (db != null) { + db.close(); + db = null; + } + } + + @Test(timeout = 120_000) + public void testRunningSimpleProcess() throws Exception { + var client = new DefaultApiClientFactory(concordServer.getApiBaseUrl()) + .create(ApiClientConfiguration.builder() + .apiKey(concordServer.getAdminApiKey()) + .build()); + + var processApi = new ProcessApi(client); + var response = processApi.startProcess(Map.of("concord.yml", """ + configuration: + runtime: "concord-v2" + flows: + default: + - log: "Hello!" + """.getBytes())); + assertNotNull(response.getInstanceId()); + + var process = processApi.waitForCompletion(response.getInstanceId(), Duration.ofSeconds(60).toMillis()); + assertEquals(FINISHED, process.getStatus()); + } + + private static int getFreePort() { + try (var socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}