You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sk...@apache.org on 2022/08/11 14:20:25 UTC

[ignite-3] branch main updated: IGNITE-17347 Add port parameters to Ignite3 CLI node start command. Fixes #961

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

sk0x50 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 8551ac43d IGNITE-17347 Add port parameters to Ignite3 CLI node start command. Fixes #961
8551ac43d is described below

commit 8551ac43dd954c2bcc2ca39fc45105c5b46ce305
Author: Vadim Pakhnushev <86...@users.noreply.github.com>
AuthorDate: Thu Aug 11 17:20:03 2022 +0300

    IGNITE-17347 Add port parameters to Ignite3 CLI node start command. Fixes #961
    
    Signed-off-by: Slava Koptilin <sl...@gmail.com>
---
 modules/cli/pom.xml                                |   6 +
 .../cli/deprecated/builtins/node/NodeManager.java  |  40 ++++--
 .../cli/deprecated/spec/NodeCommandSpec.java       |  79 ++++++++++--
 .../cli/deprecated/IgniteCliInterfaceTest.java     | 136 +++++++++++++++++++--
 modules/cli/src/test/resources/ignite-config.json  |   8 ++
 .../internal/runner/app/IgniteCliRunnerTest.java   |   2 +-
 .../org/apache/ignite/app/IgniteCliRunner.java     |  35 ++++--
 parent/pom.xml                                     |   6 +
 8 files changed, 272 insertions(+), 40 deletions(-)

diff --git a/modules/cli/pom.xml b/modules/cli/pom.xml
index d7794ce20..c579e154e 100644
--- a/modules/cli/pom.xml
+++ b/modules/cli/pom.xml
@@ -231,6 +231,12 @@
             <artifactId>mockserver-netty</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.mock-server</groupId>
+            <artifactId>mockserver-junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <profiles>
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/node/NodeManager.java b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/node/NodeManager.java
index b9c17c820..99d631d13 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/node/NodeManager.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/node/NodeManager.java
@@ -24,6 +24,7 @@ import jakarta.inject.Singleton;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.Duration;
@@ -37,7 +38,6 @@ import java.util.stream.Stream;
 import org.apache.ignite.cli.deprecated.IgniteCliException;
 import org.apache.ignite.cli.deprecated.builtins.module.ModuleRegistry;
 import org.apache.ignite.cli.deprecated.ui.Spinner;
-import org.jline.terminal.Terminal;
 
 /**
  * Manager of local Ignite nodes.
@@ -60,10 +60,9 @@ public class NodeManager {
      * Creates node manager.
      *
      * @param moduleRegistry Module registry.
-     * @param terminal       System terminal instance.
      */
     @Inject
-    public NodeManager(ModuleRegistry moduleRegistry, Terminal terminal) {
+    public NodeManager(ModuleRegistry moduleRegistry) {
         this.moduleRegistry = moduleRegistry;
     }
 
@@ -75,7 +74,8 @@ public class NodeManager {
      * @param baseWorkDir  Root directory to store nodes data.
      * @param logDir       Path to log directory for receiving node state.
      * @param pidsDir      Path to directory where pid files of running nodes will be stored.
-     * @param srvCfg       Path to configuration file for Ignite node.
+     * @param srvCfgPath   Path to configuration file for Ignite node - mutually exclusive with {@code srvCfgStr}.
+     * @param srvCfgStr    Configuration for Ignite node - mutually exclusive with {@code srvCfgPath}.
      * @param javaLogProps Path to logging properties file.
      * @param out          PrintWriter for user messages.
      * @return Information about successfully started node
@@ -85,7 +85,8 @@ public class NodeManager {
             Path baseWorkDir,
             Path logDir,
             Path pidsDir,
-            Path srvCfg,
+            Path srvCfgPath,
+            String srvCfgStr,
             Path javaLogProps,
             PrintWriter out
     ) {
@@ -133,9 +134,12 @@ public class NodeManager {
             cmdArgs.add(classpath());
             cmdArgs.add(MAIN_CLASS);
 
-            if (srvCfg != null) {
-                cmdArgs.add("--config");
-                cmdArgs.add(srvCfg.toAbsolutePath().toString());
+            if (srvCfgPath != null) {
+                cmdArgs.add("--config-path");
+                cmdArgs.add(srvCfgPath.toAbsolutePath().toString());
+            } else if (srvCfgStr != null) {
+                cmdArgs.add("--config-string");
+                cmdArgs.add(escapeQuotes(srvCfgStr));
             }
 
             cmdArgs.add("--work-dir");
@@ -334,7 +338,7 @@ public class NodeManager {
                         }
                     }).reduce((a, b) -> a && b).orElse(false);
                 } else {
-                    throw new IgniteCliException("Can't find node with name" + nodeName);
+                    throw new IgniteCliException("Can't find node with name " + nodeName);
                 }
             } catch (IOException e) {
                 throw new IgniteCliException("Can't open directory with pid files " + pidsDir);
@@ -379,6 +383,24 @@ public class NodeManager {
         return baseWorkDir.resolve(nodeName);
     }
 
+    /**
+     * Adds backslash character before double quotes to keep them when passing as a command line argument.
+     *
+     * @param str String to escape.
+     * @return Escaped string.
+     */
+    private static String escapeQuotes(String str) {
+        StringWriter out = new StringWriter();
+        for (int i = 0; i < str.length(); i++) {
+            char c = str.charAt(i);
+            if (c == '"') {
+                out.write('\\');
+            }
+            out.write(c);
+        }
+        return out.toString();
+    }
+
     /**
      * Simple structure with information about running node.
      */
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java
index 24bd301a9..49416d3d7 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java
@@ -17,12 +17,18 @@
 
 package org.apache.ignite.cli.deprecated.spec;
 
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigRenderOptions;
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Callable;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.deprecated.CliPathsConfigLoader;
@@ -30,14 +36,17 @@ import org.apache.ignite.cli.deprecated.IgniteCliException;
 import org.apache.ignite.cli.deprecated.IgnitePaths;
 import org.apache.ignite.cli.deprecated.Table;
 import org.apache.ignite.cli.deprecated.builtins.node.NodeManager;
-import picocli.CommandLine;
+import picocli.CommandLine.ArgGroup;
+import picocli.CommandLine.Command;
 import picocli.CommandLine.Help.Ansi;
 import picocli.CommandLine.Help.ColorScheme;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
 
 /**
  * Commands for start/stop/list Ignite nodes on the current machine.
  */
-@CommandLine.Command(
+@Command(
         name = "node",
         description = "Manages locally running Ignite nodes.",
         subcommands = {
@@ -51,7 +60,7 @@ public class NodeCommandSpec {
     /**
      * Starts Ignite node command.
      */
-    @CommandLine.Command(name = "start", description = "Starts an Ignite node locally.")
+    @Command(name = "start", description = "Starts an Ignite node locally.")
     @Singleton
     public static class StartNodeCommandSpec extends BaseCommand implements Callable<Integer> {
 
@@ -64,12 +73,31 @@ public class NodeCommandSpec {
         private NodeManager nodeMgr;
 
         /** Consistent id, which will be used by new node. */
-        @CommandLine.Parameters(paramLabel = "name", description = "Name of the new node")
+        @Parameters(paramLabel = "name", description = "Name of the new node")
         public String nodeName;
 
-        /** Path to node config. */
-        @CommandLine.Option(names = "--config", description = "Configuration file to start the node with")
-        private Path configPath;
+        @ArgGroup(exclusive = false)
+        private ConfigOptions configOptions;
+
+        private static class ConfigOptions {
+            @ArgGroup(exclusive = false)
+            private ConfigArguments args;
+
+            /** Path to node config. */
+            @Option(names = "--config", description = "Configuration file to start the node with")
+            private Path configPath;
+        }
+
+        private static class ConfigArguments {
+            @Option(names = "--port", description = "Node port")
+            private Integer port;
+
+            @Option(names = "--rest-port", description = "REST port")
+            private Integer restPort;
+
+            @Option(names = "--join", description = "Seed nodes", split = ",")
+            private String[] seedNodes;
+        }
 
         /** {@inheritDoc} */
         @Override
@@ -84,7 +112,8 @@ public class NodeCommandSpec {
                     ignitePaths.nodesBaseWorkDir(),
                     ignitePaths.logDir,
                     ignitePaths.cliPidsDir(),
-                    configPath,
+                    getConfigPath(),
+                    getConfigStr(),
                     ignitePaths.serverJavaUtilLoggingPros(),
                     out);
 
@@ -102,12 +131,38 @@ public class NodeCommandSpec {
             out.println(tbl);
             return 0;
         }
+
+        private Path getConfigPath() {
+            return configOptions != null ? configOptions.configPath : null;
+        }
+
+        private String getConfigStr() {
+            if (configOptions == null || configOptions.args == null) {
+                return null;
+            }
+            Map<String, Object> configMap = new HashMap<>();
+            if (configOptions.args.port != null) {
+                configMap.put("network.port", configOptions.args.port);
+            }
+            if (configOptions.args.seedNodes != null) {
+                configMap.put("network.nodeFinder.netClusterNodes", Arrays.asList(configOptions.args.seedNodes));
+            }
+            if (configOptions.args.restPort != null) {
+                configMap.put("rest.port", configOptions.args.restPort);
+            }
+            Config config = ConfigFactory.parseMap(configMap);
+            if (configOptions.configPath != null) {
+                Config fallback = ConfigFactory.parseFile(configOptions.configPath.toFile());
+                config = config.withFallback(fallback).resolve();
+            }
+            return config.root().render(ConfigRenderOptions.concise().setJson(false));
+        }
     }
 
     /**
      * Command for stopping Ignite node on the current machine.
      */
-    @CommandLine.Command(name = "stop", description = "Stops a locally running Ignite node.")
+    @Command(name = "stop", description = "Stops a locally running Ignite node.")
     public static class StopNodeCommandSpec extends BaseCommand implements Callable<Integer> {
         /** Node manager. */
         @Inject
@@ -118,7 +173,7 @@ public class NodeCommandSpec {
         private CliPathsConfigLoader cliPathsCfgLdr;
 
         /** Consistent ids of nodes to stop. */
-        @CommandLine.Parameters(
+        @Parameters(
                 arity = "1..*",
                 paramLabel = "consistent-ids",
                 description = "Consistent IDs of the nodes to stop (space separated list)"
@@ -149,7 +204,7 @@ public class NodeCommandSpec {
     /**
      * Command for listing the running nodes.
      */
-    @CommandLine.Command(name = "list", description = "Shows the list of currently running local Ignite nodes.")
+    @Command(name = "list", description = "Shows the list of currently running local Ignite nodes.")
     public static class ListNodesCommandSpec extends BaseCommand implements Callable<Integer> {
         /** Node manager. */
         @Inject
@@ -195,7 +250,7 @@ public class NodeCommandSpec {
     /**
      * Command for reading the current classpath of Ignite nodes.
      */
-    @CommandLine.Command(name = "classpath", description = "Shows the current classpath used by the Ignite nodes.")
+    @Command(name = "classpath", description = "Shows the current classpath used by the Ignite nodes.")
     public static class NodesClasspathCommandSpec extends BaseCommand implements Callable<Integer> {
         /** Node manager. */
         @Inject
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/deprecated/IgniteCliInterfaceTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/deprecated/IgniteCliInterfaceTest.java
index 377ff7c98..a31969a99 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/deprecated/IgniteCliInterfaceTest.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/deprecated/IgniteCliInterfaceTest.java
@@ -39,6 +39,7 @@ import io.micronaut.context.env.Environment;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
@@ -51,9 +52,11 @@ import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockserver.integration.ClientAndServer;
+import org.mockserver.junit.jupiter.MockServerExtension;
 import org.mockserver.model.MediaType;
 import picocli.CommandLine;
 
@@ -63,6 +66,7 @@ import picocli.CommandLine;
  */
 @DisplayName("ignite")
 @ExtendWith(MockitoExtension.class)
+@ExtendWith(MockServerExtension.class)
 public class IgniteCliInterfaceTest extends AbstractCliTest {
     /** DI application context. */
     ApplicationContext ctx;
@@ -85,9 +89,14 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
             Path.of("log"),
             "version");
 
-    private ClientAndServer clientAndServer;
+    private final ClientAndServer clientAndServer;
 
-    private String mockUrl;
+    private final String mockUrl;
+
+    public IgniteCliInterfaceTest(ClientAndServer clientAndServer) {
+        this.clientAndServer = clientAndServer;
+        mockUrl = "http://localhost:" + clientAndServer.getPort();
+    }
 
     /**
      * Sets up environment before test execution.
@@ -101,8 +110,7 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
         err = new ByteArrayOutputStream();
         out = new ByteArrayOutputStream();
 
-        clientAndServer = ClientAndServer.startClientAndServer(0);
-        mockUrl = "http://localhost:" + clientAndServer.getPort();
+        clientAndServer.reset();
     }
 
     /**
@@ -110,7 +118,6 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
      */
     @AfterEach
     void tearDown() {
-        clientAndServer.stop();
         ctx.stop();
     }
 
@@ -174,7 +181,7 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
             var node =
                     new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
 
-            when(nodeMgr.start(any(), any(), any(), any(), any(), any(), any()))
+            when(nodeMgr.start(any(), any(), any(), any(), any(), any(), any(), any()))
                     .thenReturn(node);
 
             when(cliPathsCfgLdr.loadIgnitePathsOrThrowError())
@@ -192,6 +199,7 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
                     ignitePaths.logDir,
                     ignitePaths.cliPidsDir(),
                     Path.of("conf.json"),
+                    null,
                     ignitePaths.serverJavaUtilLoggingPros(),
                     cli.getOut());
 
@@ -207,6 +215,114 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
             assertThatStderrIsEmpty();
         }
 
+        @Test
+        @DisplayName("start node1 --port 12345")
+        void startCustomPort() {
+            var nodeName = "node1";
+
+            var node = new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
+
+            when(nodeMgr.start(any(), any(), any(), any(), any(), any(), any(), any()))
+                    .thenReturn(node);
+
+            when(cliPathsCfgLdr.loadIgnitePathsOrThrowError())
+                    .thenReturn(ignitePaths);
+
+            int exitCode = execute("node start " + nodeName + " --port 12345");
+
+            assertThatExitCodeMeansSuccess(exitCode);
+
+            ArgumentCaptor<String> configStrCaptor = ArgumentCaptor.forClass(String.class);
+            verify(nodeMgr).start(any(), any(), any(), any(), any(), configStrCaptor.capture(), any(), any());
+
+            assertEqualsIgnoreLineSeparators(
+                    "network{port=12345}",
+                    configStrCaptor.getValue()
+            );
+        }
+
+        @Test
+        @DisplayName("start node1 --config ignite-config.json --port 12345")
+        void startCustomPortOverrideConfigFile() throws URISyntaxException {
+            var nodeName = "node1";
+
+            var node = new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
+
+            when(nodeMgr.start(any(), any(), any(), any(), any(), any(), any(), any()))
+                    .thenReturn(node);
+
+            when(cliPathsCfgLdr.loadIgnitePathsOrThrowError())
+                    .thenReturn(ignitePaths);
+
+            Path configPath = Path.of(IgniteCliInterfaceTest.class.getResource("/ignite-config.json").toURI());
+
+            int exitCode = execute("node start " + nodeName + " --config "
+                    + configPath.toAbsolutePath()
+                    + " --port 12345");
+
+            assertThatExitCodeMeansSuccess(exitCode);
+
+            ArgumentCaptor<String> configStrCaptor = ArgumentCaptor.forClass(String.class);
+            verify(nodeMgr).start(any(), any(), any(), any(), any(), configStrCaptor.capture(), any(), any());
+
+            assertEqualsIgnoreLineSeparators(
+                    "network{port=12345},rest{port=10300}",
+                    configStrCaptor.getValue()
+            );
+        }
+
+        @Test
+        @DisplayName("start node1 --port 12345 --rest-port 12346")
+        void startCustomPortAndRestPort() {
+            var nodeName = "node1";
+
+            var node = new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
+
+            when(nodeMgr.start(any(), any(), any(), any(), any(), any(), any(), any()))
+                    .thenReturn(node);
+
+            when(cliPathsCfgLdr.loadIgnitePathsOrThrowError())
+                    .thenReturn(ignitePaths);
+
+            int exitCode = execute("node start " + nodeName + " --port 12345 --rest-port 12346");
+
+            assertThatExitCodeMeansSuccess(exitCode);
+
+            ArgumentCaptor<String> configStrCaptor = ArgumentCaptor.forClass(String.class);
+            verify(nodeMgr).start(any(), any(), any(), any(), any(), configStrCaptor.capture(), any(), any());
+
+            assertEqualsIgnoreLineSeparators(
+                    "network{port=12345},rest{port=12346}",
+                    configStrCaptor.getValue()
+            );
+        }
+
+        @Test
+        @DisplayName("start node1 --port 12345 --rest-port 12346 --join localhost:12345")
+        void startCustomPortRestPortAndSeedNodes() {
+            var nodeName = "node1";
+
+            var node = new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
+
+            when(nodeMgr.start(any(), any(), any(), any(), any(), any(), any(), any()))
+                    .thenReturn(node);
+
+            when(cliPathsCfgLdr.loadIgnitePathsOrThrowError())
+                    .thenReturn(ignitePaths);
+
+            int exitCode = execute("node start " + nodeName + " --port 12345 --rest-port 12346 --join localhost:12345");
+
+            assertThatExitCodeMeansSuccess(exitCode);
+
+            ArgumentCaptor<String> configStrCaptor = ArgumentCaptor.forClass(String.class);
+            verify(nodeMgr).start(any(), any(), any(), any(), any(), configStrCaptor.capture(), any(), any());
+
+            assertEqualsIgnoreLineSeparators(
+                    "network{nodeFinder{netClusterNodes=[\"localhost:12345\"]},port=12345},rest{port=12346}",
+                    configStrCaptor.getValue()
+            );
+        }
+
         @Test
         @DisplayName("stop node1")
         void stopRunning() {
@@ -613,7 +729,7 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
     }
 
     /**
-     * <em>Assert</em> that {@code expected} and {@code actual} are equal.
+     * <em>Assert</em> that {@code expected} and {@code actual} are equals ignoring differences in line separators.
      *
      * <p>If both are {@code null}, they are considered equal.
      *
@@ -621,7 +737,7 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
      * @param actual Actual result.
      * @see Object#equals(Object)
      */
-    private static void assertOutputEqual(String exp, String actual) {
+    private static void assertEqualsIgnoreLineSeparators(String exp, String actual) {
         assertEquals(
                 exp.lines().collect(toList()),
                 actual.lines().collect(toList())
@@ -629,10 +745,10 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
     }
 
     private void assertOutputEqual(String exp) {
-        assertOutputEqual(exp, out.toString(UTF_8));
+        assertEqualsIgnoreLineSeparators(exp, out.toString(UTF_8));
     }
 
     private void assertErrOutputEqual(String exp) {
-        assertOutputEqual(exp, err.toString(UTF_8));
+        assertEqualsIgnoreLineSeparators(exp, err.toString(UTF_8));
     }
 }
diff --git a/modules/cli/src/test/resources/ignite-config.json b/modules/cli/src/test/resources/ignite-config.json
new file mode 100644
index 000000000..2479437c1
--- /dev/null
+++ b/modules/cli/src/test/resources/ignite-config.json
@@ -0,0 +1,8 @@
+{
+    "network": {
+        "port": 3344
+    },
+    "rest": {
+        "port": 10300
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java
index 79869d208..9d416c67c 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java
@@ -53,7 +53,7 @@ public class IgniteCliRunnerTest {
         Path configPath = Path.of(IgniteCliRunnerTest.class.getResource("/ignite-config.json").toURI());
 
         CompletableFuture<Ignite> ign = IgniteCliRunner.start(
-                "--config", configPath.toAbsolutePath().toString(),
+                "--config-file", configPath.toAbsolutePath().toString(),
                 "--work-dir", workDir.resolve("node").toAbsolutePath().toString(),
                 NODE_NAME
         );
diff --git a/modules/runner/src/main/java/org/apache/ignite/app/IgniteCliRunner.java b/modules/runner/src/main/java/org/apache/ignite/app/IgniteCliRunner.java
index c617a7a48..cb3086f4b 100644
--- a/modules/runner/src/main/java/org/apache/ignite/app/IgniteCliRunner.java
+++ b/modules/runner/src/main/java/org/apache/ignite/app/IgniteCliRunner.java
@@ -27,6 +27,7 @@ import java.util.concurrent.ExecutionException;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgnitionManager;
 import picocli.CommandLine;
+import picocli.CommandLine.Model.ArgGroupSpec;
 
 /**
  * The main entry point for run new Ignite node from CLI toolchain.
@@ -66,12 +67,25 @@ public class IgniteCliRunner {
     public static CompletableFuture<Ignite> start(String... args) {
         CommandSpec spec = CommandSpec.create();
 
-        spec.addOption(
-                OptionSpec
-                        .builder("--config")
-                        .paramLabel("configPath")
-                        .type(Path.class)
-                        .description("Path to node configuration file in HOCON format.")
+        OptionSpec configPath = OptionSpec
+                .builder("--config-file")
+                .paramLabel("configPath")
+                .type(Path.class)
+                .description("Path to node configuration file in HOCON format.")
+                .build();
+
+        OptionSpec configStr = OptionSpec
+                .builder("--config-string")
+                .paramLabel("configStr")
+                .type(String.class)
+                .description("Configuration in HOCON format.")
+                .build();
+
+        spec.addArgGroup(
+                ArgGroupSpec
+                        .builder()
+                        .addArg(configPath)
+                        .addArg(configStr)
                         .build()
         );
 
@@ -102,13 +116,14 @@ public class IgniteCliRunner {
         var parsedArgs = new Args(
                 pr.matchedPositionalValue(0, null),
                 pr.matchedOptionValue("--config", null),
+                pr.matchedOptionValue("--configStr", null),
                 pr.matchedOptionValue("--work-dir", null)
         );
 
         if (parsedArgs.config != null) {
             return IgnitionManager.start(parsedArgs.nodeName, parsedArgs.config.toAbsolutePath(), parsedArgs.nodeWorkDir, null);
         } else {
-            return IgnitionManager.start(parsedArgs.nodeName, null, parsedArgs.nodeWorkDir);
+            return IgnitionManager.start(parsedArgs.nodeName, parsedArgs.configStr, parsedArgs.nodeWorkDir);
         }
     }
 
@@ -122,6 +137,9 @@ public class IgniteCliRunner {
         /** Path to config file. */
         private final Path config;
 
+        /** Config string. */
+        private final String configStr;
+
         /** Path to node work directory. */
         private final Path nodeWorkDir;
 
@@ -131,9 +149,10 @@ public class IgniteCliRunner {
          * @param nodeName Name of the node.
          * @param config   Path to config file.
          */
-        private Args(String nodeName, Path config, Path nodeWorkDir) {
+        private Args(String nodeName, Path config, String configStr, Path nodeWorkDir) {
             this.nodeName = nodeName;
             this.config = config;
+            this.configStr = configStr;
             this.nodeWorkDir = nodeWorkDir;
         }
     }
diff --git a/parent/pom.xml b/parent/pom.xml
index 33233a4a4..8a7de0db1 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -943,6 +943,12 @@
                 <artifactId>mockserver-netty</artifactId>
                 <version>${mock-server.version}</version>
             </dependency>
+
+            <dependency>
+                <groupId>org.mock-server</groupId>
+                <artifactId>mockserver-junit-jupiter</artifactId>
+                <version>${mock-server.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>