tree: ff28a0da1c076c3045e1ac6ac2f0f08686daf2ee [path history] [tgz]
  1. src/
  2. build.gradle
  3. DEVNOTES.md
  4. ignite.bat
  5. ignite.sh
  6. README.md
modules/cli/README.md

Ignite 3 CLI

Ignite 3 CLI architecture overview

Non-interactive command frontend

Non-interactive (or non-REPL) command is a command that executes under ignite top command. Example of non-interactive command:

> ignite node status --node-url http://localhost:10300

The non-REPL fronted is responsible for:

  • finding the command to execute
  • parsing arguments and options
  • calling the command

Interactive command frontend

Interactive (or REPL, Read-Eval-Print-Loop) mode can be activated by ignite command. Example of interactive command:

> ignite
[diconnected]> node status --node-url http://localhost:10300

The REPL frontend is responsible for:

  • show completions while a user types a command and options
  • after the user presses enter, parse the command and its arguments and options
  • call the command

Common Execution Backend

After the command is found and all options are parsed, the command class (class annotated with @Command) builds a CallExecutionPipeline and runs it.

Call Execution Pipeline

This is where both REPL and non-REPL frontends meet and start to use common code. Call Execution Pipeline consists of:

Flow

For the interactive mode there is a common situation when a user is not connected to any node and executes some command. The user might forget to connect to the node and always type --node-url option instead of connecting to the node once and type only commands. So, it is useful to ask the user if he/she wants to connect to the node with the last --node-url value.

It might be implemented as several checks and read-line operations in every interactive command. To avoid code duplication the Flow was introduced. Flow is a DSL for building user interactions such as dialogs, questions, etc. Flow can be easily integrated with Call. A simplified example of Flow that asks a user to connect to the last connected node on the CLI start:

String clusterUrl = stateConfigProvider.get().getProperty(ConfigConstants.LAST_CONNECTED_URL);
QuestionUiComponent question = QuestionUiComponent.fromQuestion(
        "Do you want to connect to the last connected node %s? %s ",UiElements.url(lastConnectedUrl),UiElements.yesNo()
);

Flows.acceptQuestion(question, ()->new ConnectCallInput(clusterUrl))
        .then(Flows.fromCall(connectCall))
        .print()
        .start(Flowable.empty());

An example of interactive and non-interactive commands that use the common backend:

ignite node config show

@Command(name = "show", description = "Shows node configuration")
public class NodeConfigShowCommand extends BaseCommand implements Callable<Integer> {
    /** Node URL option. */
    @Mixin
    private NodeUrlProfileMixin nodeUrl;

    /** Configuration selector option. */
    @Parameters(arity = "0..1", description = "Configuration path selector")
    private String selector;

    @Inject
    private NodeConfigShowCall call;

    /** {@inheritDoc} */
    @Override
    public Integer call() {
        return CallExecutionPipeline.builder(call)
                .inputProvider(this::buildCallInput)
                .output(spec.commandLine().getOut())
                .errOutput(spec.commandLine().getErr())
                .decorator(new JsonDecorator())
                .build()
                .runPipeline();
    }

    private NodeConfigShowCallInput buildCallInput() {
        return NodeConfigShowCallInput.builder()
                .nodeUrl(nodeUrl.getNodeUrl())
                .selector(selector)
                .build();
    }
}

node config show

@Command(name = "show", description = "Shows node configuration")
public class NodeConfigShowReplCommand extends BaseCommand implements Runnable {
    /** Node URL option. */
    @Mixin
    private NodeUrlMixin nodeUrl;

    /** Configuration selector option. */
    @Parameters(arity = "0..1", description = "Configuration path selector")
    private String selector;

    @Inject
    private NodeConfigShowCall call;

    @Inject
    private ConnectToClusterQuestion question;

    /** {@inheritDoc} */
    @Override
    public void run() {
        question.askQuestionIfNotConnected(nodeUrl.getNodeUrl())
                .map(this::nodeConfigShowCallInput)
                .then(Flows.fromCall(call))
                .print()
                .start();
    }

    private NodeConfigShowCallInput nodeConfigShowCallInput(String nodeUrl) {
        return NodeConfigShowCallInput.builder().selector(selector).nodeUrl(nodeUrl).build();
    }
}

As you can see, both classes use the same NodeConfigShowCall call that performs the logic of fetching the node configuration.

Completions

Completions in non-interactive mode are generated as shell scripts that have to be sourced into the user’s terminal. See generateAutocompletionScript in build.gradle.

But interactive mode is different. There is runtime information that can be used to provide more advanced completions (DynamicCompleter). For example, if a user is connected to the cluster and wants to see a part of cluster configuration using --selector option. The configuration key might be fetched from the cluster and passed as a completion candidate. See HoconDynamicCompleter.

How to

How to run the CLI

./gradlew clean cliDistZip -x test
cd packaging/build/distributions
unzip ignite3-cli-3.0.0-SNAPSHOT.zip
cd ignite3-cli-3.0.0-SNAPSHOT/
./bin/ignite3-cli 

How to develop and test non-interactive command

You can take as an example ignite node status implementation – NodeStatusCommand. As a REST call implementation – NodeStatusCall. To enable the command in CLI, put it as a subcommand to TopLevelCliCommand.

ItClusterStatusCommandInitializedTest is an example of integration test for command class.

How to develop and test interactive command

You can take as an example node status implementation – NodeStatusReplCommand. As a REST call implementation – NodeStatusCall. To enable the command in CLI, put it as a subcommand to TopLevelCliReplCommand.

ItConnectToClusterTest is an example of integration test for interactive command.