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 2020/12/18 22:44:59 UTC
[ignite-3] 01/01: IGNITE-13610 Added initial version of unified CLI
tool. Fixes #4
This is an automated email from the ASF dual-hosted git repository.
sk0x50 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
commit dd017fe9cca88d6165116c3a1ad50f9785813676
Author: Kirill Gusakov <kg...@gmail.com>
AuthorDate: Sat Dec 19 01:44:22 2020 +0300
IGNITE-13610 Added initial version of unified CLI tool. Fixes #4
Signed-off-by: Slava Koptilin <sl...@gmail.com>
---
.gitignore | 1 +
modules/cli-demo/cli-common/pom.xml | 33 ++
.../apache/ignite/cli/common/IgniteCommand.java | 21 +
modules/cli-demo/cli/ignite.sh | 11 +
modules/cli-demo/cli/pom.xml | 234 ++++++++++++
.../apache/ignite/cli/CliPathsConfigLoader.java | 79 ++++
.../java/org/apache/ignite/cli/CliVersionInfo.java | 46 +++
.../java/org/apache/ignite/cli/CommandFactory.java | 36 ++
.../java/org/apache/ignite/cli/ErrorHandler.java | 54 +++
.../org/apache/ignite/cli/HelpFactoryImpl.java | 167 ++++++++
.../org/apache/ignite/cli/IgniteCLIException.java | 30 ++
.../java/org/apache/ignite/cli/IgniteCliApp.java | 30 ++
.../java/org/apache/ignite/cli/IgnitePaths.java | 60 +++
.../org/apache/ignite/cli/InteractiveWrapper.java | 93 +++++
.../src/main/java/org/apache/ignite/cli/Table.java | 99 +++++
.../org/apache/ignite/cli/VersionProvider.java | 39 ++
.../ignite/cli/builtins/SystemPathResolver.java | 46 +++
.../cli/builtins/config/ConfigurationClient.java | 110 ++++++
.../cli/builtins/config/HttpClientFactory.java | 34 ++
.../cli/builtins/init/InitIgniteCommand.java | 141 +++++++
.../cli/builtins/module/MavenArtifactResolver.java | 256 +++++++++++++
.../cli/builtins/module/MavenCoordinates.java | 45 +++
.../ignite/cli/builtins/module/ModuleManager.java | 183 +++++++++
.../ignite/cli/builtins/module/ModuleStorage.java | 120 ++++++
.../ignite/cli/builtins/module/ResolveResult.java | 33 ++
.../builtins/module/StandardModuleDefinition.java | 38 ++
.../ignite/cli/builtins/node/NodeManager.java | 231 +++++++++++
.../ignite/cli/spec/AbstractCommandSpec.java | 28 ++
.../apache/ignite/cli/spec/ConfigCommandSpec.java | 107 ++++++
.../org/apache/ignite/cli/spec/IgniteCliSpec.java | 128 +++++++
.../ignite/cli/spec/InitIgniteCommandSpec.java | 36 ++
.../apache/ignite/cli/spec/ModuleCommandSpec.java | 108 ++++++
.../apache/ignite/cli/spec/NodeCommandSpec.java | 139 +++++++
.../cli/src/main/resources/builtin_modules.conf | 13 +
.../cli/src/main/resources/default-config.xml | 29 ++
.../cli-demo/cli/src/main/resources/logback.xml | 14 +
.../cli/src/main/resources/version.properties | 18 +
.../apache/ignite/cli/IgniteCliInterfaceTest.java | 425 +++++++++++++++++++++
.../demo-module-all/demo-module-cli/pom.xml | 55 +++
.../ignite/snapshot/cli/SnapshotCommand.java | 65 ++++
.../org.apache.ignite.cli.common.IgniteCommand | 1 +
.../cli-demo/demo-module-all/demo-module/pom.xml | 34 ++
.../org/apache/ignite/snapshot/IgniteSnapshot.java | 25 ++
modules/cli-demo/demo-module-all/pom.xml | 56 +++
modules/cli-demo/pom.xml | 90 +++++
pom.xml | 1 +
46 files changed, 3642 insertions(+)
diff --git a/.gitignore b/.gitignore
index 5231862..c3484a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.idea
*.iml
target
+.DS_Store
diff --git a/modules/cli-demo/cli-common/pom.xml b/modules/cli-demo/cli-common/pom.xml
new file mode 100644
index 0000000..add7fab
--- /dev/null
+++ b/modules/cli-demo/cli-common/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ignite-cli-demo</artifactId>
+ <groupId>org.apache.ignite</groupId>
+ <version>3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ignite-cli-common</artifactId>
+
+
+</project>
diff --git a/modules/cli-demo/cli-common/src/main/java/org/apache/ignite/cli/common/IgniteCommand.java b/modules/cli-demo/cli-common/src/main/java/org/apache/ignite/cli/common/IgniteCommand.java
new file mode 100644
index 0000000..b7088db
--- /dev/null
+++ b/modules/cli-demo/cli-common/src/main/java/org/apache/ignite/cli/common/IgniteCommand.java
@@ -0,0 +1,21 @@
+/*
+ * 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.common;
+
+public interface IgniteCommand {
+}
diff --git a/modules/cli-demo/cli/ignite.sh b/modules/cli-demo/cli/ignite.sh
new file mode 100644
index 0000000..64c177b
--- /dev/null
+++ b/modules/cli-demo/cli/ignite.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -o nounset; set -o errexit; set -o pipefail; set -o errtrace; set -o functrace
+
+MYSELF=`which "${0}" 2>/dev/null`
+[ $? -gt 0 -a -f "${0}" ] && MYSELF="./${0}"
+java=java
+if test -n "${JAVA_HOME:-}"; then
+ java="${JAVA_HOME}/bin/java"
+fi
+exec "${java}" ${java_args:-} -jar ${MYSELF} "$@"
+exit 1
diff --git a/modules/cli-demo/cli/pom.xml b/modules/cli-demo/cli/pom.xml
new file mode 100644
index 0000000..e73714f
--- /dev/null
+++ b/modules/cli-demo/cli/pom.xml
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<!--
+ POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-cli-demo</artifactId>
+ <version>3.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>ignite</artifactId>
+
+ <dependencies>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <version>1.2.3</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>2.11.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-cli-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ivy</groupId>
+ <artifactId>ivy</artifactId>
+ <version>2.5.0</version>
+ </dependency>
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli-shell-jline3</artifactId>
+ <version>${picocli.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.fusesource.jansi</groupId>
+ <artifactId>jansi</artifactId>
+ <version>1.18</version>
+ </dependency>
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains</groupId>
+ <artifactId>annotations</artifactId>
+ <version>16.0.3</version>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>5.7.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <version>3.3.3</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.micronaut</groupId>
+ <artifactId>micronaut-inject-java</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.micronaut</groupId>
+ <artifactId>micronaut-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe</groupId>
+ <artifactId>config</artifactId>
+ <version>1.4.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.github.freva</groupId>
+ <artifactId>ascii-table</artifactId>
+ <version>1.1.0</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <version>3.2.0</version>
+
+ <configuration>
+ <finalName>ignite</finalName>
+ <appendAssemblyId>false</appendAssemblyId>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ <archive>
+ <manifest>
+ <mainClass>org.apache.ignite.cli.IgniteCliApp</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <!-- Uncomment to enable incremental compilation -->
+ <!-- <useIncrementalCompilation>false</useIncrementalCompilation> -->
+ <annotationProcessorPaths>
+ <path>
+ <groupId>io.micronaut</groupId>
+ <artifactId>micronaut-inject-java</artifactId>
+ <version>${micronaut.version}</version>
+ </path>
+ <path>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli-codegen</artifactId>
+ <version>${picocli.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ <executions>
+ <execution>
+ <id>test-compile</id>
+ <goals>
+ <goal>testCompile</goal>
+ </goals>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>io.micronaut</groupId>
+ <artifactId>micronaut-inject-java</artifactId>
+ <version>${micronaut.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.22.2</version>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>3.0.0</version>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <configuration>
+ <target>
+ <concat destfile="${project.build.directory}/ignite" binary="true">
+ <filelist dir="${project.build.directory}/"
+ files="../ignite.sh,ignite.jar"/>
+ </concat>
+ <chmod file="${project.build.directory}/ignite" perm="+x"/>
+ </target>
+ </configuration>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>com.akathist.maven.plugins.launch4j</groupId>
+ <artifactId>launch4j-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>l4j-clui</id>
+ <phase>package</phase>
+ <goals>
+ <goal>launch4j</goal>
+ </goals>
+ <configuration>
+ <headerType>console</headerType>
+ <jar>${project.build.directory}/ignite.jar</jar>
+ <outfile>${project.build.directory}/ignite.exe</outfile>
+ <jre>
+ <path>%JAVA_HOME%</path>
+ <minVersion>11</minVersion>
+ </jre>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliPathsConfigLoader.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliPathsConfigLoader.java
new file mode 100644
index 0000000..794086a
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliPathsConfigLoader.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.Properties;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.apache.ignite.cli.builtins.SystemPathResolver;
+
+@Singleton
+public class CliPathsConfigLoader {
+
+ private final SystemPathResolver pathResolver;
+ private final String version;
+
+ @Inject
+ public CliPathsConfigLoader(SystemPathResolver pathResolver,
+ CliVersionInfo cliVersionInfo) {
+ this.pathResolver = pathResolver;
+ this.version = cliVersionInfo.version;
+ }
+
+ public Optional<IgnitePaths> loadIgnitePathsConfig() {
+ return searchConfigPathsFile()
+ .map(f -> CliPathsConfigLoader.readConfigFile(f, version));
+ }
+
+ public IgnitePaths loadIgnitePathsOrThrowError() {
+ Optional<IgnitePaths> ignitePaths = loadIgnitePathsConfig();
+ if (ignitePaths.isPresent())
+ return ignitePaths.get();
+ else
+ throw new IgniteCLIException("To execute node module/node management commands you must run 'init' first");
+ }
+
+ public Optional<File> searchConfigPathsFile() {
+ File homeDirCfg = pathResolver.osHomeDirectoryPath().resolve(".ignitecfg").toFile();
+ if (homeDirCfg.exists())
+ return Optional.of(homeDirCfg);
+
+ return Optional.empty();
+ }
+
+ private static IgnitePaths readConfigFile(File configFile, String version) {
+ try (InputStream inputStream = new FileInputStream(configFile)) {
+ Properties properties = new Properties();
+ properties.load(inputStream);
+ if ((properties.getProperty("bin") == null) || (properties.getProperty("work") == null))
+ throw new IgniteCLIException("Config file has wrong format. " +
+ "It must contain correct paths to bin and work dirs");
+ return new IgnitePaths(Path.of(properties.getProperty("bin")),
+ Path.of(properties.getProperty("work")), version);
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't read config file");
+ }
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliVersionInfo.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliVersionInfo.java
new file mode 100644
index 0000000..95d24b7
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliVersionInfo.java
@@ -0,0 +1,46 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+import javax.inject.Singleton;
+import io.micronaut.core.annotation.Introspected;
+
+@Singleton
+@Introspected
+public class CliVersionInfo {
+
+ public final String version;
+
+ public CliVersionInfo() {
+ try (InputStream inputStream = CliVersionInfo.class.getResourceAsStream("/version.properties")) {
+ Properties prop = new Properties();
+ prop.load(inputStream);
+ version = prop.getProperty("version", "undefined");
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can' read ignite version info");
+ }
+ }
+
+ public CliVersionInfo(String version) {
+ this.version = version;
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CommandFactory.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CommandFactory.java
new file mode 100644
index 0000000..f57e0a8
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CommandFactory.java
@@ -0,0 +1,36 @@
+/*
+ * 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 java.util.Optional;
+import io.micronaut.context.ApplicationContext;
+import picocli.CommandLine;
+
+public class CommandFactory implements CommandLine.IFactory {
+
+ private final ApplicationContext applicationContext;
+
+ public CommandFactory(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+ @Override public <K> K create(Class<K> cls) throws Exception {
+ Optional<K> bean = applicationContext.findOrInstantiateBean(cls);
+ return bean.isPresent() ? bean.get() : CommandLine.defaultFactory().create(cls);// custom factory lookup or instantiation
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java
new file mode 100644
index 0000000..eba44d7
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java
@@ -0,0 +1,54 @@
+/*
+ * 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 javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.spec.IgniteCliSpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+
+public class ErrorHandler implements CommandLine.IExecutionExceptionHandler, CommandLine.IParameterExceptionHandler {
+ Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
+
+ @Inject
+ private ApplicationContext applicationContext;
+
+ @Override public int handleExecutionException(Exception ex, CommandLine cmd,
+ CommandLine.ParseResult parseResult) throws Exception {
+ if (ex instanceof IgniteCLIException)
+ cmd.getErr().println(cmd.getColorScheme().errorText(ex.getMessage()));
+ else
+ logger.error("", ex);
+
+ return cmd.getExitCodeExceptionMapper() != null
+ ? cmd.getExitCodeExceptionMapper().getExitCode(ex)
+ : cmd.getCommandSpec().exitCodeOnExecutionException();
+ }
+
+ @Override public int handleParseException(CommandLine.ParameterException ex, String[] args) {
+ CommandLine cli = ex.getCommandLine();
+
+ cli.getErr().println(cli.getColorScheme().errorText("ERROR: ") + ex.getMessage() + '\n');
+
+ cli.usage(cli.getOut());
+
+ return cli.getCommandSpec().exitCodeOnInvalidInput();
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java
new file mode 100644
index 0000000..a5c40b5
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java
@@ -0,0 +1,167 @@
+package org.apache.ignite.cli;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.ToIntFunction;
+import java.util.stream.Collectors;
+import picocli.CommandLine;
+
+public class HelpFactoryImpl implements CommandLine.IHelpFactory {
+
+ public static final String SECTION_KEY_BANNER = "banner";
+ public static final String SECTION_KEY_SYNOPSIS_EXTENSION = "synopsisExt";
+
+ private static final String[] BANNER = new String[] {
+ " ___ __",
+ " / | ____ ____ _ _____ / /_ ___",
+ " @|red,bold ⣠⣶⣿|@ / /| | / __ \\ / __ `// ___// __ \\ / _ \\",
+ " @|red,bold ⣿⣿⣿⣿|@ / ___ | / /_/ // /_/ // /__ / / / // __/",
+ " @|red,bold ⢠⣿⡏⠈⣿⣿⣿⣿⣷|@ /_/ |_|/ .___/ \\__,_/ \\___//_/ /_/ \\___/",
+ " @|red,bold ⢰⣿⣿⣿⣧⠈⢿⣿⣿⣿⣿⣦|@ /_/",
+ " @|red,bold ⠘⣿⣿⣿⣿⣿⣦⠈⠛⢿⣿⣿⣿⡄|@ ____ _ __ _____",
+ " @|red,bold ⠈⠛⣿⣿⣿⣿⣿⣿⣦⠉⢿⣿⡟|@ / _/____ _ ____ (_)/ /_ ___ |__ /",
+ " @|red,bold ⢰⣿⣶⣀⠈⠙⠿⣿⣿⣿⣿ ⠟⠁|@ / / / __ `// __ \\ / // __// _ \\ /_ <",
+ " @|red,bold ⠈⠻⣿⣿⣿⣿⣷⣤⠙⢿⡟|@ _/ / / /_/ // / / // // /_ / __/ ___/ /",
+ " @|red,bold ⠉⠉⠛⠏⠉|@ /___/ \\__, //_/ /_//_/ \\__/ \\___/ /____/",
+ " /____/\n"};
+
+ @Override public CommandLine.Help create(CommandLine.Model.CommandSpec commandSpec,
+ CommandLine.Help.ColorScheme colorScheme) {
+ commandSpec.usageMessage().sectionKeys(Arrays.asList(
+ SECTION_KEY_BANNER,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS,
+ SECTION_KEY_SYNOPSIS_EXTENSION,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING,
+ CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST));
+
+ var sectionMap = new HashMap<String, CommandLine.IHelpSectionRenderer>();
+
+ boolean hasCommands = !commandSpec.subcommands().isEmpty();
+ boolean hasOptions = commandSpec.options().stream().anyMatch(o -> !o.hidden());
+ boolean hasParameters = !commandSpec.positionalParameters().isEmpty();
+
+ sectionMap.put(SECTION_KEY_BANNER,
+ help -> Arrays
+ .stream(BANNER)
+ .map(CommandLine.Help.Ansi.AUTO::string)
+ .collect(Collectors.joining("\n")) +
+ "\n"
+ );
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER,
+ help -> CommandLine.Help.Ansi.AUTO.string(
+ Arrays.stream(help.commandSpec().version())
+ .collect(Collectors.joining("\n")) + "\n\n")
+ );
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
+ help -> CommandLine.Help.Ansi.AUTO.string("@|bold,green " + help.commandSpec().qualifiedName() +
+ "|@\n " + help.description() + "\n"));
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING,
+ help -> CommandLine.Help.Ansi.AUTO.string("@|bold USAGE|@\n"));
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS,
+ help -> {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(" ");
+ sb.append(help.colorScheme().commandText(help.commandSpec().qualifiedName()));
+
+ if (hasCommands) {
+ sb.append(help.colorScheme().commandText(" <COMMAND>"));
+ }
+ else {
+ if (hasOptions)
+ sb.append(help.colorScheme().optionText(" [OPTIONS]"));
+
+ if (hasParameters) {
+ for (CommandLine.Model.PositionalParamSpec parameter : commandSpec.positionalParameters())
+ sb.append(' ').append(help.colorScheme().parameterText(parameter.paramLabel()));
+ }
+ }
+
+ sb.append("\n\n");
+
+ return sb.toString();
+ });
+
+
+ Optional.ofNullable(commandSpec.usageMessage().sectionMap().get(SECTION_KEY_SYNOPSIS_EXTENSION))
+ .ifPresent(v -> sectionMap.put(SECTION_KEY_SYNOPSIS_EXTENSION, v));
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING,
+ help -> hasParameters ? CommandLine.Help.Ansi.AUTO.string("@|bold REQUIRED PARAMETERS|@\n") : "");
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST, new TableRenderer<>(
+ h -> h.commandSpec().positionalParameters(),
+ p -> p.paramLabel().length(),
+ (h, p) -> h.colorScheme().parameterText(p.paramLabel()),
+ (h, p) -> h.colorScheme().text(p.description()[0])));
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING,
+ help -> hasOptions ? CommandLine.Help.Ansi.AUTO.string("@|bold OPTIONS|@\n") : "");
+
+ if (hasOptions)
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST, new TableRenderer<>(
+ h -> h.commandSpec().options(),
+ o -> o.shortestName().length() + o.paramLabel().length() + 1,
+ (h, o) -> h.colorScheme().optionText(o.shortestName()).concat("=").concat(o.paramLabel()),
+ (h, o) -> h.colorScheme().text(o.description()[0])));
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING,
+ help -> hasCommands ? CommandLine.Help.Ansi.AUTO.string("@|bold COMMANDS|@\n") : "");
+
+ sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST, new TableRenderer<>(
+ h -> h.subcommands().values(),
+ c -> c.commandSpec().name().length(),
+ (h, c) -> h.colorScheme().commandText(c.commandSpec().name()),
+ (h, c) -> h.colorScheme().text(c.description().stripTrailing())));
+ commandSpec.usageMessage().sectionMap(sectionMap);
+ return new CommandLine.Help(commandSpec, colorScheme);
+ }
+
+ private static class TableRenderer<T> implements CommandLine.IHelpSectionRenderer {
+ private final Function<CommandLine.Help, Collection<T>> itemsFunc;
+ private final ToIntFunction<T> nameLenFunc;
+ private final BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> nameFunc;
+ private final BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> descriptionFunc;
+
+ TableRenderer(Function<CommandLine.Help, Collection<T>> itemsFunc, ToIntFunction<T> nameLenFunc,
+ BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> nameFunc, BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> descriptionFunc) {
+ this.itemsFunc = itemsFunc;
+ this.nameLenFunc = nameLenFunc;
+ this.nameFunc = nameFunc;
+ this.descriptionFunc = descriptionFunc;
+ }
+
+ @Override public String render(CommandLine.Help help) {
+ Collection<T> items = itemsFunc.apply(help);
+
+ if (items.isEmpty())
+ return "";
+
+ int len = 2 + items.stream().mapToInt(nameLenFunc).max().getAsInt();
+
+ CommandLine.Help.TextTable table = CommandLine.Help.TextTable.forColumns(help.colorScheme(),
+ new CommandLine.Help.Column(len, 2, CommandLine.Help.Column.Overflow.SPAN),
+ new CommandLine.Help.Column(160 - len, 4, CommandLine.Help.Column.Overflow.WRAP));
+
+ for (T item : items)
+ table.addRowValues(nameFunc.apply(help, item), descriptionFunc.apply(help, item));
+
+ return table.toString() + '\n';
+ }
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCLIException.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCLIException.java
new file mode 100644
index 0000000..1132008
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCLIException.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+public class IgniteCLIException extends RuntimeException {
+ private static final long serialVersionUID = 0L;
+
+ public IgniteCLIException(String message) {
+ super(message);
+ }
+
+ public IgniteCLIException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCliApp.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCliApp.java
new file mode 100644
index 0000000..020e97e
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCliApp.java
@@ -0,0 +1,30 @@
+/*
+ * 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 org.apache.ignite.cli.spec.IgniteCliSpec;
+
+public class IgniteCliApp {
+ public static void main(String... args) {
+ ApplicationContext applicationContext = ApplicationContext.run();
+
+ System.exit(IgniteCliSpec.initCli(applicationContext).execute(args));
+ }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgnitePaths.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgnitePaths.java
new file mode 100644
index 0000000..4a5ac67
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgnitePaths.java
@@ -0,0 +1,60 @@
+/*
+ * 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 java.nio.file.Path;
+
+public class IgnitePaths {
+
+ public final Path binDir;
+ public final Path workDir;
+ private final String version;
+
+ public IgnitePaths(Path binDir, Path workDir, String version) {
+ this.binDir = binDir;
+ this.workDir = workDir;
+ this.version = version;
+ }
+
+
+ public Path cliLibsDir() {
+ return binDir.resolve(version).resolve("cli");
+ }
+
+ public Path libsDir() {
+ return binDir.resolve(version).resolve("libs");
+ }
+
+ public Path cliPidsDir() {
+ return workDir.resolve("cli").resolve("pids");
+ }
+
+ public Path installedModulesFile() {
+ return workDir.resolve("modules.json");
+ }
+
+ public Path serverConfigDir() {
+ return workDir.resolve("config");
+ }
+
+ public Path serverDefaultConfigFile() {
+ return serverConfigDir().resolve("default-config.xml");
+ }
+
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/InteractiveWrapper.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/InteractiveWrapper.java
new file mode 100644
index 0000000..f60c942
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/InteractiveWrapper.java
@@ -0,0 +1,93 @@
+/*
+ * 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 java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.jline.console.SystemRegistry;
+import org.jline.console.impl.SystemRegistryImpl;
+import org.jline.keymap.KeyMap;
+import org.jline.reader.Binding;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.MaskingCallback;
+import org.jline.reader.Parser;
+import org.jline.reader.Reference;
+import org.jline.reader.UserInterruptException;
+import org.jline.reader.impl.DefaultParser;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
+import org.jline.widget.TailTipWidgets;
+import picocli.CommandLine;
+import picocli.shell.jline3.PicocliCommands;
+
+public class InteractiveWrapper {
+
+ public void run(CommandLine cmd) {
+ PicocliCommands picocliCommands = new PicocliCommands(workDir(), cmd) {
+ @Override public Object invoke(CommandSession ses, String cmd, Object... args) throws Exception {
+ return execute(ses, cmd, (String[])args);
+ }
+ };
+
+ Parser parser = new DefaultParser();
+ try (Terminal terminal = TerminalBuilder.builder().build()) {
+ SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, InteractiveWrapper::workDir, null);
+ systemRegistry.setCommandRegistries(picocliCommands);
+
+ LineReader reader = LineReaderBuilder.builder()
+ .terminal(terminal)
+ .completer(systemRegistry.completer())
+ .parser(parser)
+ .variable(LineReader.LIST_MAX, 50) // max tab completion candidates
+ .build();
+
+ TailTipWidgets widgets = new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
+ widgets.enable();
+ KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
+ keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
+
+ String prompt = "ignite> ";
+ String rightPrompt = null;
+
+ String line;
+ while (true) {
+ try {
+ systemRegistry.cleanUp();
+ line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null);
+ systemRegistry.execute(line);
+ } catch (UserInterruptException e) {
+ // Ignore
+ } catch (EndOfFileException e) {
+ return;
+ } catch (Exception e) {
+ systemRegistry.trace(e);
+ }
+ }
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static Path workDir() {
+ return Paths.get(System.getProperty("user.dir"));
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java
new file mode 100644
index 0000000..5a0f6cb
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java
@@ -0,0 +1,99 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import picocli.CommandLine.Help.Ansi.Text;
+import picocli.CommandLine.Help.ColorScheme;
+
+public class Table {
+ private final int indent;
+
+ private final ColorScheme colorScheme;
+
+ private final Collection<Text[]> data = new ArrayList<>();
+
+ private int[] lengths;
+
+ public Table(int indent, ColorScheme colorScheme) {
+ if (indent < 0)
+ throw new IllegalArgumentException("Indent can't be negative.");
+
+ this.indent = indent;
+ this.colorScheme = colorScheme;
+ }
+
+ public void addRow(Object... items) {
+ if (lengths == null) {
+ lengths = new int[items.length];
+ }
+ else if (items.length != lengths.length) {
+ throw new IllegalArgumentException("Wrong number of items.");
+ }
+
+ Text[] row = new Text[items.length];
+
+ for (int i = 0; i < items.length; i++) {
+ Text item = colorScheme.text(items[i].toString());
+
+ row[i] = item;
+
+ lengths[i] = Math.max(lengths[i], item.getCJKAdjustedLength());
+ }
+
+ data.add(row);
+ }
+
+ @Override public String toString() {
+ String indentStr = " ".repeat(indent);
+
+ StringBuilder sb = new StringBuilder();
+
+ for (Text[] row : data) {
+ sb.append(indentStr);
+
+ appendLine(sb);
+ appendRow(sb, row);
+ }
+
+ appendLine(sb);
+
+ return sb.toString();
+ }
+
+ private void appendLine(StringBuilder sb) {
+ for (int length : lengths) {
+ sb.append('+').append("-".repeat(length + 2));
+ }
+
+ sb.append("+\n");
+ }
+
+ private void appendRow(StringBuilder sb, Text[] row) {
+ assert row.length == lengths.length;
+
+ for (int i = 0; i < row.length; i++) {
+ Text item = row[i];
+
+ sb.append("| ").append(item.toString()).append(" ".repeat(lengths[i] + 1 - item.getCJKAdjustedLength()));
+ }
+
+ sb.append("|\n");
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/VersionProvider.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/VersionProvider.java
new file mode 100644
index 0000000..0bb2570
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/VersionProvider.java
@@ -0,0 +1,39 @@
+/*
+ * 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 javax.inject.Inject;
+import javax.inject.Singleton;
+import io.micronaut.core.annotation.Introspected;
+import picocli.CommandLine;
+
+@Singleton
+@Introspected
+public class VersionProvider implements CommandLine.IVersionProvider {
+
+ private final CliVersionInfo cliVersionInfo;
+
+ @Inject
+ public VersionProvider(CliVersionInfo cliVersionInfo) {
+ this.cliVersionInfo = cliVersionInfo;
+ }
+
+ @Override public String[] getVersion() throws Exception {
+ return new String[] { "Apache Ignite CLI ver. " + cliVersionInfo.version};
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/SystemPathResolver.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/SystemPathResolver.java
new file mode 100644
index 0000000..eacef87
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/SystemPathResolver.java
@@ -0,0 +1,46 @@
+/*
+ * 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.builtins;
+
+import java.nio.file.Path;
+import javax.inject.Singleton;
+import io.micronaut.core.annotation.Introspected;
+
+public interface SystemPathResolver {
+
+ Path osHomeDirectoryPath();
+
+ Path osCurrentDirPath();
+
+ /**
+ *
+ */
+ @Singleton
+ @Introspected
+ class DefaultPathResolver implements SystemPathResolver {
+
+ @Override public Path osHomeDirectoryPath() {
+ return Path.of(System.getProperty("user.home"));
+ }
+
+ @Override public Path osCurrentDirPath() {
+ return Path.of(System.getProperty("user.dir"));
+ }
+
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java
new file mode 100644
index 0000000..26a07c0
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java
@@ -0,0 +1,110 @@
+/*
+ * 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.builtins.config;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigRenderOptions;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.jetbrains.annotations.Nullable;
+
+@Singleton
+public class ConfigurationClient {
+
+ private final String GET_URL = "/management/v1/configuration/";
+ private final String SET_URL = "/management/v1/configuration/";
+
+ private final HttpClient httpClient;
+ private final ObjectMapper mapper;
+
+ @Inject
+ public ConfigurationClient(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ mapper = new ObjectMapper();
+ }
+
+ public String get(String host, int port,
+ @Nullable String rawHoconPath) {
+ var request = HttpRequest
+ .newBuilder()
+ .header("Content-Type", "application/json");
+
+ if (rawHoconPath == null)
+ request.uri(URI.create("http://" + host + ":" + port + GET_URL));
+ else
+ request.uri(URI.create("http://" + host + ":" + port + GET_URL +
+ rawHoconPath));
+
+ try {
+ HttpResponse<String> response =
+ httpClient.send(request.build(),
+ HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() == HttpURLConnection.HTTP_OK)
+ return mapper.writerWithDefaultPrettyPrinter()
+ .writeValueAsString(mapper.readValue(response.body(), JsonNode.class));
+ else
+ throw error("Can't get configuration", response);
+ }
+ catch (IOException | InterruptedException e) {
+ throw new IgniteCLIException("Connection issues while trying to send http request");
+ }
+ }
+
+ public String set(String host, int port, String rawHoconData) {
+ var request = HttpRequest
+ .newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(renderJsonFromHocon(rawHoconData)))
+ .header("Content-Type", "application/json")
+ .uri(URI.create("http://" + host + ":" + port + SET_URL))
+ .build();
+
+ try {
+ HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() == HttpURLConnection.HTTP_OK)
+ return "";
+ else
+ throw error("Fail to set configuration", response);
+ }
+ catch (IOException | InterruptedException e) {
+ throw new IgniteCLIException("Connection issues while trying to send http request");
+ }
+ }
+
+ private IgniteCLIException error(String message, HttpResponse<String> response) throws JsonProcessingException {
+ var errorMessage = mapper.writerWithDefaultPrettyPrinter()
+ .writeValueAsString(mapper.readValue(response.body(), JsonNode.class));
+ return new IgniteCLIException(message + "\n\n" + errorMessage);
+ }
+
+ private static String renderJsonFromHocon(String rawHoconData) {
+ return ConfigFactory.parseString(rawHoconData)
+ .root().render(ConfigRenderOptions.concise());
+ }
+
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/HttpClientFactory.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/HttpClientFactory.java
new file mode 100644
index 0000000..896246b
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/HttpClientFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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.builtins.config;
+
+import java.net.http.HttpClient;
+import javax.inject.Singleton;
+import io.micronaut.context.annotation.Factory;
+
+@Factory
+public class HttpClientFactory {
+
+ @Singleton
+ HttpClient httpClient() {
+ return HttpClient
+ .newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .build();
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/init/InitIgniteCommand.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/init/InitIgniteCommand.java
new file mode 100644
index 0000000..c3eef48
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/init/InitIgniteCommand.java
@@ -0,0 +1,141 @@
+/*
+ * 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.builtins.init;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Properties;
+import javax.inject.Inject;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.CliVersionInfo;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.IgnitePaths;
+import org.apache.ignite.cli.builtins.SystemPathResolver;
+import org.apache.ignite.cli.builtins.module.ModuleManager;
+import org.jetbrains.annotations.NotNull;
+
+public class InitIgniteCommand {
+
+ private final SystemPathResolver pathResolver;
+ private final CliVersionInfo cliVersionInfo;
+ private final ModuleManager moduleManager;
+ private final CliPathsConfigLoader cliPathsConfigLoader;
+
+ @Inject
+ public InitIgniteCommand(SystemPathResolver pathResolver, CliVersionInfo cliVersionInfo,
+ ModuleManager moduleManager, CliPathsConfigLoader cliPathsConfigLoader) {
+ this.pathResolver = pathResolver;
+ this.cliVersionInfo = cliVersionInfo;
+ this.moduleManager = moduleManager;
+ this.cliPathsConfigLoader = cliPathsConfigLoader;
+ }
+
+ public void init(PrintWriter out) {
+ moduleManager.setOut(out);
+ Optional<IgnitePaths> ignitePathsOpt = cliPathsConfigLoader.loadIgnitePathsConfig();
+ if (ignitePathsOpt.isEmpty()) {
+ out.println("Init ignite directories...");
+ IgnitePaths ignitePaths = initDirectories(out);
+ out.println("Download and install current ignite version...");
+ installIgnite(ignitePaths);
+ out.println("Init default Ignite configs");
+ initDefaultServerConfigs();
+ out.println();
+ out.println("Apache Ignite version " + cliVersionInfo.version + " sucessfully installed");
+ } else {
+ IgnitePaths cfg = ignitePathsOpt.get();
+ out.println("Apache Ignite was initialized earlier\n" +
+ "Configuration file: " + cliPathsConfigLoader.searchConfigPathsFile().get() + "\n" +
+ "Ignite binaries dir: " + cfg.binDir + "\n" +
+ "Ignite work dir: " + cfg.workDir);
+ }
+ }
+
+ private void initDefaultServerConfigs() {
+ Path serverCfgFile = cliPathsConfigLoader.loadIgnitePathsOrThrowError().serverDefaultConfigFile();
+ try {
+ Files.copy(InitIgniteCommand.class.getResourceAsStream("/default-config.xml"), serverCfgFile);
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't create default config file for server");
+ }
+ }
+
+ private IgnitePaths initDirectories(PrintWriter out) {
+ File cfgFile = initConfigFile();
+ out.println("Configuration file initialized: " + cfgFile);
+ IgnitePaths cfg = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+ out.println("Ignite binaries dir: " + cfg.binDir);
+ out.println("Ignite work dir: " + cfg.workDir);
+
+ File igniteWork = cfg.workDir.toFile();
+ if (!(igniteWork.exists() || igniteWork.mkdirs()))
+ throw new IgniteCLIException("Can't create working directory: " + cfg.workDir);
+
+ File igniteBin = cfg.libsDir().toFile();
+ if (!(igniteBin.exists() || igniteBin.mkdirs()))
+ throw new IgniteCLIException("Can't create a directory for ignite modules: " + cfg.libsDir());
+
+ File igniteBinCli = cfg.cliLibsDir().toFile();
+ if (!(igniteBinCli.exists() || igniteBinCli.mkdirs()))
+ throw new IgniteCLIException("Can't create a directory for cli modules: " + cfg.cliLibsDir());
+
+ File serverConfig = cfg.serverConfigDir().toFile();
+ if (!(serverConfig.exists() || serverConfig.mkdirs()))
+ throw new IgniteCLIException("Can't create a directory for server configs: " + cfg.serverConfigDir());
+
+ return cfg;
+ }
+
+ private void installIgnite(IgnitePaths ignitePaths) {
+ moduleManager.addModule("_server", ignitePaths, Collections.emptyList());
+ }
+
+ private File initConfigFile() {
+ Path newCfgPath = pathResolver.osHomeDirectoryPath().resolve(".ignitecfg");
+ File newCfgFile = newCfgPath.toFile();
+ try {
+ newCfgFile.createNewFile();
+ Path binDir = pathResolver.osCurrentDirPath().resolve("ignite-bin");
+ Path workDir = pathResolver.osCurrentDirPath().resolve("ignite-work");
+ fillNewConfigFile(newCfgFile, binDir, workDir);
+ return newCfgFile;
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't create configuration file in current directory: " + newCfgPath);
+ }
+ }
+
+ private void fillNewConfigFile(File f, @NotNull Path binDir, @NotNull Path workDir) {
+ try (FileWriter fileWriter = new FileWriter(f)) {
+ Properties properties = new Properties();
+ properties.setProperty("bin", binDir.toString());
+ properties.setProperty("work", workDir.toString());
+ properties.store(fileWriter, "");
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't write to ignitecfg file");
+ }
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenArtifactResolver.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenArtifactResolver.java
new file mode 100644
index 0000000..6aa026d
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenArtifactResolver.java
@@ -0,0 +1,256 @@
+/*
+ * 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.builtins.module;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.builtins.SystemPathResolver;
+import org.apache.ivy.Ivy;
+import org.apache.ivy.core.IvyContext;
+import org.apache.ivy.core.event.EventManager;
+import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
+import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
+import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.core.report.ResolveReport;
+import org.apache.ivy.core.resolve.ResolveOptions;
+import org.apache.ivy.core.retrieve.RetrieveOptions;
+import org.apache.ivy.core.retrieve.RetrieveReport;
+import org.apache.ivy.core.settings.IvySettings;
+import org.apache.ivy.plugins.resolver.ChainResolver;
+import org.apache.ivy.plugins.resolver.IBiblioResolver;
+import org.apache.ivy.util.AbstractMessageLogger;
+import org.apache.ivy.util.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+@Singleton
+public class MavenArtifactResolver {
+
+ private final SystemPathResolver pathResolver;
+ private PrintWriter out;
+
+ @Inject
+ public MavenArtifactResolver(SystemPathResolver pathResolver) {
+ this.pathResolver = pathResolver;
+ }
+
+ public void setOut(PrintWriter out) {
+ this.out = out;
+ }
+
+ public ResolveResult resolve(
+ Path mavenRoot,
+ String grpId,
+ String artifactId,
+ String version,
+ List<URL> customRepositories
+ ) throws IOException {
+ Ivy ivy = ivyInstance(customRepositories); // needed for init right output logger before any operations
+ out.print("Resolve artifact " + grpId + ":" +
+ artifactId + ":" + version);
+ ModuleDescriptor md = rootModuleDescriptor(grpId, artifactId, version);
+
+ // Step 1: you always need to resolve before you can retrieve
+ //
+ ResolveOptions ro = new ResolveOptions();
+ // this seems to have no impact, if you resolve by module descriptor
+ //
+ // (in contrast to resolve by ModuleRevisionId)
+ ro.setTransitive(true);
+ // if set to false, nothing will be downloaded
+ ro.setDownload(true);
+
+ try {
+ // now resolve
+ ResolveReport rr = ivy.resolve(md,ro);
+
+ if (rr.hasError())
+ throw new IgniteCLIException(rr.getAllProblemMessages().toString());
+
+ // Step 2: retrieve
+ ModuleDescriptor m = rr.getModuleDescriptor();
+
+ RetrieveReport retrieveReport = ivy.retrieve(
+ m.getModuleRevisionId(),
+ new RetrieveOptions()
+ // this is from the envelop module
+ .setConfs(new String[] {"default"})
+ .setDestArtifactPattern(mavenRoot.toFile().getAbsolutePath() + "/[artifact](-[classifier]).[revision].[ext]")
+ );
+
+
+ return new ResolveResult(
+ retrieveReport.getRetrievedFiles().stream().map(File::toPath).collect(Collectors.toList())
+ );
+ }
+ catch (ParseException e) {
+ // TOOD
+ throw new IOException(e);
+ }
+ finally {
+ out.println();
+ }
+ }
+
+ private Ivy ivyInstance(List<URL> repositories) {
+ File tmpDir = null;
+ try {
+ tmpDir = Files.createTempDirectory("ignite-installer-cache").toFile();
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't create temp directory for ivy");
+ }
+ tmpDir.deleteOnExit();
+
+ EventManager eventManager = new EventManager();
+ eventManager.addIvyListener(event -> {
+ out.print(".");
+ out.flush();
+ });
+
+ IvySettings ivySettings = new IvySettings();
+ ivySettings.setDefaultCache(tmpDir);
+ ivySettings.setDefaultCacheArtifactPattern("[artifact](-[classifier]).[revision].[ext]");
+
+ ChainResolver chainResolver = new ChainResolver();
+ chainResolver.setName("chainResolver");
+ chainResolver.setEventManager(eventManager);
+
+ for (URL repoUrl: repositories) {
+ IBiblioResolver br = new IBiblioResolver();
+ br.setEventManager(eventManager);
+ br.setM2compatible(true);
+ br.setUsepoms(true);
+ br.setRoot(repoUrl.toString());
+ br.setName(repoUrl.getPath());
+ chainResolver.add(br);
+ }
+ // use the biblio resolver, if you consider resolving
+ // POM declared dependencies
+ IBiblioResolver br = new IBiblioResolver();
+ br.setEventManager(eventManager);
+ br.setM2compatible(true);
+ br.setUsepoms(true);
+ br.setName("central");
+
+ chainResolver.add(br);
+
+ IBiblioResolver localBr = new IBiblioResolver();
+ localBr.setEventManager(eventManager);
+ localBr.setM2compatible(true);
+ localBr.setUsepoms(true);
+ localBr.setRoot("file://" + pathResolver.osHomeDirectoryPath().resolve(".m2").resolve("repository/"));
+ localBr.setName("local");
+ chainResolver.add(localBr);
+
+ ivySettings.addResolver(chainResolver);
+ ivySettings.setDefaultResolver(chainResolver.getName());
+
+ Ivy ivy = new Ivy();
+ ivy.getLoggerEngine().setDefaultLogger(new IvyLogger());
+ // needed for setting the message logger before logging info from loading settings
+ IvyContext.getContext().setIvy(ivy);
+ ivy.setSettings(ivySettings);
+ ivy.bind();
+
+ return ivy;
+ }
+
+ private ModuleDescriptor rootModuleDescriptor(String grpId, String artifactId, String version) {
+ // 1st create an ivy module (this always(!) has a "default" configuration already)
+ DefaultModuleDescriptor md = DefaultModuleDescriptor.newDefaultInstance(
+ // give it some related name (so it can be cached)
+ ModuleRevisionId.newInstance(
+ "org.apache.ignite",
+ "installer-envelope",
+ "working"
+ )
+ );
+
+ // 2. add dependencies for what we are really looking for
+ ModuleRevisionId ri = ModuleRevisionId.newInstance(
+ grpId,
+ artifactId,
+ version
+ );
+ // don't go transitive here, if you want the single artifact
+ DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(md, ri, false, true, true);
+
+ // map to master to just get the code jar. See generated ivy module xmls from maven repo
+ // on how configurations are mapped into ivy. Or check
+ // e.g. http://lightguard-jp.blogspot.de/2009/04/ivy-configurations-when-pulling-from.html
+ dd.addDependencyConfiguration("default", "master");
+ dd.addDependencyConfiguration("default", "runtime");
+ dd.addDependencyConfiguration("default", "compile");
+
+ md.addDependency(dd);
+ return md;
+ }
+
+
+ private static class IvyLogger extends AbstractMessageLogger {
+
+ private final Logger logger = LoggerFactory.getLogger(IvyLogger.class);
+
+ @Override protected void doProgress() {
+ // no-op
+ }
+
+ @Override protected void doEndProgress(String msg) {
+ // no-op
+ }
+
+ @Override public void log(String msg, int level) {
+ switch (level) {
+ case Message.MSG_ERR:
+ logger.error(msg);
+ break;
+ case Message.MSG_WARN:
+ logger.warn(msg);
+ break;
+ case Message.MSG_INFO:
+ logger.info(msg);
+ break;
+ case Message.MSG_VERBOSE:
+ logger.debug(msg);
+ break;
+ case Message.MSG_DEBUG:
+ logger.trace(msg);
+ break;
+ }
+ }
+
+ @Override public void rawlog(String msg, int level) {
+ log(msg, level);
+ }
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenCoordinates.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenCoordinates.java
new file mode 100644
index 0000000..a405855
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenCoordinates.java
@@ -0,0 +1,45 @@
+/*
+ * 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.builtins.module;
+
+import org.apache.ignite.cli.IgniteCLIException;
+
+public class MavenCoordinates {
+ public final String groupId;
+ public final String artifactId;
+ public final String version;
+
+ public MavenCoordinates(String groupId, String artifactId, String version) {
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ this.version = version;
+ }
+
+ static MavenCoordinates of(String mvnString) {
+ String[] coords = mvnString.split(":");
+
+ if (coords.length == 4)
+ return new MavenCoordinates(coords[1], coords[2], coords[3]);
+ else
+ throw new IgniteCLIException("Incorrect maven coordinates " + mvnString);
+ }
+
+ static MavenCoordinates of(String mvnString, String version) {
+ return of(mvnString + ":" + version);
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleManager.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleManager.java
new file mode 100644
index 0000000..5add240
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleManager.java
@@ -0,0 +1,183 @@
+/*
+ * 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.builtins.module;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigValue;
+import org.apache.ignite.cli.IgnitePaths;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.CliVersionInfo;
+
+@Singleton
+public class ModuleManager {
+
+ private final MavenArtifactResolver mavenArtifactResolver;
+ private final CliVersionInfo cliVersionInfo;
+ private final ModuleStorage moduleStorage;
+ private final List<StandardModuleDefinition> modules;
+
+ public static final String INTERNAL_MODULE_PREFIX = "_";
+
+ @Inject
+ public ModuleManager(
+ MavenArtifactResolver mavenArtifactResolver, CliVersionInfo cliVersionInfo,
+ ModuleStorage moduleStorage) {
+ modules = readBuiltinModules();
+ this.mavenArtifactResolver = mavenArtifactResolver;
+ this.cliVersionInfo = cliVersionInfo;
+ this.moduleStorage = moduleStorage;
+ }
+
+ public void setOut(PrintWriter out) {
+ mavenArtifactResolver.setOut(out);
+ }
+
+ public void addModule(String name, IgnitePaths ignitePaths, List<URL> repositories) {
+ Path installPath = ignitePaths.libsDir();
+ if (name.startsWith("mvn:")) {
+ MavenCoordinates mavenCoordinates = MavenCoordinates.of(name);
+
+ try {
+ ResolveResult resolveResult = mavenArtifactResolver.resolve(
+ installPath,
+ mavenCoordinates.groupId,
+ mavenCoordinates.artifactId,
+ mavenCoordinates.version,
+ repositories
+ );
+ moduleStorage.saveModule(new ModuleStorage.ModuleDefinition(
+ name,
+ resolveResult.artifacts(),
+ new ArrayList<>(),
+ ModuleStorage.SourceType.Maven,
+ name
+ ));
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Error during resolving maven module " + name, e);
+ }
+
+ }
+ else if (name.startsWith("file://"))
+ throw new RuntimeException("File urls is not implemented yet");
+ else if (isStandardModuleName(name)) {
+ StandardModuleDefinition moduleDescription = readBuiltinModules()
+ .stream()
+ .filter(m -> m.name.equals(name))
+ .findFirst().get();
+ List<ResolveResult> libsResolveResults = new ArrayList<>();
+ for (String artifact: moduleDescription.artifacts) {
+ MavenCoordinates mavenCoordinates = MavenCoordinates.of(artifact, cliVersionInfo.version);
+ try {
+ libsResolveResults.add(mavenArtifactResolver.resolve(
+ ignitePaths.libsDir(),
+ mavenCoordinates.groupId,
+ mavenCoordinates.artifactId,
+ mavenCoordinates.version,
+ repositories
+ ));
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Error during resolving standard module " + name, e);
+ }
+ }
+
+ List<ResolveResult> cliResolvResults = new ArrayList<>();
+ for (String artifact: moduleDescription.cliArtifacts) {
+ MavenCoordinates mavenCoordinates = MavenCoordinates.of(artifact, cliVersionInfo.version);
+ try {
+ cliResolvResults.add(mavenArtifactResolver.resolve(
+ ignitePaths.cliLibsDir(),
+ mavenCoordinates.groupId,
+ mavenCoordinates.artifactId,
+ mavenCoordinates.version,
+ repositories
+ ));
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Error during resolving module " + name, e);
+ }
+ }
+
+ try {
+ moduleStorage.saveModule(new ModuleStorage.ModuleDefinition(
+ name,
+ libsResolveResults.stream().flatMap(r -> r.artifacts().stream()).collect(Collectors.toList()),
+ cliResolvResults.stream().flatMap(r -> r.artifacts().stream()).collect(Collectors.toList()),
+ ModuleStorage.SourceType.Maven,
+ name
+ ));
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Error during saving the installed module info");
+ }
+
+ }
+ else {
+ throw new IgniteCLIException(
+ "Module coordinates for non-standard modules must be started with mvn:|file://");
+ }
+ }
+
+ public boolean removeModule(String name) {
+ try {
+ return moduleStorage.removeModule(name);
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException(
+ "Can't remove module " + name, e);
+ }
+ }
+
+ public List<StandardModuleDefinition> builtinModules() {
+ return modules;
+ }
+
+ private boolean isStandardModuleName(String name) {
+ return readBuiltinModules().stream().anyMatch(m -> m.name.equals(name));
+ }
+
+
+
+ private static List<StandardModuleDefinition> readBuiltinModules() {
+ com.typesafe.config.ConfigObject config = ConfigFactory.load("builtin_modules.conf").getObject("modules");
+ List<StandardModuleDefinition> modules = new ArrayList<>();
+ for (Map.Entry<String, ConfigValue> entry: config.entrySet()) {
+ ConfigObject configObject = (ConfigObject) entry.getValue();
+ modules.add(new StandardModuleDefinition(
+ entry.getKey(),
+ configObject.toConfig().getString("description"),
+ configObject.toConfig().getStringList("artifacts"),
+ configObject.toConfig().getStringList("cli-artifacts")
+ ));
+ }
+ return modules;
+ }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleStorage.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleStorage.java
new file mode 100644
index 0000000..1219327
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleStorage.java
@@ -0,0 +1,120 @@
+/*
+ * 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.builtins.module;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+
+@Singleton
+public class ModuleStorage {
+
+ private final CliPathsConfigLoader cliPathsConfigLoader;
+
+ @Inject
+ public ModuleStorage(CliPathsConfigLoader cliPathsConfigLoader) {
+ this.cliPathsConfigLoader = cliPathsConfigLoader;
+ }
+
+ private Path moduleFile() {
+ return cliPathsConfigLoader.loadIgnitePathsOrThrowError().installedModulesFile();
+ }
+
+ //TODO: write-to-tmp->move approach should be used to prevent file corruption on accidental exit
+ public void saveModule(ModuleDefinition moduleDefinition) throws IOException {
+ ModuleDefinitionsRegistry moduleDefinitionsRegistry = listInstalled();
+ moduleDefinitionsRegistry.modules.add(moduleDefinition);
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.writeValue(moduleFile().toFile(), moduleDefinitionsRegistry);
+ }
+
+ //TODO: write-to-tmp->move approach should be used to prevent file corruption on accidental exit
+ public boolean removeModule(String name) throws IOException {
+ ModuleDefinitionsRegistry moduleDefinitionsRegistry = listInstalled();
+ boolean removed = moduleDefinitionsRegistry.modules.removeIf(m -> m.name.equals(name));
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.writeValue(moduleFile().toFile(), moduleDefinitionsRegistry);
+ return removed;
+ }
+
+ public ModuleDefinitionsRegistry listInstalled() throws IOException {
+ if (!moduleFile().toFile().exists())
+ return new ModuleDefinitionsRegistry(new ArrayList<>());
+ else {
+ ObjectMapper objectMapper = new ObjectMapper();
+ return objectMapper.readValue(
+ moduleFile().toFile(),
+ ModuleDefinitionsRegistry.class);
+ }
+ }
+
+
+ public static class ModuleDefinitionsRegistry {
+ public final List<ModuleDefinition> modules;
+
+ @JsonCreator
+ public ModuleDefinitionsRegistry(
+ @JsonProperty("modules") List<ModuleDefinition> modules) {
+ this.modules = modules;
+ }
+ }
+ public static class ModuleDefinition {
+ public final String name;
+ public final List<Path> artifacts;
+ public final List<Path> cliArtifacts;
+ public final SourceType type;
+ public final String source;
+
+ @JsonCreator
+ public ModuleDefinition(
+ @JsonProperty("name") String name, @JsonProperty("artifacts") List<Path> artifacts,
+ @JsonProperty("cliArtifacts") List<Path> cliArtifacts,
+ @JsonProperty("type") SourceType type, @JsonProperty("source") String source) {
+ this.name = name;
+ this.artifacts = artifacts;
+ this.cliArtifacts = cliArtifacts;
+ this.type = type;
+ this.source = source;
+ }
+
+ @JsonGetter("artifacts")
+ public List<String> getArtifacts() {
+ return artifacts.stream().map(a -> a.toAbsolutePath().toString()).collect(Collectors.toList());
+ }
+
+ @JsonGetter("cliArtifacts")
+ public List<String> getCliArtifacts() {
+ return cliArtifacts.stream().map(a -> a.toAbsolutePath().toString()).collect(Collectors.toList());
+ }
+ }
+
+ public enum SourceType {
+ File,
+ Maven,
+ Standard
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ResolveResult.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ResolveResult.java
new file mode 100644
index 0000000..d058211
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ResolveResult.java
@@ -0,0 +1,33 @@
+/*
+ * 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.builtins.module;
+
+import java.nio.file.Path;
+import java.util.List;
+
+public class ResolveResult {
+ private List<Path> artifacts;
+
+ public ResolveResult(List<Path> artifacts) {
+ this.artifacts = artifacts;
+ }
+
+ public List<Path> artifacts() {
+ return artifacts;
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java
new file mode 100644
index 0000000..0fa3eb5
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java
@@ -0,0 +1,38 @@
+/*
+ * 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.builtins.module;
+
+import java.util.List;
+
+public class StandardModuleDefinition {
+ public final String name;
+ public final String description;
+ public final List<String> artifacts;
+ public final List<String> cliArtifacts;
+
+ public StandardModuleDefinition(String name, String description, List<String> artifacts, List<String> cliArtifacts) {
+ this.name = name;
+ this.description = description;
+ this.artifacts = artifacts;
+ this.cliArtifacts = cliArtifacts;
+ }
+
+ public String toString() {
+ return this.name + ":\t" + this.description;
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/node/NodeManager.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/node/NodeManager.java
new file mode 100644
index 0000000..13afe4c
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/node/NodeManager.java
@@ -0,0 +1,231 @@
+/*
+ * 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.builtins.node;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchService;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.apache.ignite.cli.builtins.module.ModuleStorage;
+import org.apache.ignite.cli.IgniteCLIException;
+
+@Singleton
+public class NodeManager {
+
+ private static final String MAIN_CLASS = "org.apache.ignite.app.IgniteRunner";
+ private static final Duration NODE_START_TIMEOUT = Duration.ofSeconds(30);
+ private static final Duration LOG_FILE_POLL_INTERVAL = Duration.ofMillis(50);
+
+ private final ModuleStorage moduleStorage;
+
+ @Inject
+ public NodeManager(ModuleStorage moduleStorage) {
+ this.moduleStorage = moduleStorage;
+ }
+
+ public RunningNode start(String consistentId,
+ Path workDir, Path pidsDir, Path serverConfig) {
+
+ if (getRunningNodes(workDir, pidsDir).stream().anyMatch(n -> n.consistentId.equals(consistentId)))
+ throw new IgniteCLIException("Node with consistentId " + consistentId + " is already exist");
+ try {
+ Path logFile = logFile(workDir, consistentId);
+ if (Files.exists(logFile))
+ Files.delete(logFile);
+
+ Files.createFile(logFile);
+
+ var commandArgs = Arrays.asList(
+ "java", "-cp", classpath(), MAIN_CLASS,
+ "--config", serverConfig.toAbsolutePath().toString()
+ );
+
+ ProcessBuilder pb = new ProcessBuilder(
+ commandArgs
+ )
+ .redirectError(logFile.toFile())
+ .redirectOutput(logFile.toFile());
+ Process p = pb.start();
+ try {
+ if (!waitForStart("Ignite application started successfully", logFile, NODE_START_TIMEOUT)) {
+ p.destroyForcibly();
+ throw new IgniteCLIException("Node wasn't started during timeout period "
+ + NODE_START_TIMEOUT.toMillis() + "ms");
+ }
+ }
+ catch (InterruptedException|IOException e) {
+ throw new IgniteCLIException("Waiting for node start was failed", e);
+ }
+ createPidFile(consistentId, p.pid(), pidsDir);
+ return new RunningNode(p.pid(), consistentId, logFile);
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't load classpath", e);
+ }
+ }
+
+ // TODO: We need more robust way of checking if node successfully run
+ private static boolean waitForStart(String started, Path file, Duration timeout) throws IOException, InterruptedException {
+ try(WatchService watchService = FileSystems.getDefault().newWatchService()) {
+ file.getParent().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
+ var beginTime = System.currentTimeMillis();
+ while ((System.currentTimeMillis() - beginTime) < timeout.toMillis()) {
+ var key = watchService.poll(LOG_FILE_POLL_INTERVAL.toMillis(), TimeUnit.MILLISECONDS);
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ if (event.kind().equals(StandardWatchEventKinds.ENTRY_MODIFY) &&
+ (((WatchEvent<Path>)event).context().getFileName()).equals(file.getFileName())) {
+ var content = Files.readString(file);
+ if (content.contains(started))
+ return true;
+ else if (content.contains("Exception"))
+ throw new IgniteCLIException("Can't start the node. Read logs for details: " + file);
+ }
+ }
+ key.reset();
+ }
+ }
+ return false;
+ }
+ }
+
+ public String classpath() throws IOException {
+ return moduleStorage.listInstalled().modules.stream()
+ .flatMap(m -> m.artifacts.stream())
+ .map(m -> m.toAbsolutePath().toString())
+ .collect(Collectors.joining(":"));
+ }
+
+ public void createPidFile(String consistentId, long pid,Path pidsDir) {
+ if (!Files.exists(pidsDir)) {
+ if (!pidsDir.toFile().mkdirs())
+ throw new IgniteCLIException("Can't create directory for storing the process pids: " + pidsDir);
+ }
+
+ Path pidPath = pidsDir.resolve(consistentId + "_" + System.currentTimeMillis() + ".pid");
+
+ try (FileWriter fileWriter = new FileWriter(pidPath.toFile())) {
+ fileWriter.write(String.valueOf(pid));
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't write pid file " + pidPath);
+ }
+ }
+
+ public List<RunningNode> getRunningNodes(Path worksDir, Path pidsDir) {
+ if (Files.exists(pidsDir)) {
+ try (Stream<Path> files = Files.find(pidsDir, 1, (f, attrs) -> f.getFileName().toString().endsWith(".pid"))) {
+ return files
+ .map(f -> {
+ long pid = 0;
+ try {
+ pid = Long.parseLong(Files.readAllLines(f).get(0));
+ if (!ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false))
+ return Optional.<RunningNode>empty();
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't parse pid file " + f);
+ }
+ String filename = f.getFileName().toString();
+ if (filename.lastIndexOf("_") == -1)
+ return Optional.<RunningNode>empty();
+ else {
+ String consistentId = filename.substring(0, filename.lastIndexOf("_"));
+ return Optional.of(new RunningNode(pid, consistentId, logFile(worksDir, consistentId)));
+ }
+
+ })
+ .filter(Optional::isPresent)
+ .map(Optional::get).collect(Collectors.toList());
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't find directory with pid files for running nodes " + pidsDir);
+ }
+ }
+ else
+ return Collections.emptyList();
+ }
+
+ public boolean stopWait(String consistentId, Path pidsDir) {
+ if (Files.exists(pidsDir)) {
+ try {
+ List<Path> files = Files.find(pidsDir, 1,
+ (f, attrs) ->
+ f.getFileName().toString().startsWith(consistentId + "_")).collect(Collectors.toList());
+ if (files.size() > 0) {
+ return files.stream().map(f -> {
+ try {
+ long pid = Long.parseLong(Files.readAllLines(f).get(0));
+ boolean result = stopWait(pid);
+ Files.delete(f);
+ return result;
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't read pid file " + f);
+ }
+ }).reduce((a, b) -> a && b).orElse(false);
+ }
+ else
+ throw new IgniteCLIException("Can't find node with consistent id " + consistentId);
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't open directory with pid files " + pidsDir);
+ }
+ }
+ else
+ return false;
+ }
+
+ private boolean stopWait(long pid) {
+ return ProcessHandle
+ .of(pid)
+ .map(ProcessHandle::destroy)
+ .orElse(false);
+ }
+
+ private static Path logFile(Path workDir, String consistentId) {
+ return workDir.resolve(consistentId + ".log");
+ }
+
+ public static class RunningNode {
+
+ public final long pid;
+ public final String consistentId;
+ public final Path logFile;
+
+ public RunningNode(long pid, String consistentId, Path logFile) {
+ this.pid = pid;
+ this.consistentId = consistentId;
+ this.logFile = logFile;
+ }
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java
new file mode 100644
index 0000000..4cc905d
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java
@@ -0,0 +1,28 @@
+/*
+ * 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.spec;
+
+import org.apache.ignite.cli.VersionProvider;
+import picocli.CommandLine;
+import picocli.CommandLine.Model.CommandSpec;
+
+@CommandLine.Command(versionProvider = VersionProvider.class)
+public abstract class AbstractCommandSpec implements Runnable {
+ @CommandLine.Spec
+ protected CommandSpec spec;
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java
new file mode 100644
index 0000000..aa08afc
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java
@@ -0,0 +1,107 @@
+/*
+ * 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.spec;
+
+import javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.builtins.config.ConfigurationClient;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+ name = "config",
+ description = "Inspect and update Ignite cluster configuration.",
+ subcommands = {
+ ConfigCommandSpec.GetConfigCommandSpec.class,
+ ConfigCommandSpec.SetConfigCommandSpec.class
+ }
+)
+public class ConfigCommandSpec extends AbstractCommandSpec {
+
+ @Override public void run() {
+ spec.commandLine().usage(spec.commandLine().getOut());
+ }
+
+ @CommandLine.Command(name = "get", description = "Get current Ignite cluster configuration values.")
+ public static class GetConfigCommandSpec extends AbstractCommandSpec {
+
+ @Inject private ConfigurationClient configurationClient;
+
+ @CommandLine.Mixin CfgHostnameOptions cfgHostnameOptions;
+
+ @CommandLine.Option(names = {"--subtree"},
+ description = "any text representation of hocon for querying considered subtree of config " +
+ "(example: local.baseline)")
+ private String subtree;
+
+ @Override public void run() {
+ spec.commandLine().getOut().println(
+ configurationClient.get(cfgHostnameOptions.host(), cfgHostnameOptions.port(), subtree));
+ }
+ }
+
+ @CommandLine.Command(
+ name = "set",
+ description = "Update Ignite cluster configuration values."
+ )
+ public static class SetConfigCommandSpec extends AbstractCommandSpec {
+
+ @Inject private ConfigurationClient configurationClient;
+
+ @CommandLine.Parameters(paramLabel = "hocon-string", description = "any text representation of hocon config")
+ private String config;
+
+ @CommandLine.Mixin CfgHostnameOptions cfgHostnameOptions;
+
+ @Override public void run() {
+ spec.commandLine().getOut().println(
+ configurationClient
+ .set(cfgHostnameOptions.host(), cfgHostnameOptions.port(), config));
+ }
+ }
+
+ private static class CfgHostnameOptions {
+
+ @CommandLine.Option(names = "--node-endpoint", required = true,
+ description = "host:port of node for configuration")
+ String cfgHostPort;
+
+
+ int port() {
+ var hostPort = parse();
+
+ try {
+ return Integer.parseInt(hostPort[1]);
+ } catch (NumberFormatException ex) {
+ throw new IgniteCLIException("Can't parse port from " + hostPort[1] + " value");
+ }
+ }
+
+ String host() {
+ return parse()[0];
+ }
+
+ private String[] parse() {
+ var hostPort = cfgHostPort.split(":");
+ if (hostPort.length != 2)
+ throw new IgniteCLIException("Incorrect host:port pair provided " +
+ "(example of valid value 'localhost:8080')");
+ return hostPort;
+ }
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java
new file mode 100644
index 0000000..ba25e51
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java
@@ -0,0 +1,128 @@
+/*
+ * 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.spec;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.stream.Collectors;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.CommandFactory;
+import org.apache.ignite.cli.ErrorHandler;
+import org.apache.ignite.cli.HelpFactoryImpl;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.InteractiveWrapper;
+import org.apache.ignite.cli.builtins.module.ModuleStorage;
+import org.apache.ignite.cli.common.IgniteCommand;
+import picocli.CommandLine;
+
+import static org.apache.ignite.cli.HelpFactoryImpl.SECTION_KEY_SYNOPSIS_EXTENSION;
+
+/**
+ *
+ */
+@CommandLine.Command(
+ name = "ignite",
+ description = "Entry point.",
+ subcommands = {
+ InitIgniteCommandSpec.class,
+ ModuleCommandSpec.class,
+ NodeCommandSpec.class,
+ ConfigCommandSpec.class,
+ }
+)
+public class IgniteCliSpec extends AbstractCommandSpec {
+
+ @CommandLine.Option(names = "-i", hidden = true, required = false)
+ boolean interactive;
+
+ @Override public void run() {
+ spec.usageMessage().sectionMap().put(SECTION_KEY_SYNOPSIS_EXTENSION,
+ help -> " Or type " + help.colorScheme().commandText(spec.qualifiedName()) +
+ ' ' + help.colorScheme().parameterText("-i") + " to enter interactive mode.\n\n");
+
+ CommandLine cli = spec.commandLine();
+
+ if (interactive)
+ new InteractiveWrapper().run(cli);
+ else
+ cli.usage(cli.getOut());
+ }
+
+ public static CommandLine initCli(ApplicationContext applicationContext) {
+ CommandLine.IFactory factory = new CommandFactory(applicationContext);
+ ErrorHandler errorHandler = applicationContext.createBean(ErrorHandler.class);
+ CommandLine cli = new CommandLine(IgniteCliSpec.class, factory)
+ .setExecutionExceptionHandler(errorHandler)
+ .setParameterExceptionHandler(errorHandler);
+
+ cli.setHelpFactory(new HelpFactoryImpl());
+
+ cli.setColorScheme(new CommandLine.Help.ColorScheme.Builder()
+ .commands(CommandLine.Help.Ansi.Style.fg_green)
+ .options(CommandLine.Help.Ansi.Style.fg_yellow)
+ .parameters(CommandLine.Help.Ansi.Style.fg_cyan)
+ .errors(CommandLine.Help.Ansi.Style.fg_red, CommandLine.Help.Ansi.Style.bold)
+ .build());
+
+ applicationContext.createBean(CliPathsConfigLoader.class)
+ .loadIgnitePathsConfig()
+ .ifPresent(ignitePaths ->
+ {
+ try {
+ loadSubcommands(
+ cli,
+ applicationContext.createBean(ModuleStorage.class)
+ .listInstalled()
+ .modules
+ .stream()
+ .flatMap(m -> m.cliArtifacts.stream())
+ .collect(Collectors.toList()));
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't load cli modules due to IO error");
+ }
+ }
+ );
+ return cli;
+ }
+
+ public static void loadSubcommands(CommandLine commandLine, List<Path> cliLibs) {
+ URL[] urls = cliLibs.stream()
+ .map(p -> {
+ try {
+ return p.toUri().toURL();
+ }
+ catch (MalformedURLException e) {
+ throw new IgniteCLIException("Can't convert cli module path to URL for loading by classloader");
+ }
+ }).toArray(URL[]::new);
+ ClassLoader classLoader = new URLClassLoader(urls,
+ IgniteCliSpec.class.getClassLoader());
+ ServiceLoader<IgniteCommand> loader = ServiceLoader.load(IgniteCommand.class, classLoader);
+ loader.reload();
+ for (IgniteCommand igniteCommand: loader) {
+ commandLine.addSubcommand(igniteCommand);
+ }
+ }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
new file mode 100644
index 0000000..af1aefd
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
@@ -0,0 +1,36 @@
+/*
+ * 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.spec;
+
+import javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.common.IgniteCommand;
+import org.apache.ignite.cli.builtins.init.InitIgniteCommand;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "init", description = "Install Ignite core modules locally.")
+public class InitIgniteCommandSpec extends AbstractCommandSpec implements IgniteCommand {
+
+ @Inject
+ InitIgniteCommand command;
+
+ @Override public void run() {
+ command.init(spec.commandLine().getOut());
+ }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java
new file mode 100644
index 0000000..ab114ff
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java
@@ -0,0 +1,108 @@
+/*
+ * 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.spec;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.builtins.module.ModuleManager;
+import org.apache.ignite.cli.common.IgniteCommand;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+ name = "module",
+ description = "Manage optional Ignite modules and external artifacts.",
+ subcommands = {
+ ModuleCommandSpec.ListModuleCommandSpec.class,
+ ModuleCommandSpec.AddModuleCommandSpec.class,
+ ModuleCommandSpec.RemoveModuleCommandSpec.class
+ }
+)
+public class ModuleCommandSpec extends AbstractCommandSpec implements IgniteCommand {
+
+ @Override public void run() {
+ spec.commandLine().usage(spec.commandLine().getOut());
+ }
+
+ @CommandLine.Command(name = "add", description = "Add an optional Ignite module or an external artifact.")
+ public static class AddModuleCommandSpec extends AbstractCommandSpec {
+
+ @Inject private ModuleManager moduleManager;
+
+ @Inject
+ private CliPathsConfigLoader cliPathsConfigLoader;
+
+ @CommandLine.Option(names = "--repo",
+ description = "Url to custom maven repo")
+ public URL[] urls;
+
+ @CommandLine.Parameters(paramLabel = "module",
+ description = "can be a 'builtin module name (see module list)'|'mvn:groupId:artifactId:version'")
+ public String moduleName;
+
+ @Override public void run() {
+ var ignitePaths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+ moduleManager.setOut(spec.commandLine().getOut());
+ moduleManager.addModule(moduleName,
+ ignitePaths,
+ (urls == null)? Collections.emptyList() : Arrays.asList(urls));
+ }
+ }
+
+ @CommandLine.Command(name = "remove", description = "Add an optional Ignite module or an external artifact.")
+ public static class RemoveModuleCommandSpec extends AbstractCommandSpec {
+
+ @Inject private ModuleManager moduleManager;
+
+ @CommandLine.Parameters(paramLabel = "module",
+ description = "can be a 'builtin module name (see module list)'|'mvn:groupId:artifactId:version'")
+ public String moduleName;
+
+ @Override public void run() {
+ if (moduleManager.removeModule(moduleName))
+ spec.commandLine().getOut().println("Module " + moduleName + " was removed successfully");
+ else
+ spec.commandLine().getOut().println("Module " + moduleName + " is not found");
+ }
+ }
+
+ @CommandLine.Command(name = "list", description = "Show the list of available optional Ignite modules.")
+ public static class ListModuleCommandSpec extends AbstractCommandSpec {
+
+ @Inject private ModuleManager moduleManager;
+
+ @Override public void run() {
+ var builtinModules = moduleManager.builtinModules()
+ .stream()
+ .filter(m -> !m.name.startsWith(ModuleManager.INTERNAL_MODULE_PREFIX));
+ String table = AsciiTable.getTable(builtinModules.collect(Collectors.toList()), Arrays.asList(
+ new Column().header("Name").dataAlign(HorizontalAlign.LEFT).with(m -> m.name),
+ new Column().header("Description").dataAlign(HorizontalAlign.LEFT).with(m -> m.description)
+ ));
+ spec.commandLine().getOut().println(table);
+ }
+ }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java
new file mode 100644
index 0000000..cc13eac
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java
@@ -0,0 +1,139 @@
+/*
+ * 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.spec;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.IgnitePaths;
+import org.apache.ignite.cli.Table;
+import org.apache.ignite.cli.builtins.node.NodeManager;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+ name = "node",
+ description = "Start, stop and manage locally running Ignite nodes.",
+ subcommands = {
+ NodeCommandSpec.StartNodeCommandSpec.class,
+ NodeCommandSpec.StopNodeCommandSpec.class,
+ NodeCommandSpec.NodesClasspathCommandSpec.class,
+ NodeCommandSpec.ListNodesCommandSpec.class
+ }
+)
+public class NodeCommandSpec extends AbstractCommandSpec {
+
+ @Override public void run() {
+ spec.commandLine().usage(spec.commandLine().getOut());
+ }
+
+ @CommandLine.Command(name = "start", description = "Start an Ignite node locally.")
+ public static class StartNodeCommandSpec extends AbstractCommandSpec {
+
+ @Inject private CliPathsConfigLoader cliPathsConfigLoader;
+
+ @Inject private NodeManager nodeManager;
+
+ @CommandLine.Parameters(paramLabel = "consistent-id", description = "ConsistentId for new node")
+ public String consistentId;
+
+ @CommandLine.Option(names = {"--config"}, required = true,
+ description = "path to configuration file")
+ public Path configPath;
+
+ @Override public void run() {
+ IgnitePaths ignitePaths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+
+ NodeManager.RunningNode node = nodeManager.start(consistentId, ignitePaths.workDir,
+ ignitePaths.cliPidsDir(),
+ configPath);
+
+ spec.commandLine().getOut().println("Started ignite node.\nPID: " + node.pid +
+ "\nConsistent Id: " + node.consistentId + "\nLog file: " + node.logFile);
+ }
+ }
+
+ @CommandLine.Command(name = "stop", description = "Stop a locally running Ignite node.")
+ public static class StopNodeCommandSpec extends AbstractCommandSpec {
+
+ @Inject private NodeManager nodeManager;
+ @Inject private CliPathsConfigLoader cliPathsConfigLoader;
+
+ @CommandLine.Parameters(arity = "1..*", paramLabel = "consistent-ids",
+ description = "consistent ids of nodes to start")
+ public List<String> consistentIds;
+
+ @Override public void run() {
+ IgnitePaths ignitePaths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+
+ consistentIds.forEach(p -> {
+ if (nodeManager.stopWait(p, ignitePaths.cliPidsDir()))
+ spec.commandLine().getOut().println("Node with consistent id " + p + " was stopped");
+ else
+ spec.commandLine().getOut().println("Stop of node " + p + " was failed");
+ });
+ }
+ }
+
+ @CommandLine.Command(name = "list", description = "Show the list of currently running local Ignite nodes.")
+ public static class ListNodesCommandSpec extends AbstractCommandSpec {
+
+ @Inject private NodeManager nodeManager;
+ @Inject private CliPathsConfigLoader cliPathsConfigLoader;
+
+ @Override public void run() {
+ IgnitePaths paths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+
+ List<NodeManager.RunningNode> nodes = nodeManager
+ .getRunningNodes(paths.workDir, paths.cliPidsDir());
+
+ if (nodes.isEmpty())
+ spec.commandLine().getOut().println("No running nodes");
+ else {
+ Table table = new Table(0, spec.commandLine().getColorScheme());
+
+ table.addRow("@|bold PID|@", "@|bold Consistent ID|@", "@|bold Log|@");
+
+ for (NodeManager.RunningNode node : nodes) {
+ table.addRow(node.pid, node.consistentId, node.logFile);
+ }
+
+ spec.commandLine().getOut().println(table);
+ }
+ }
+ }
+
+ @CommandLine.Command(name = "classpath", description = "Show the current classpath used by the Ignite nodes.")
+ public static class NodesClasspathCommandSpec extends AbstractCommandSpec {
+
+ @Inject private NodeManager nodeManager;
+
+ @Override public void run() {
+ try {
+ spec.commandLine().getOut().println(nodeManager.classpath());
+ }
+ catch (IOException e) {
+ throw new IgniteCLIException("Can't get current classpath", e);
+ }
+ }
+ }
+
+}
diff --git a/modules/cli-demo/cli/src/main/resources/builtin_modules.conf b/modules/cli-demo/cli/src/main/resources/builtin_modules.conf
new file mode 100644
index 0000000..c7f4e80
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/builtin_modules.conf
@@ -0,0 +1,13 @@
+modules : {
+ _server: {
+ description: "Collection of base modules, which needed for start ignite server",
+ artifacts: ["mvn:org.apache.ignite:ignite-runner"],
+ cli-artifacts: []
+ },
+
+ ignite-demo-cli {
+ description: "Demo Ignite module with additional cli command 'snapshot'",
+ artifacts: ["mvn:org.apache.ignite:ignite-demo-module"],
+ cli-artifacts: ["mvn:org.apache.ignite:ignite-demo-module-cli"]
+ }
+}
\ No newline at end of file
diff --git a/modules/cli-demo/cli/src/main/resources/default-config.xml b/modules/cli-demo/cli/src/main/resources/default-config.xml
new file mode 100644
index 0000000..5da95ba
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/default-config.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans.xsd">
+ <!--
+ Alter configuration below as needed.
+ -->
+ <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration"/>
+</beans>
diff --git a/modules/cli-demo/cli/src/main/resources/logback.xml b/modules/cli-demo/cli/src/main/resources/logback.xml
new file mode 100644
index 0000000..6a1caa1
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/logback.xml
@@ -0,0 +1,14 @@
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <!-- encoders are assigned the type
+ ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="error">
+ <appender-ref ref="STDOUT" />
+ </root>
+</configuration>
\ No newline at end of file
diff --git a/modules/cli-demo/cli/src/main/resources/version.properties b/modules/cli-demo/cli/src/main/resources/version.properties
new file mode 100644
index 0000000..0d488d3
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/version.properties
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+version=${project.version}
diff --git a/modules/cli-demo/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java b/modules/cli-demo/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java
new file mode 100644
index 0000000..4a8ce62
--- /dev/null
+++ b/modules/cli-demo/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java
@@ -0,0 +1,425 @@
+package org.apache.ignite.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.http.HttpClient;
+import java.net.http.HttpResponse;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.env.Environment;
+import org.apache.ignite.cli.builtins.init.InitIgniteCommand;
+import org.apache.ignite.cli.builtins.module.ModuleManager;
+import org.apache.ignite.cli.builtins.module.StandardModuleDefinition;
+import org.apache.ignite.cli.builtins.node.NodeManager;
+import org.apache.ignite.cli.spec.IgniteCliSpec;
+import org.junit.jupiter.api.BeforeEach;
+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.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@DisplayName("ignite")
+@ExtendWith(MockitoExtension.class)
+public class IgniteCliInterfaceTest {
+
+ ApplicationContext applicationContext;
+ ByteArrayOutputStream err;
+ ByteArrayOutputStream out;
+
+ @Mock CliPathsConfigLoader cliPathsConfigLoader;
+
+ @BeforeEach
+ void setup() {
+ applicationContext = ApplicationContext.run(Environment.TEST);
+ applicationContext.registerSingleton(cliPathsConfigLoader);
+ err = new ByteArrayOutputStream();
+ out = new ByteArrayOutputStream();
+ }
+
+ CommandLine commandLine(ApplicationContext applicationContext) {
+ CommandLine.IFactory factory = new CommandFactory(applicationContext);
+ return new CommandLine(IgniteCliSpec.class, factory)
+ .setErr(new PrintWriter(err, true))
+ .setOut(new PrintWriter(out, true));
+ }
+
+ @DisplayName("init")
+ @Nested
+ class Init {
+
+ @Test
+ @DisplayName("init")
+ void init() {
+ var initIgniteCommand = mock(InitIgniteCommand.class);
+ applicationContext.registerSingleton(InitIgniteCommand.class, initIgniteCommand);
+ assertEquals(0, commandLine(applicationContext).execute("init"));
+ verify(initIgniteCommand).init(any());
+ }
+ }
+
+ @DisplayName("module")
+ @Nested
+ class Module {
+
+ @Mock ModuleManager moduleManager;
+
+ @BeforeEach
+ void setUp() {
+ applicationContext.registerSingleton(moduleManager);
+ }
+
+ @Test
+ @DisplayName("add mvn:groupId:artifact:version")
+ void add() {
+ IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
+ Path.of("worksDir"), "version");
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
+
+ var exitCode =
+ commandLine(applicationContext).execute("module add mvn:groupId:artifactId:version".split(" "));
+ verify(moduleManager).addModule("mvn:groupId:artifactId:version", paths, Arrays.asList());
+ assertEquals(0, exitCode);
+ }
+
+ @Test
+ @DisplayName("add mvn:groupId:artifact:version --repo http://mvnrepo.com/repostiory")
+ void addWithCustomRepo() throws MalformedURLException {
+ doNothing().when(moduleManager).addModule(any(), any(), any());
+
+ IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
+ Path.of("worksDir"), "version");
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
+
+ var exitCode =
+ commandLine(applicationContext)
+ .execute("module add mvn:groupId:artifactId:version --repo http://mvnrepo.com/repostiory".split(" "));
+ verify(moduleManager).addModule("mvn:groupId:artifactId:version", paths,
+ Arrays.asList(new URL("http://mvnrepo.com/repostiory")));
+ assertEquals(0, exitCode);
+ }
+
+ @Test
+ @DisplayName("add demo-module")
+ void addBuiltinModule() {
+ doNothing().when(moduleManager).addModule(any(), any(), any());
+
+ IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
+ Path.of("worksDir"), "version");
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
+
+ var exitCode =
+ commandLine(applicationContext).execute("module add demo-module".split(" "));
+ verify(moduleManager).addModule("demo-module", paths, Collections.emptyList());
+ assertEquals(0, exitCode);
+ }
+
+ @Test
+ @DisplayName("remove builtin-module")
+ void remove() {
+ var moduleName = "builtin-module";
+ when(moduleManager.removeModule(moduleName)).thenReturn(true);
+
+ var exitCode =
+ commandLine(applicationContext).execute("module remove builtin-module".split(" "));
+ verify(moduleManager).removeModule(moduleName);
+ assertEquals(0, exitCode);
+ assertEquals("Module " + moduleName + " was removed successfully\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("remove unknown-module")
+ void removeUnknownModule() {
+ var moduleName = "unknown-module";
+ when(moduleManager.removeModule(moduleName)).thenReturn(false);
+
+ var exitCode =
+ commandLine(applicationContext).execute("module remove unknown-module".split(" "));
+ verify(moduleManager).removeModule(moduleName);
+ assertEquals(0, exitCode);
+ assertEquals("Module " + moduleName + " is not found\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("list")
+ void list() {
+ when(moduleManager.builtinModules()).thenReturn(Arrays.asList(
+ new StandardModuleDefinition("module1", "description1", Arrays.asList("artifact1"), Arrays.asList("cli-artifact1") ),
+ new StandardModuleDefinition("module2", "description2", Arrays.asList("artifact2"), Arrays.asList("cli-artifact2") )
+ ));
+
+ var exitCode =
+ commandLine(applicationContext).execute("module list".split(" "));
+ verify(moduleManager).builtinModules();
+ assertEquals(0, exitCode);
+
+ var expectedOutput =
+ "+---------+--------------+\n" +
+ "| Name | Description |\n" +
+ "+---------+--------------+\n" +
+ "| module1 | description1 |\n" +
+ "+---------+--------------+\n" +
+ "| module2 | description2 |\n" +
+ "+---------+--------------+\n";
+ assertEquals(expectedOutput, out.toString());
+ }
+ }
+
+ @Nested
+ @DisplayName("node")
+ class Node {
+
+ @Mock NodeManager nodeManager;
+
+ @BeforeEach
+ void setUp() {
+ applicationContext.registerSingleton(nodeManager);
+ }
+
+ @Test
+ @DisplayName("start node1 --config conf.json")
+ void start() {
+ var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+ var nodeName = "node1";
+ var node =
+ new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
+ when(nodeManager.start(any(), any(), any(), any()))
+ .thenReturn(node);
+
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+ .thenReturn(ignitePaths);
+
+ var exitCode =
+ commandLine(applicationContext).execute(("node start " + nodeName + " --config conf.json").split(" "));
+
+ assertEquals(0, exitCode);
+ verify(nodeManager).start(nodeName, ignitePaths.workDir, ignitePaths.cliPidsDir(), Path.of("conf.json"));
+ assertEquals("Started ignite node.\n" +
+ "PID: 1\n" +
+ "Consistent Id: node1\n" +
+ "Log file: logfile\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("stop node1")
+ void stopRunning() {
+ var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+ var nodeName = "node1";
+ when(nodeManager.stopWait(any(), any()))
+ .thenReturn(true);
+
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+ .thenReturn(ignitePaths);
+
+ var exitCode =
+ commandLine(applicationContext).execute(("node stop " + nodeName).split(" "));
+
+ assertEquals(0, exitCode);
+ verify(nodeManager).stopWait(nodeName, ignitePaths.cliPidsDir());
+ assertEquals("Node with consistent id " + nodeName + " was stopped\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("stop unknown-node")
+ void stopUnknown() {
+ var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+ var nodeName = "unknown-node";
+ when(nodeManager.stopWait(any(), any()))
+ .thenReturn(false);
+
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+ .thenReturn(ignitePaths);
+
+ var exitCode =
+ commandLine(applicationContext).execute(("node stop " + nodeName).split(" "));
+
+ assertEquals(0, exitCode);
+ verify(nodeManager).stopWait(nodeName, ignitePaths.cliPidsDir());
+ assertEquals("Stop of node " + nodeName + " was failed\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("list")
+ void list() {
+ var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+ when(nodeManager.getRunningNodes(any(), any()))
+ .thenReturn(Arrays.asList(
+ new NodeManager.RunningNode(1, "new1", Path.of("logFile1")),
+ new NodeManager.RunningNode(2, "new2", Path.of("logFile2"))
+ ));
+
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+ .thenReturn(ignitePaths);
+
+ var exitCode =
+ commandLine(applicationContext).execute("node list".split(" "));
+
+ assertEquals(0, exitCode);
+ verify(nodeManager).getRunningNodes(ignitePaths.workDir, ignitePaths.cliPidsDir());
+ assertEquals("+-----+---------------+----------+\n" +
+ "| PID | Consistent ID | Log |\n" +
+ "+-----+---------------+----------+\n" +
+ "| 1 | new1 | logFile1 |\n" +
+ "+-----+---------------+----------+\n" +
+ "| 2 | new2 | logFile2 |\n" +
+ "+-----+---------------+----------+\n\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("list")
+ void listEmpty() {
+ var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+ when(nodeManager.getRunningNodes(any(), any()))
+ .thenReturn(Arrays.asList());
+
+ when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+ .thenReturn(ignitePaths);
+
+ var exitCode =
+ commandLine(applicationContext).execute("node list".split(" "));
+
+ assertEquals(0, exitCode);
+ verify(nodeManager).getRunningNodes(ignitePaths.workDir, ignitePaths.cliPidsDir());
+ assertEquals("No running nodes\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("classpath")
+ void classpath() throws IOException {
+ when(nodeManager.classpath())
+ .thenReturn("classpath");
+
+ var exitCode =
+ commandLine(applicationContext).execute("node classpath".split(" "));
+
+ assertEquals(0, exitCode);
+ verify(nodeManager).classpath();
+ assertEquals("classpath\n", out.toString());
+ }
+ }
+
+ @Nested
+ @DisplayName("config")
+ class Config {
+
+ @Mock private HttpClient httpClient;
+ @Mock private HttpResponse<String> response;
+
+ @BeforeEach
+ void setUp() {
+ applicationContext.registerSingleton(httpClient);
+ }
+
+ @Test
+ @DisplayName("get --node-endpoint localhost:8081")
+ void get() throws IOException, InterruptedException {
+ when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(response.body()).thenReturn("{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}");
+ when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+ var exitCode =
+ commandLine(applicationContext).execute("config get --node-endpoint localhost:8081".split(" "));
+
+ assertEquals(0, exitCode);
+ verify(httpClient).send(
+ argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
+ r.headers().firstValue("Content-Type").get().equals("application/json")),
+ any());
+ assertEquals("{\n" +
+ " \"baseline\" : {\n" +
+ " \"autoAdjust\" : {\n" +
+ " \"enabled\" : true\n" +
+ " }\n" +
+ " }\n" +
+ "}\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("get --node-endpoint localhost:8081 --subtree local.baseline")
+ void getSubtree() throws IOException, InterruptedException {
+ when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(response.body()).thenReturn("{\"autoAdjust\":{\"enabled\":true}}");
+ when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+ var exitCode =
+ commandLine(applicationContext).execute(("config get --node-endpoint localhost:8081 " +
+ "--subtree local.baseline").split(" "));
+
+ assertEquals(0, exitCode);
+ verify(httpClient).send(
+ argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/local.baseline") &&
+ r.headers().firstValue("Content-Type").get().equals("application/json")),
+ any());
+ assertEquals("{\n" +
+ " \"autoAdjust\" : {\n" +
+ " \"enabled\" : true\n" +
+ " }\n" +
+ "}\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("set --node-endpoint localhost:8081 local.baseline.autoAdjust.enabled=true")
+ void setHocon() throws IOException, InterruptedException {
+ when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+ var expectedSentContent = "{\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}";
+
+ var exitCode =
+ commandLine(applicationContext).execute(("config set --node-endpoint localhost:8081 " +
+ "local.baseline.autoAdjust.enabled=true"
+ ).split(" "));
+
+ assertEquals(0, exitCode);
+ verify(httpClient).send(
+ argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
+ r.method().equals("POST") &&
+ // TODO: body matcher should be fixed to more appropriate
+ r.bodyPublisher().get().contentLength() == expectedSentContent.getBytes().length &&
+ r.headers().firstValue("Content-Type").get().equals("application/json")),
+ any());
+ assertEquals("\n", out.toString());
+ }
+
+ @Test
+ @DisplayName("set --node-endpoint localhost:8081 {\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}")
+ void setJson() throws IOException, InterruptedException {
+ when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+ var expectedSentContent = "{\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}";
+
+ var exitCode =
+ commandLine(applicationContext).execute(("config set --node-endpoint localhost:8081 " +
+ "local.baseline.autoAdjust.enabled=true"
+ ).split(" "));
+
+ assertEquals(0, exitCode);
+ verify(httpClient).send(
+ argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
+ r.method().equals("POST") &&
+ // TODO: body matcher should be fixed to more appropriate
+ r.bodyPublisher().get().contentLength() == expectedSentContent.getBytes().length &&
+ r.headers().firstValue("Content-Type").get().equals("application/json")),
+ any());
+ assertEquals("\n", out.toString());
+
+ }
+ }
+}
diff --git a/modules/cli-demo/demo-module-all/demo-module-cli/pom.xml b/modules/cli-demo/demo-module-all/demo-module-cli/pom.xml
new file mode 100644
index 0000000..5904b10
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module-cli/pom.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ignite-cli-demo</artifactId>
+ <groupId>org.apache.ignite</groupId>
+ <version>3.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ignite-demo-module-cli</artifactId>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-cli-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ </build>
+
+
+</project>
diff --git a/modules/cli-demo/demo-module-all/demo-module-cli/src/main/java/org/apache/ignite/snapshot/cli/SnapshotCommand.java b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/java/org/apache/ignite/snapshot/cli/SnapshotCommand.java
new file mode 100644
index 0000000..9b1316c
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/java/org/apache/ignite/snapshot/cli/SnapshotCommand.java
@@ -0,0 +1,65 @@
+/*
+ * 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.snapshot.cli;
+
+import org.apache.ignite.cli.common.IgniteCommand;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+ name = "snapshot",
+ description = "Snapshots management",
+ subcommands = {
+ SnapshotCommand.CreateSnashotCommand.class,
+ SnapshotCommand.CancelSnapshotCommand.class}
+)
+public class SnapshotCommand implements IgniteCommand, Runnable {
+
+ @CommandLine.Spec CommandLine.Model.CommandSpec spec;
+
+ @Override public void run() {
+ spec.commandLine().usage(spec.commandLine().getOut());
+ }
+
+ @CommandLine.Command(
+ name = "create",
+ description = "Create snapshot"
+ )
+ public static class CreateSnashotCommand implements Runnable {
+
+ @CommandLine.Spec CommandLine.Model.CommandSpec spec;
+
+ @Override public void run() {
+ spec.commandLine().getOut().println("Create snapshot command was executed");
+ }
+ }
+
+
+ @CommandLine.Command(
+ name = "cancel",
+ description = "Cancel snapshot"
+ )
+ public static class CancelSnapshotCommand implements Runnable {
+
+ @CommandLine.Spec CommandLine.Model.CommandSpec spec;
+
+ @Override public void run() {
+ spec.commandLine().getOut().println("Cancel snapshot command was executed");
+ }
+
+ }
+}
diff --git a/modules/cli-demo/demo-module-all/demo-module-cli/src/main/resources/META-INF/services/org.apache.ignite.cli.common.IgniteCommand b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/resources/META-INF/services/org.apache.ignite.cli.common.IgniteCommand
new file mode 100644
index 0000000..d916aa9
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/resources/META-INF/services/org.apache.ignite.cli.common.IgniteCommand
@@ -0,0 +1 @@
+org.apache.ignite.snapshot.cli.SnapshotCommand
\ No newline at end of file
diff --git a/modules/cli-demo/demo-module-all/demo-module/pom.xml b/modules/cli-demo/demo-module-all/demo-module/pom.xml
new file mode 100644
index 0000000..c4ea834
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ignite-cli-demo</artifactId>
+ <groupId>org.apache.ignite</groupId>
+ <version>3.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ignite-demo-module</artifactId>
+ <name>ignite-demo-module</name>
+
+
+</project>
diff --git a/modules/cli-demo/demo-module-all/demo-module/src/main/java/org/apache/ignite/snapshot/IgniteSnapshot.java b/modules/cli-demo/demo-module-all/demo-module/src/main/java/org/apache/ignite/snapshot/IgniteSnapshot.java
new file mode 100644
index 0000000..a885054
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module/src/main/java/org/apache/ignite/snapshot/IgniteSnapshot.java
@@ -0,0 +1,25 @@
+/*
+ * 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.snapshot;
+
+public class IgniteSnapshot {
+
+ static {
+ System.out.println("Snapshot module loaded");
+ }
+}
diff --git a/modules/cli-demo/demo-module-all/pom.xml b/modules/cli-demo/demo-module-all/pom.xml
new file mode 100644
index 0000000..1f63a6d
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<!--
+ POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-cli-demo</artifactId>
+ <version>3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>ignite-demo-module-all</artifactId>
+ <version>3.0-SNAPSHOT</version>
+ <url>http://ignite.apache.org</url>
+ <name>ignite-demo-module-all</name>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>demo-module-cli</module>
+ <module>demo-module</module>
+ </modules>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-demo-module</artifactId>
+ <version>3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-demo-module-cli</artifactId>
+ <version>3.0-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/modules/cli-demo/pom.xml b/modules/cli-demo/pom.xml
new file mode 100644
index 0000000..c5236ca
--- /dev/null
+++ b/modules/cli-demo/pom.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-cli-demo</artifactId>
+ <version>3.0-SNAPSHOT</version>
+ <url>http://ignite.apache.org</url>
+ <packaging>pom</packaging>
+ <name>ignite-cli-demo</name>
+
+ <modules>
+ <module>cli-common</module>
+ <module>cli</module>
+ <module>demo-module-all</module>
+ </modules>
+
+ <properties>
+ <micronaut.version>2.1.3</micronaut.version>
+ <picocli.version>4.5.2</picocli.version>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ <version>${picocli.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.micronaut</groupId>
+ <artifactId>micronaut-inject-java</artifactId>
+ <version>${micronaut.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.micronaut</groupId>
+ <artifactId>micronaut-core</artifactId>
+ <version>${micronaut.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <!-- annotationProcessorPaths requires maven-compiler-plugin version 3.5 or higher -->
+ <version>3.8.1</version>
+ <configuration>
+ <source>11</source>
+ <target>11</target>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>io.micronaut</groupId>
+ <artifactId>micronaut-inject-java</artifactId>
+ <version>${micronaut.version}</version>
+ </path>
+ <path>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli-codegen</artifactId>
+ <version>4.5.2</version>
+ </path>
+ </annotationProcessorPaths>
+ <compilerArgs>
+ <arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
+ </compilerArgs>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/pom.xml b/pom.xml
index 64f2265..9e93eef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,5 +41,6 @@
<module>modules/configuration</module>
<module>modules/configuration-annotation-processor</module>
<module>modules/ignite-runner</module>
+ <module>modules/cli-demo</module>
</modules>
</project>