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:58 UTC
[ignite-3] branch master created (now dd017fe)
This is an automated email from the ASF dual-hosted git repository.
sk0x50 pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-3.git.
at dd017fe IGNITE-13610 Added initial version of unified CLI tool. Fixes #4
This branch includes the following new commits:
new dd017fe IGNITE-13610 Added initial version of unified CLI tool. Fixes #4
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
[ignite-3] 01/01: IGNITE-13610 Added initial version of unified CLI
tool. Fixes #4
Posted by sk...@apache.org.
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>