This is an automated email from the ASF dual-hosted git repository. mpochatkin pushed a commit to branch IGNITE-23793 in repository https://gitbox.apache.org/repos/asf/ignite-3.git
commit 5b9f602e44886228ebbea78d7d3f02cbf0b162be Author: Mikhail Pochatkin <[email protected]> AuthorDate: Wed Nov 27 18:59:00 2024 +0300 IGNITE-23793 Add exception handler for config option mismatching --- .../cli/call/cluster/ClusterInitCallInput.java | 15 +++++++ .../commands/cluster/init/ClusterInitOptions.java | 32 +++++++++++++- .../cluster/init/ClusterInitReplCommand.java | 28 ++++++++++++- ...seException.java => ConfigAsPathException.java} | 18 ++++++-- .../cluster/init/ConfigAsPathExceptionHandler.java | 49 ++++++++++++++++++++++ .../cluster/init/ConfigFileParseException.java | 5 +++ .../handler/DefaultExceptionHandlers.java | 2 + .../internal/cli/core/flow/builder/Flows.java | 17 ++++++++ 8 files changed, 160 insertions(+), 6 deletions(-) diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCallInput.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCallInput.java index 24444e40c0..c4c882c292 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCallInput.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCallInput.java @@ -125,6 +125,21 @@ public class ClusterInitCallInput implements CallInput { return this; } + public ClusterInitCallInputBuilder metaStorageNodes(List<String> metaStorageNodes) { + this.metaStorageNodes = metaStorageNodes; + return this; + } + + public ClusterInitCallInputBuilder cmgNodes(List<String> cmgNodes) { + this.cmgNodes = cmgNodes; + return this; + } + + public ClusterInitCallInputBuilder clusterName(String clusterName) { + this.clusterName = clusterName; + return this; + } + /** * Extract cluster initialization options. * diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java index e69fe1e8a7..dd5ba6f165 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java @@ -38,6 +38,9 @@ import com.typesafe.config.ConfigRenderOptions; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -138,7 +141,11 @@ public class ClusterInitOptions { if (clusterConfigOptions == null) { return null; } else if (clusterConfigOptions.config != null) { - return clusterConfigOptions.config; + String config = clusterConfigOptions.config; + if (checkConfigAsPath(config)) { + throw new ConfigAsPathException(config); + } + return config; } else if (clusterConfigOptions.files != null) { Config config = ConfigFactory.empty(); @@ -160,6 +167,29 @@ public class ClusterInitOptions { } } + public String readConfigAsPath() { + if (clusterConfigOptions == null || clusterConfigOptions.config == null) { + throw new ConfigFileParseException("Couldn't parse cluster configuration file."); + } + Path file = Paths.get(clusterConfigOptions.config); + try { + String content = Files.readString(file); + return ConfigFactory.parseString(content).root().render(ConfigRenderOptions.concise().setFormatted(true).setJson(true)); + } catch (IOException e) { + throw new IgniteCliException("Couldn't read cluster configuration file " + file, e); + } catch (ConfigException e) { + throw new ConfigFileParseException("Couldn't parse cluster configuration file " + file, e); + } + } + + private static boolean checkConfigAsPath(String config) { + try { + Paths.get(config); + return true; + } catch (InvalidPathException e) { + return false; + } + } private static class DuplicatesChecker { private final String optionToCheck; diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java index b59616ddd6..0cf94f8eff 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.cli.commands.cluster.init; +import static org.apache.ignite.internal.cli.core.style.component.QuestionUiComponent.fromYesNoQuestion; import static picocli.CommandLine.Command; import jakarta.inject.Inject; @@ -25,7 +26,9 @@ import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallInput; import org.apache.ignite.internal.cli.commands.BaseCommand; import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlMixin; import org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion; +import org.apache.ignite.internal.cli.core.flow.builder.FlowBuilder; import org.apache.ignite.internal.cli.core.flow.builder.Flows; +import org.apache.ignite.internal.cli.core.style.component.QuestionUiComponent; import picocli.CommandLine.Mixin; /** @@ -50,12 +53,35 @@ public class ClusterInitReplCommand extends BaseCommand implements Runnable { @Override public void run() { runFlow(question.askQuestionIfNotConnected(clusterUrl.getClusterUrl()) - .map(this::buildCallInput) + .then(askQuestionIfConfigIsPath().build()) .then(Flows.fromCall(call)) .print() ); } + + private FlowBuilder<String, ClusterInitCallInput> askQuestionIfConfigIsPath() { + try { + clusterInitOptions.clusterConfiguration(); + return Flows.from(this::buildCallInput); + } catch (ConfigAsPathException e) { + QuestionUiComponent questionUiComponent = fromYesNoQuestion( + "It seems that you passed the path to the config file to the config content option. " + + "Do you want this file to be read as a config?" + ); + + return Flows.acceptQuestion(questionUiComponent, + clusterUrl -> ClusterInitCallInput.builder() + .clusterConfiguration(clusterInitOptions.readConfigAsPath()) + .cmgNodes(clusterInitOptions.cmgNodes()) + .metaStorageNodes(clusterInitOptions.metaStorageNodes()) + .clusterName(clusterInitOptions.clusterName()) + .clusterUrl(clusterUrl) + .build() + ); + } + } + private ClusterInitCallInput buildCallInput(String clusterUrl) { return ClusterInitCallInput.builder() .clusterUrl(clusterUrl) diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigFileParseException.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigAsPathException.java similarity index 69% copy from modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigFileParseException.java copy to modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigAsPathException.java index c551bbb7b6..1eda6e210f 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigFileParseException.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigAsPathException.java @@ -17,9 +17,19 @@ package org.apache.ignite.internal.cli.commands.cluster.init; -/** Exception thrown when config file parse failed. */ -public class ConfigFileParseException extends RuntimeException { - public ConfigFileParseException(String message, Throwable cause) { - super(message, cause); +/** + * Exception throws when config file path passed to config content option. + */ +public class ConfigAsPathException extends RuntimeException { + private static final long serialVersionUID = -7683264630128999379L; + + private final String path; + + public ConfigAsPathException(String path) { + this.path = path; + } + + public String path() { + return path; } } diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigAsPathExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigAsPathExceptionHandler.java new file mode 100644 index 0000000000..adfb4bd0b5 --- /dev/null +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigAsPathExceptionHandler.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.cli.commands.cluster.init; + +import static org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_CONFIG_FILE_OPTION; + +import org.apache.ignite.internal.cli.core.exception.ExceptionHandler; +import org.apache.ignite.internal.cli.core.exception.ExceptionWriter; +import org.apache.ignite.internal.cli.core.style.component.ErrorUiComponent; +import org.apache.ignite.internal.cli.core.style.element.UiElements; + +/** + * Handler for {@link ConfigAsPathException}. + */ +public class ConfigAsPathExceptionHandler implements ExceptionHandler<ConfigAsPathException> { + @Override + public int handle(ExceptionWriter err, ConfigAsPathException e) { + err.write( + ErrorUiComponent.builder() + .header(String.format("Failed to parse configuration file." + + " Looks like config file path passed." + + " Did you mean %s option?", + UiElements.command(CLUSTER_CONFIG_FILE_OPTION))) + .build().render() + ); + + return 1; + } + + @Override + public Class<ConfigAsPathException> applicableException() { + return ConfigAsPathException.class; + } +} diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigFileParseException.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigFileParseException.java index c551bbb7b6..631130ac14 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigFileParseException.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ConfigFileParseException.java @@ -19,6 +19,11 @@ package org.apache.ignite.internal.cli.commands.cluster.init; /** Exception thrown when config file parse failed. */ public class ConfigFileParseException extends RuntimeException { + public ConfigFileParseException(String message) { + super(message); + } + + public ConfigFileParseException(String message, Throwable cause) { super(message, cause); } diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/DefaultExceptionHandlers.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/DefaultExceptionHandlers.java index 983949c9c2..f52af34b36 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/DefaultExceptionHandlers.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/DefaultExceptionHandlers.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.cli.core.exception.handler; +import org.apache.ignite.internal.cli.commands.cluster.init.ConfigAsPathExceptionHandler; import org.apache.ignite.internal.cli.commands.cluster.init.ConfigParseExceptionHandler; import org.apache.ignite.internal.cli.core.exception.ExceptionHandlers; @@ -41,5 +42,6 @@ public final class DefaultExceptionHandlers extends ExceptionHandlers { addExceptionHandler(new UnitAlreadyExistsExceptionHandler()); addExceptionHandler(new FileNotFoundExceptionHandler()); addExceptionHandler(new ConfigParseExceptionHandler()); + addExceptionHandler(new ConfigAsPathExceptionHandler()); } } diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/flow/builder/Flows.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/flow/builder/Flows.java index 08ccf949d7..e03b3bafc0 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/flow/builder/Flows.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/flow/builder/Flows.java @@ -171,6 +171,23 @@ public final class Flows { return acceptQuestion(question.render(), onAccept); } + /** + * Create new {@link FlowBuilder} which starts from yes/no question and pass the result of the {@code onAccept} + * call on positive answer or interrupts the flow on negative answer. + * + * @param question question UI component. + * @param onAccept mapping function. + * @param <I> input type. + * @param <O> output type. + * @return new {@link FlowBuilder}. + */ + public static <I, O> FlowBuilder<I, O> acceptQuestion(QuestionUiComponent question, Function<I, O> onAccept) { + return question(question.render(), + List.of(new AcceptedQuestionAnswer<>((a, i) -> onAccept.apply(i)), + new InterruptQuestionAnswer<>()) + ); + } + /** * Create new {@link Flow} which asks questions and returns the result of answer. *
