You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sd...@apache.org on 2022/05/04 09:29:53 UTC

[ignite-3] branch main updated: IGNITE-16513 Add an integration test for 'cluster init' CLI command (#794)

This is an automated email from the ASF dual-hosted git repository.

sdanilov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new dda822680 IGNITE-16513 Add an integration test for 'cluster init' CLI command (#794)
dda822680 is described below

commit dda822680855dc1986510ee39474d7688f52fa0d
Author: Roman Puchkovskiy <ro...@gmail.com>
AuthorDate: Wed May 4 13:29:48 2022 +0400

    IGNITE-16513 Add an integration test for 'cluster init' CLI command (#794)
---
 .../src/main/java/org/apache/ignite/Ignition.java  |  15 ++
 .../java/org/apache/ignite/IgnitionManager.java    |   6 +
 .../ignite/cli/AbstractCliIntegrationTest.java     |  49 +++++
 .../apache/ignite/cli/ItClusterCommandTest.java    | 198 +++++++++++++++++++++
 .../org/apache/ignite/cli/ItConfigCommandTest.java |  26 +--
 .../java/org/apache/ignite/cli/NoOpHandler.java}   |  21 ++-
 .../resources/hardcoded-ports-config.json          |  13 ++
 .../org/apache/ignite/cli/AbstractCliTest.java     |   4 +-
 .../ignite/internal/compute/ItComputeTest.java     |   1 +
 .../org/apache/ignite/internal/app/IgniteImpl.java |   3 +
 10 files changed, 300 insertions(+), 36 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/Ignition.java b/modules/api/src/main/java/org/apache/ignite/Ignition.java
index cba3b8b33..2ea9ea63a 100644
--- a/modules/api/src/main/java/org/apache/ignite/Ignition.java
+++ b/modules/api/src/main/java/org/apache/ignite/Ignition.java
@@ -32,6 +32,9 @@ public interface Ignition {
     /**
      * Starts an Ignite node with an optional bootstrap configuration from a HOCON file.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param name Name of the node. Must not be {@code null}.
      * @param configPath Path to the node configuration in the HOCON format. Can be {@code null}.
      * @param workDir Work directory for the started node. Must not be {@code null}.
@@ -44,6 +47,9 @@ public interface Ignition {
      * Starts an Ignite node with an optional bootstrap configuration from a HOCON file, with an optional class loader for further usage by
      * {@link java.util.ServiceLoader}.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param name Name of the node. Must not be {@code null}.
      * @param configPath Path to the node configuration in the HOCON format. Can be {@code null}.
      * @param workDir Work directory for the started node. Must not be {@code null}.
@@ -57,6 +63,9 @@ public interface Ignition {
     /**
      * Starts an Ignite node with an optional bootstrap configuration from a URL linking to HOCON configs.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param name Name of the node. Must not be {@code null}.
      * @param cfgUrl URL linking to the node configuration in the HOCON format. Can be {@code null}.
      * @param workDir Work directory for the started node. Must not be {@code null}.
@@ -68,6 +77,9 @@ public interface Ignition {
     /**
      * Starts an Ignite node with an optional bootstrap configuration from an input stream with HOCON configs.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param name Name of the node. Must not be {@code null}.
      * @param config Optional node configuration based on
      *      {@link org.apache.ignite.configuration.schemas.network.NetworkConfigurationSchema}.
@@ -94,6 +106,9 @@ public interface Ignition {
     /**
      * Starts an Ignite node with the default configuration.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param name Name of the node. Must not be {@code null}.
      * @param workDir Work directory for the started node. Must not be {@code null}.
      * @return Completable future that resolves into an Ignite node after all components are started and the cluster initialization is
diff --git a/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java b/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java
index 946210c29..87ac37bb6 100644
--- a/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java
+++ b/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java
@@ -39,6 +39,9 @@ public class IgnitionManager {
     /**
      * Starts an Ignite node with an optional bootstrap configuration from an input stream with HOCON configs.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param nodeName Name of the node. Must not be {@code null}.
      * @param configStr Optional node configuration based on
      *      {@link org.apache.ignite.configuration.schemas.network.NetworkConfigurationSchema}.
@@ -79,6 +82,9 @@ public class IgnitionManager {
     /**
      * Starts an Ignite node with an optional bootstrap configuration from a HOCON file.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param nodeName Name of the node. Must not be {@code null}.
      * @param cfgPath  Path to the node configuration in the HOCON format. Can be {@code null}.
      * @param workDir  Work directory for the started node. Must not be {@code null}.
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/AbstractCliIntegrationTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/AbstractCliIntegrationTest.java
new file mode 100644
index 000000000..5247474ad
--- /dev/null
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/AbstractCliIntegrationTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.cli;
+
+import io.micronaut.context.ApplicationContext;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import org.apache.ignite.cli.spec.IgniteCliSpec;
+import picocli.CommandLine;
+
+/**
+ * Base class for CLI-related integration tests.
+ */
+public abstract class AbstractCliIntegrationTest extends AbstractCliTest {
+    /** stderr. */
+    protected final ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+    /** stdout. */
+    protected final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+    /**
+     * Creates a new command line interpreter.
+     *
+     * @param applicationCtx DI context.
+     * @return New command line instance.
+     */
+    protected final CommandLine cmd(ApplicationContext applicationCtx) {
+        CommandLine.IFactory factory = new CommandFactory(applicationCtx);
+
+        return new CommandLine(IgniteCliSpec.class, factory)
+                .setErr(new PrintWriter(err, true))
+                .setOut(new PrintWriter(out, true));
+    }
+}
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ItClusterCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ItClusterCommandTest.java
new file mode 100644
index 000000000..fbfd6a1ea
--- /dev/null
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ItClusterCommandTest.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.cli;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.joining;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.env.Environment;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import org.apache.ignite.IgnitionManager;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Integration test for {@code ignite cluster} commands.
+ */
+@ExtendWith(WorkDirectoryExtension.class)
+class ItClusterCommandTest extends AbstractCliIntegrationTest {
+    private static final Node FIRST_NODE = new Node(0, 10100, 10300);
+    private static final Node SECOND_NODE = new Node(1, 11100, 11300);
+    private static final Node THIRD_NODE = new Node(2, 12100, 12300);
+    private static final Node FOURTH_NODE = new Node(3, 13100, 13300);
+
+    private static final List<Node> NODES = List.of(FIRST_NODE, SECOND_NODE, THIRD_NODE, FOURTH_NODE);
+
+    private static final String NL = System.lineSeparator();
+
+    private static final Logger topologyLogger = Logger.getLogger("org.apache.ignite.network.scalecube.ScaleCubeTopologyService");
+
+    /** DI context. */
+    private ApplicationContext ctx;
+
+    @BeforeEach
+    void setup(@WorkDirectory Path workDir, TestInfo testInfo) throws Exception {
+        CountDownLatch allNodesAreInPhysicalTopology = new CountDownLatch(1);
+
+        Handler physicalTopologyWaiter = physicalTopologyWaiter(allNodesAreInPhysicalTopology);
+        topologyLogger.addHandler(physicalTopologyWaiter);
+
+        try {
+            startClusterWithoutInit(workDir, testInfo);
+
+            waitTillAllNodesJoinPhysicalTopology(allNodesAreInPhysicalTopology);
+        } finally {
+            topologyLogger.removeHandler(physicalTopologyWaiter);
+        }
+
+        ctx = ApplicationContext.run(Environment.TEST);
+    }
+
+    private Handler physicalTopologyWaiter(CountDownLatch physicalTopologyIsFull) {
+        return new NoOpHandler() {
+            @Override
+            public void publish(LogRecord record) {
+                if (record.getMessage().contains("Topology snapshot [nodes=" + NODES.size() + "]")) {
+                    physicalTopologyIsFull.countDown();
+                }
+            }
+        };
+    }
+
+    private void startClusterWithoutInit(Path workDir, TestInfo testInfo) {
+        NODES.parallelStream().forEach(node -> startNodeWithoutInit(node, workDir, testInfo));
+    }
+
+    private void waitTillAllNodesJoinPhysicalTopology(CountDownLatch allNodesAreInPhysicalTopology) throws InterruptedException {
+        assertTrue(allNodesAreInPhysicalTopology.await(10, SECONDS), "Physical topology was not formed in time");
+    }
+
+    /**
+     * Initiates node start and waits till it makes its REST endpoints available, but does NOT invoke init.
+     *
+     * @param node      node
+     * @param workDir   working directory
+     * @param testInfo  test info
+     */
+    private void startNodeWithoutInit(Node node, Path workDir, TestInfo testInfo) {
+        String nodeName = testNodeName(testInfo, node.nodeIndex);
+
+        String config;
+        try {
+            config = configJsonFor(node);
+        } catch (IOException e) {
+            throw new RuntimeException("Cannot load config", e);
+        }
+
+        IgnitionManager.start(nodeName, config, workDir.resolve(nodeName));
+    }
+
+    private String configJsonFor(Node node) throws IOException {
+        String config = Files.readString(Path.of("src/integrationTest/resources/hardcoded-ports-config.json"));
+        config = config.replaceAll("<NETWORK_PORT>", String.valueOf(node.networkPort));
+        config = config.replaceAll("<REST_PORT>", String.valueOf(node.restPort));
+        config = config.replaceAll("<NET_CLUSTER_NODES>", netClusterNodes());
+
+        return config;
+    }
+
+    private String netClusterNodes() {
+        return NODES.stream()
+                .map(Node::networkHostPort)
+                .map(s -> "\"" + s + "\"")
+                .collect(joining(", ", "[", "]"));
+    }
+
+    @AfterEach
+    void tearDown(TestInfo testInfo) {
+        for (int i = 0; i < NODES.size(); i++) {
+            IgnitionManager.stop(testNodeName(testInfo, i));
+        }
+
+        if (ctx != null) {
+            ctx.stop();
+        }
+    }
+
+    /**
+     * Starts a cluster of 4 nodes and executes init command on it. First node is used to issue the command via REST endpoint,
+     * second will host the meta-storage RAFT group, third will host the Cluster Management RAFT Group (CMG), fourth
+     * will be just a node.
+     *
+     * @param testInfo test info (used to derive node names)
+     */
+    @Test
+    void initClusterWithNodesOfDifferentRoles(TestInfo testInfo) {
+        int exitCode = cmd(ctx).execute(
+                "cluster", "init",
+                "--node-endpoint", FIRST_NODE.restHostPort(),
+                "--meta-storage-node", SECOND_NODE.nodeName(testInfo),
+                "--cmg-node", THIRD_NODE.nodeName(testInfo)
+        );
+
+        assertThat(
+                String.format("Wrong exit code; std is '%s', stderr is '%s'", out.toString(UTF_8), err.toString(UTF_8)),
+                exitCode, is(0)
+        );
+        assertThat(out.toString(UTF_8), is("Cluster was initialized successfully." + NL));
+
+        // TODO: when IGNITE-16526 is implemented, also check that the logical topology contains all 4 nodes
+    }
+
+    private static class Node {
+        private final int nodeIndex;
+        private final int networkPort;
+        private final int restPort;
+
+        private Node(int nodeIndex, int networkPort, int restPort) {
+            this.nodeIndex = nodeIndex;
+            this.networkPort = networkPort;
+            this.restPort = restPort;
+        }
+
+        String nodeName(TestInfo testInfo) {
+            return testNodeName(testInfo, nodeIndex);
+        }
+
+        String networkHostPort() {
+            return "localhost:" + networkPort;
+        }
+
+        String restHostPort() {
+            return "localhost:" + restPort;
+        }
+    }
+}
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ItConfigCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ItConfigCommandTest.java
index 41c5ef34f..8a9b404ca 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ItConfigCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/ItConfigCommandTest.java
@@ -32,8 +32,6 @@ import com.jayway.jsonpath.DocumentContext;
 import com.jayway.jsonpath.JsonPath;
 import io.micronaut.context.ApplicationContext;
 import io.micronaut.context.env.Environment;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
@@ -41,7 +39,6 @@ import net.minidev.json.JSONObject;
 import net.minidev.json.JSONValue;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgnitionManager;
-import org.apache.ignite.cli.spec.IgniteCliSpec;
 import org.apache.ignite.internal.app.IgniteImpl;
 import org.apache.ignite.internal.testframework.WorkDirectory;
 import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
@@ -50,22 +47,15 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.extension.ExtendWith;
-import picocli.CommandLine;
 
 /**
  * Integration test for {@code ignite config} commands.
  */
 @ExtendWith(WorkDirectoryExtension.class)
-public class ItConfigCommandTest extends AbstractCliTest {
+public class ItConfigCommandTest extends AbstractCliIntegrationTest {
     /** DI context. */
     private ApplicationContext ctx;
 
-    /** stderr. */
-    private final ByteArrayOutputStream err = new ByteArrayOutputStream();
-
-    /** stdout. */
-    private final ByteArrayOutputStream out = new ByteArrayOutputStream();
-
     /** Node. */
     private IgniteImpl node;
 
@@ -186,20 +176,6 @@ public class ItConfigCommandTest extends AbstractCliTest {
         assertFalse(outResult.containsKey("node"));
     }
 
-    /**
-     * Creates a new command line interpreter.
-     *
-     * @param applicationCtx DI context.
-     * @return New command line instance.
-     */
-    private CommandLine cmd(ApplicationContext applicationCtx) {
-        CommandLine.IFactory factory = new CommandFactory(applicationCtx);
-
-        return new CommandLine(IgniteCliSpec.class, factory)
-                .setErr(new PrintWriter(err, true))
-                .setOut(new PrintWriter(out, true));
-    }
-
     /**
      * Reset stderr and stdout streams.
      */
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/AbstractCliTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/NoOpHandler.java
similarity index 69%
copy from modules/cli/src/test/java/org/apache/ignite/cli/AbstractCliTest.java
copy to modules/cli/src/integrationTest/java/org/apache/ignite/cli/NoOpHandler.java
index 151ba472a..32fbdbfdb 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/AbstractCliTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/NoOpHandler.java
@@ -17,17 +17,20 @@
 
 package org.apache.ignite.cli;
 
-import org.junit.jupiter.api.BeforeAll;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
 
 /**
- * Base class for any CLI tests.
+ * Adapter for {@link Handler} with empty implementations of all methods but {@link Handler#publish(LogRecord)}.
  */
-public class AbstractCliTest {
-    /**
-     * Sets up a dumb terminal before tests.
-     */
-    @BeforeAll
-    private static void beforeAll() {
-        System.setProperty("org.jline.terminal.dumb", "true");
+public abstract class NoOpHandler extends Handler {
+    @Override
+    public void flush() {
+        // no-op
+    }
+
+    @Override
+    public void close() throws SecurityException {
+        // no-op
     }
 }
diff --git a/modules/cli/src/integrationTest/resources/hardcoded-ports-config.json b/modules/cli/src/integrationTest/resources/hardcoded-ports-config.json
new file mode 100644
index 000000000..3873c574f
--- /dev/null
+++ b/modules/cli/src/integrationTest/resources/hardcoded-ports-config.json
@@ -0,0 +1,13 @@
+{
+  "network": {
+    "port": <NETWORK_PORT>,
+    "portRange": 0,
+    "nodeFinder": {
+      "netClusterNodes": <NET_CLUSTER_NODES>
+    }
+  },
+  "rest": {
+    "port": <REST_PORT>,
+    "portRange": 0
+  }
+}
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/AbstractCliTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/AbstractCliTest.java
index 151ba472a..7387f57aa 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/AbstractCliTest.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/AbstractCliTest.java
@@ -22,12 +22,12 @@ import org.junit.jupiter.api.BeforeAll;
 /**
  * Base class for any CLI tests.
  */
-public class AbstractCliTest {
+public abstract class AbstractCliTest {
     /**
      * Sets up a dumb terminal before tests.
      */
     @BeforeAll
-    private static void beforeAll() {
+    static void beforeAll() {
         System.setProperty("org.jline.terminal.dumb", "true");
     }
 }
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTest.java
index cc554e6e9..bfec096e6 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTest.java
@@ -52,6 +52,7 @@ import org.junit.jupiter.api.Test;
 /**
  * Integration tests for Compute functionality.
  */
+@SuppressWarnings("resource")
 class ItComputeTest extends AbstractClusterIntegrationTest {
     @Test
     void executesJobLocally() throws Exception {
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index c92185f47..6c08c585d 100644
--- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -352,6 +352,9 @@ public class IgniteImpl implements Ignite {
     /**
      * Starts ignite node.
      *
+     * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its
+     * REST endpoint is functional).
+     *
      * @param cfg Optional node configuration based on
      *         {@link org.apache.ignite.configuration.schemas.network.NetworkConfigurationSchema}. Following rules are used for applying the
      *         configuration properties: