This is an automated email from the ASF dual-hosted git repository.
ibessonov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 41208ef9b64 IGNITE-27994 Create CLI command for REST endpoint to edit
cluster name (#7740)
41208ef9b64 is described below
commit 41208ef9b64855aaf69774ce053b0bf248454536
Author: Aditya Mukhopadhyay <[email protected]>
AuthorDate: Tue Mar 24 12:10:41 2026 +0530
IGNITE-27994 Create CLI command for REST endpoint to edit cluster name
(#7740)
---
.../cli/call/rename/ItClusterRenameCallTest.java | 100 +++++++++++++++++++++
.../cli/call/cluster/rename/ClusterRenameCall.java | 56 ++++++++++++
.../cluster/rename/ClusterRenameCallInput.java | 91 +++++++++++++++++++
.../cli/commands/cluster/ClusterCommand.java | 1 +
.../cli/commands/cluster/ClusterNameMixin.java | 47 ++++++++++
.../cli/commands/cluster/ClusterRenameCommand.java | 62 +++++++++++++
.../cli/commands/cluster/ClusterReplCommand.java | 1 +
.../internal/cli/commands/CliCommandTestBase.java | 46 +++++++++-
.../internal/cli/commands/ProfileMixinTest.java | 8 ++
.../cli/commands/UrlOptionsNegativeTest.java | 2 +
.../cluster/rename/ClusterRenameCommandTest.java | 71 +++++++++++++++
.../rest/api/cluster/ClusterManagementApi.java | 2 +
.../cluster/ItClusterManagementControllerTest.java | 26 ++++++
.../rest/cluster/ClusterManagementController.java | 34 +++++--
.../InvalidArgumentClusterRenameException.java | 27 ++++++
...validArgumentClusterRenameExceptionHandler.java | 44 +++++++++
16 files changed, 611 insertions(+), 7 deletions(-)
diff --git
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/rename/ItClusterRenameCallTest.java
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/rename/ItClusterRenameCallTest.java
new file mode 100644
index 00000000000..ed332dadc28
--- /dev/null
+++
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/rename/ItClusterRenameCallTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.call.rename;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import jakarta.inject.Inject;
+import org.apache.ignite.internal.cli.CliIntegrationTest;
+import org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCall;
+import
org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCallInput;
+import org.apache.ignite.internal.cli.call.cluster.status.ClusterStatusCall;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
+import org.apache.ignite.internal.cli.core.call.UrlCallInput;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests for {@link ClusterRenameCall}.
+ */
+public class ItClusterRenameCallTest extends CliIntegrationTest {
+ private static final UrlCallInput URL_CALL_INPUT = new
UrlCallInput(NODE_URL);
+
+ @Inject
+ ClusterRenameCall renameCall;
+
+ @Inject
+ ClusterStatusCall statusCall;
+
+ @ParameterizedTest
+ @DisplayName("Should rename the cluster")
+ @ValueSource(strings = {"cluster2", "cluster name with spaces",
"$p0ng3808!"})
+ public void testRename(String newName) {
+ String name = readClusterName();
+ assertNotEquals(newName, name);
+
+ var input = ClusterRenameCallInput.builder()
+ .clusterUrl(NODE_URL)
+ .name(newName)
+ .build();
+
+ DefaultCallOutput<String> output = renameCall.execute(input);
+ assertFalse(output.hasError());
+ assertThat(output.body()).contains("Cluster was renamed successfully");
+
+ name = readClusterName();
+ assertEquals(newName, name);
+ }
+
+ @ParameterizedTest
+ @DisplayName("Should fail on passing an empty name")
+ @ValueSource(strings = {"", " "})
+ public void testFailOnEmptyName(String name) {
+ var input = ClusterRenameCallInput.builder()
+ .clusterUrl(NODE_URL)
+ .name(name)
+ .build();
+
+ DefaultCallOutput<String> output = renameCall.execute(input);
+ assertTrue(output.hasError());
+ assertThat(output.body()).isNull();
+ }
+
+ @Test
+ @DisplayName("Should fail on passing a NULL name")
+ public void testFailOnNullName() {
+ var input = ClusterRenameCallInput.builder()
+ .clusterUrl(NODE_URL)
+ .name(null)
+ .build();
+
+ DefaultCallOutput<String> output = renameCall.execute(input);
+ assertTrue(output.hasError());
+ assertThat(output.body()).isNull();
+ }
+
+ private String readClusterName() {
+ return statusCall.execute(URL_CALL_INPUT).body().getName();
+ }
+}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/rename/ClusterRenameCall.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/rename/ClusterRenameCall.java
new file mode 100644
index 00000000000..cc8ac317b8b
--- /dev/null
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/rename/ClusterRenameCall.java
@@ -0,0 +1,56 @@
+/*
+ * 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.call.cluster.rename;
+
+import jakarta.inject.Singleton;
+import org.apache.ignite.internal.cli.core.call.Call;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
+import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
+import org.apache.ignite.rest.client.api.ClusterManagementApi;
+import org.apache.ignite.rest.client.invoker.ApiException;
+
+/**
+ * Renames the cluster.
+ */
+@Singleton
+public class ClusterRenameCall implements Call<ClusterRenameCallInput, String>
{
+ private final ApiClientFactory clientFactory;
+
+ public ClusterRenameCall(ApiClientFactory clientFactory) {
+ this.clientFactory = clientFactory;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public DefaultCallOutput<String> execute(ClusterRenameCallInput input) {
+ ClusterManagementApi client = createApiClient(input);
+
+ try {
+ client.rename(input.getName());
+
+ return DefaultCallOutput.success("Cluster was renamed
successfully");
+ } catch (ApiException e) {
+ return DefaultCallOutput.failure(new IgniteCliApiException(e,
input.getClusterUrl()));
+ }
+ }
+
+ private ClusterManagementApi createApiClient(ClusterRenameCallInput input)
{
+ return new
ClusterManagementApi(clientFactory.getClient(input.getClusterUrl()));
+ }
+}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/rename/ClusterRenameCallInput.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/rename/ClusterRenameCallInput.java
new file mode 100644
index 00000000000..0b70a6fa884
--- /dev/null
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/rename/ClusterRenameCallInput.java
@@ -0,0 +1,91 @@
+/*
+ * 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.call.cluster.rename;
+
+import org.apache.ignite.internal.cli.core.call.CallInput;
+
+/**
+ * Input for {@link ClusterRenameCall}.
+ */
+public class ClusterRenameCallInput implements CallInput {
+ /**
+ * New name.
+ */
+ private final String name;
+
+ /**
+ * Cluster url.
+ */
+ private final String clusterUrl;
+
+ private ClusterRenameCallInput(String name, String clusterUrl) {
+ this.name = name;
+ this.clusterUrl = clusterUrl;
+ }
+
+ /**
+ * Builder method.
+ *
+ * @return Builder for {@link ClusterRenameCallInput}.
+ */
+ public static RenameCallInputBuilder builder() {
+ return new RenameCallInputBuilder();
+ }
+
+ /**
+ * Get configuration.
+ *
+ * @return Configuration to update.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Get cluster URL.
+ *
+ * @return Cluster URL.
+ */
+ public String getClusterUrl() {
+ return clusterUrl;
+ }
+
+ /**
+ * Builder for {@link ClusterRenameCallInput}.
+ */
+ public static class RenameCallInputBuilder {
+
+ private String name;
+
+ private String clusterUrl;
+
+ public RenameCallInputBuilder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public RenameCallInputBuilder clusterUrl(String clusterUrl) {
+ this.clusterUrl = clusterUrl;
+ return this;
+ }
+
+ public ClusterRenameCallInput build() {
+ return new ClusterRenameCallInput(name, clusterUrl);
+ }
+ }
+}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterCommand.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterCommand.java
index 0f5e6a0f7f7..9b8cb2df6ff 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterCommand.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterCommand.java
@@ -37,6 +37,7 @@ import picocli.CommandLine.Command;
TopologyCommand.class,
ClusterUnitCommand.class,
ClusterMetricCommand.class,
+ ClusterRenameCommand.class,
},
description = "Manages an Ignite cluster")
public class ClusterCommand extends BaseCommand {
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterNameMixin.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterNameMixin.java
new file mode 100644
index 00000000000..e526ba77831
--- /dev/null
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterNameMixin.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_NAME_OPTION;
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_NAME_OPTION_DESC;
+
+import org.jetbrains.annotations.Nullable;
+import picocli.CommandLine.Option;
+
+/**
+ * Mixin class for cluster name.
+ */
+public class ClusterNameMixin {
+ /** Cluster name option. */
+ @SuppressWarnings("unused")
+ @Option(
+ names = CLUSTER_NAME_OPTION,
+ description = CLUSTER_NAME_OPTION_DESC
+ )
+ private String name;
+
+ /**
+ * Gets cluster name from the command line.
+ *
+ * @return cluster name
+ */
+ @Nullable
+ public String getName() {
+ return name;
+ }
+}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterRenameCommand.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterRenameCommand.java
new file mode 100644
index 00000000000..f8c21b49c98
--- /dev/null
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterRenameCommand.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+import jakarta.inject.Inject;
+import java.util.concurrent.Callable;
+import org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCall;
+import
org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCallInput;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+
+/**
+ * Command that renames the cluster.
+ */
+@Command(name = "rename", description = "Renames the cluster")
+public class ClusterRenameCommand extends BaseCommand implements
Callable<Integer> {
+ /** Cluster endpoint URL option. */
+ @SuppressWarnings("unused")
+ @Mixin
+ private ClusterUrlMixin clusterUrl;
+
+ /** Name that will be updated. */
+ @SuppressWarnings("unused")
+ @Mixin
+ private ClusterNameMixin name;
+
+ @Inject
+ ClusterRenameCall call;
+
+ /** {@inheritDoc} */
+ @Override
+ public Integer call() {
+ return runPipeline(CallExecutionPipeline.builder(call)
+ .input(buildCallInput())
+ .exceptionHandler(createHandler("Cannot update cluster name"))
+ );
+ }
+
+ private ClusterRenameCallInput buildCallInput() {
+ return ClusterRenameCallInput.builder()
+ .clusterUrl(clusterUrl.getClusterUrl())
+ .name(name.getName())
+ .build();
+ }
+}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterReplCommand.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterReplCommand.java
index 2a9f456a1ed..98ba90074f5 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterReplCommand.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterReplCommand.java
@@ -37,6 +37,7 @@ import picocli.CommandLine.Command;
TopologyCommand.class,
ClusterUnitCommand.class,
ClusterMetricCommand.class,
+ ClusterRenameCommand.class,
},
description = "Manages an Ignite cluster")
public class ClusterReplCommand extends BaseCommand {
diff --git
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
index 47ba427771e..b9c28d9d077 100644
---
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
+++
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
@@ -39,9 +39,11 @@ import
io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
+import java.util.regex.Pattern;
import org.apache.ignite.internal.cli.core.call.AsyncCall;
import org.apache.ignite.internal.cli.core.call.AsyncCallFactory;
import org.apache.ignite.internal.cli.core.call.Call;
@@ -52,6 +54,7 @@ import
org.apache.ignite.internal.cli.core.repl.context.CommandLineContextProvid
import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.ArgumentCaptor;
@@ -73,6 +76,10 @@ public abstract class CliCommandTestBase extends
BaseIgniteAbstractTest {
private CommandLine cmd;
+ private final Pattern pattern =
Pattern.compile("\"([^\"]*)\"|'([^']*)'|[^\\s\"']+");
+
+ private final List<Object> mockCallSingletons = new ArrayList<>();
+
@BeforeAll
static void setDumbTerminal() {
System.setProperty("org.jline.terminal.dumb", "true");
@@ -85,14 +92,49 @@ public abstract class CliCommandTestBase extends
BaseIgniteAbstractTest {
createCommand();
}
+ @AfterEach
+ void tearDown() {
+ mockCallSingletons.forEach(mock -> context.destroyBean(mock));
+ mockCallSingletons.clear();
+ }
+
private void createCommand() {
cmd = new CommandLine(getCommandClass(), new
MicronautFactory(context));
cmd.setExecutionExceptionHandler(new
PicocliExecutionExceptionHandler());
CommandLineContextProvider.setCmd(cmd);
}
+ private List<String> getArgs(String argsLine) {
+ var matcher = pattern.matcher(argsLine);
+ List<String> args = new ArrayList<>();
+
+ while (matcher.find()) {
+ if (matcher.group(1) != null) {
+ args.add(matcher.group(1)); // double-quoted content (no
quotes)
+ } else if (matcher.group(2) != null) {
+ args.add(matcher.group(2)); // single-quoted content (no
quotes)
+ } else {
+ args.add(matcher.group(0)); // unquoted token
+ }
+ }
+
+ return args;
+ }
+
protected void execute(String argsLine) {
- execute(argsLine.split(" "));
+ final var args = getArgs(argsLine);
+
+ List<String> joined = new ArrayList<>();
+ for (int i = 0; i < args.size(); i++) {
+ if (args.get(i).endsWith("=") && i + 1 < args.size()) {
+ joined.add(args.get(i) + args.get(i + 1));
+ i++;
+ } else {
+ joined.add(args.get(i));
+ }
+ }
+
+ execute(joined.toArray(new String[0]));
}
protected void execute(String... args) {
@@ -281,6 +323,7 @@ public abstract class CliCommandTestBase extends
BaseIgniteAbstractTest {
private <IT extends CallInput, OT, T extends Call<IT, OT>> T
registerMockCall(Class<T> callClass) {
T mock = mock(callClass);
context.registerSingleton(mock);
+ mockCallSingletons.add(mock);
when(mock.execute(any())).thenReturn(DefaultCallOutput.empty());
return mock;
}
@@ -300,6 +343,7 @@ public abstract class CliCommandTestBase extends
BaseIgniteAbstractTest {
T registerMockCallAsync(Class<FT> callFactoryClass, Class<T>
callClass) {
FT mockCallFactory = mock(callFactoryClass);
context.registerSingleton(mockCallFactory);
+ mockCallSingletons.add(mockCallFactory);
T mockCall = mock(callClass);
when(mockCall.execute(any())).thenReturn(completedFuture(DefaultCallOutput.empty()));
diff --git
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
index 1c3566f7c45..e0375b3a7bb 100644
---
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
+++
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
@@ -28,6 +28,8 @@ import java.util.stream.Stream;
import org.apache.ignite.internal.cli.call.cluster.ClusterInitCall;
import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallFactory;
import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallInput;
+import org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCall;
+import
org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCallInput;
import org.apache.ignite.internal.cli.call.cluster.status.ClusterStatusCall;
import
org.apache.ignite.internal.cli.call.cluster.topology.LogicalTopologyCall;
import
org.apache.ignite.internal.cli.call.cluster.topology.PhysicalTopologyCall;
@@ -268,6 +270,12 @@ public class ProfileMixinTest extends CliCommandTestBase {
ClusterConfigUpdateCallInput.class,
(Function<ClusterConfigUpdateCallInput, String>)
ClusterConfigUpdateCallInput::getClusterUrl
),
+ arguments(
+ "cluster rename --name=cluster2",
+ ClusterRenameCall.class,
+ ClusterRenameCallInput.class,
+ (Function<ClusterRenameCallInput, String>)
ClusterRenameCallInput::getClusterUrl
+ ),
arguments(
"cluster topology physical",
PhysicalTopologyCall.class,
diff --git
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
index c262c612c34..38a5287ec95 100644
---
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
+++
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
@@ -37,6 +37,7 @@ import java.util.ArrayList;
import java.util.List;
import
org.apache.ignite.internal.cli.commands.cliconfig.TestConfigManagerHelper;
import
org.apache.ignite.internal.cli.commands.cliconfig.TestConfigManagerProvider;
+import org.apache.ignite.internal.cli.commands.cluster.ClusterRenameCommand;
import
org.apache.ignite.internal.cli.commands.cluster.config.ClusterConfigShowCommand;
import
org.apache.ignite.internal.cli.commands.cluster.config.ClusterConfigUpdateCommand;
import org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitCommand;
@@ -139,6 +140,7 @@ public class UrlOptionsNegativeTest extends
BaseIgniteAbstractTest {
arguments(NodeStatusCommand.class, NODE_URL_OPTION, List.of()),
arguments(ClusterConfigShowCommand.class, NODE_URL_OPTION,
List.of()),
arguments(ClusterConfigUpdateCommand.class, NODE_URL_OPTION,
List.of("{key: value}")),
+ arguments(ClusterRenameCommand.class, NODE_URL_OPTION,
List.of("--name=cluster2")),
arguments(ClusterStatusCommand.class, NODE_URL_OPTION,
List.of()),
arguments(NodeMetricSourceEnableCommand.class,
NODE_URL_OPTION, List.of("srcName")),
arguments(NodeMetricSourceDisableCommand.class,
NODE_URL_OPTION, List.of("srcName")),
diff --git
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/rename/ClusterRenameCommandTest.java
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/rename/ClusterRenameCommandTest.java
new file mode 100644
index 00000000000..f00064c523c
--- /dev/null
+++
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/rename/ClusterRenameCommandTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.rename;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCall;
+import
org.apache.ignite.internal.cli.call.cluster.rename.ClusterRenameCallInput;
+import org.apache.ignite.internal.cli.commands.CliCommandTestBase;
+import org.apache.ignite.internal.cli.commands.TopLevelCliCommand;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class ClusterRenameCommandTest extends CliCommandTestBase {
+ @Override
+ protected Class<?> getCommandClass() {
+ return TopLevelCliCommand.class;
+ }
+
+ private static Stream<Arguments> names() {
+ return Stream.of(
+ arguments("cluster", "cluster"), // normal
+ arguments("", ""), // empty
+ arguments("!@#!@$#%@#^%#$^#&^#*^#*$&*",
"!@#!@$#%@#^%#$^#&^#*^#*$&*"), // special chars
+ arguments("'cluster'", "cluster"), // single quotes
+ arguments("\"cluster\"", "cluster"), // double quotes
+ arguments("'cluster with spaces'", "cluster with spaces"), //
single quotes with spaces
+ arguments("\"cluster with spaces\"", "cluster with spaces") //
double quotes with spaces
+ );
+ }
+
+ @Test
+ void noParameter() {
+ // When executed without arguments
+ execute("cluster rename");
+
+ assertAll(
+ () -> assertExitCodeIs(1),
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputContains("Missing the required parameter
'body' when calling rename(Async)")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("names")
+ void nameParameterTests(String name, String expectedName) {
+ String parameters = String.format("--name=%s", name);
+
+ checkParameters("cluster rename", ClusterRenameCall.class,
ClusterRenameCallInput.class, ClusterRenameCallInput::getName,
+ parameters, expectedName);
+ }
+}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java
index 25c547e32dc..061b2da170a 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java
@@ -79,6 +79,8 @@ public interface ClusterManagementApi {
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema
= @Schema(implementation = ClusterTag.class)))
@ApiResponse(responseCode = "500", description = "Internal error.",
content = @Content(mediaType = MediaType.PROBLEM_JSON, schema =
@Schema(implementation = Problem.class)))
+ @ApiResponse(responseCode = "400", description = "Invalid name.",
+ content = @Content(mediaType = MediaType.PROBLEM_JSON, schema =
@Schema(implementation = Problem.class)))
@Consumes(MediaType.TEXT_PLAIN)
@Produces({MediaType.APPLICATION_JSON, MediaType.PROBLEM_JSON})
CompletableFuture<ClusterTag> rename(@Body String newName);
diff --git
a/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
index 511f6568d96..196309d432e 100644
---
a/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
+++
b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
@@ -34,6 +34,8 @@ import org.apache.ignite.internal.rest.AbstractRestTestBase;
import org.apache.ignite.internal.rest.api.cluster.ClusterState;
import org.apache.ignite.internal.rest.api.cluster.ClusterTag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Cluster management REST test.
@@ -154,4 +156,28 @@ public class ItClusterManagementControllerTest extends
AbstractRestTestBase {
assertThat(clusterTag2.clusterName(), is("cluster2"));
assertThat(clusterTag2.clusterId(),
is(state.clusterTag().clusterId()));
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ void testClusterRenameFailsWithNoName(String name) {
+ String initRequestBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + " \"" + cluster.nodeName(0) + "\"\n"
+ + " ],\n"
+ + " \"cmgNodes\": [\n"
+ + " \"" + cluster.nodeName(0) + "\"\n"
+ + " ],\n"
+ + " \"clusterName\": \"cluster\"\n"
+ + "}";
+ HttpResponse<String> initResponse =
send(post("/management/v1/cluster/init", initRequestBody));
+ assertThat(initResponse.statusCode(), is(HttpStatus.OK.getCode()));
+ assertThat(cluster.server(0).waitForInitAsync(),
willCompleteSuccessfully());
+
+ HttpRequest renameRequest =
HttpRequest.newBuilder(URI.create(HTTP_HOST_PORT +
"/management/v1/cluster/rename"))
+ .header("content-type", TEXT_PLAIN)
+ .POST(BodyPublishers.ofString(name))
+ .build();
+ HttpResponse<String> renameResponse = send(renameRequest);
+ assertThat(renameResponse.statusCode(),
is(HttpStatus.BAD_REQUEST.getCode()));
+ }
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
index ecc1cc81c07..1f94f573d2c 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.rest.cluster;
import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
import static org.apache.ignite.lang.ErrorGroups.Rest.CLUSTER_NOT_INIT_ERR;
+import static org.jsoup.internal.StringUtil.isBlank;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
@@ -35,6 +36,7 @@ import
org.apache.ignite.internal.rest.api.cluster.ClusterState;
import org.apache.ignite.internal.rest.api.cluster.ClusterTag;
import org.apache.ignite.internal.rest.api.cluster.InitCommand;
import
org.apache.ignite.internal.rest.cluster.exception.InvalidArgumentClusterInitializationException;
+import
org.apache.ignite.internal.rest.cluster.exception.InvalidArgumentClusterRenameException;
import org.apache.ignite.internal.util.ExceptionUtils;
import org.apache.ignite.lang.IgniteException;
import org.jetbrains.annotations.Nullable;
@@ -74,7 +76,7 @@ public class ClusterManagementController implements
ClusterManagementApi, Resour
return clusterManagementGroupManager.clusterState()
.thenApply(ClusterManagementController::mapClusterState)
.exceptionally(ex -> {
- throw mapException(ex);
+ throw mapCause(ExceptionUtils.unwrapCause(ex));
});
}
@@ -93,7 +95,7 @@ public class ClusterManagementController implements
ClusterManagementApi, Resour
.thenCompose(unused -> joinFutureProvider.joinFuture())
.handle((unused, ex) -> {
if (ex != null) {
- throw mapException(ex);
+ throw mapInitException(ex);
}
return null;
});
@@ -103,11 +105,15 @@ public class ClusterManagementController implements
ClusterManagementApi, Resour
public CompletableFuture<ClusterTag> rename(String newName) {
LOG.info("Received rename command with new name = '{}'", newName);
+ if (isBlank(newName)) {
+ return CompletableFuture.failedFuture(mapRenameException(new
IllegalArgumentException("Cluster name must not be empty")));
+ }
+
return clusterManagementGroupManager.renameCluster(newName)
.thenCompose(unused ->
clusterManagementGroupManager.clusterState())
.thenApply(ClusterManagementController::mapClusterTag)
.exceptionally(ex -> {
- throw mapException(ex);
+ throw mapRenameException(ex);
});
}
@@ -141,13 +147,29 @@ public class ClusterManagementController implements
ClusterManagementApi, Resour
);
}
- private static RuntimeException mapException(Throwable ex) {
+ private static RuntimeException mapInitException(Throwable ex) {
+ Throwable cause = ExceptionUtils.unwrapCause(ex);
+
+ if (cause instanceof IllegalArgumentException || cause instanceof
ConfigurationValidationException) {
+ return new InvalidArgumentClusterInitializationException(cause);
+ } else {
+ return mapCause(cause);
+ }
+ }
+
+ private static RuntimeException mapRenameException(Throwable ex) {
Throwable cause = ExceptionUtils.unwrapCause(ex);
+ if (cause instanceof IllegalArgumentException || cause instanceof
ConfigurationValidationException) {
+ return new InvalidArgumentClusterRenameException(cause);
+ } else {
+ return mapCause(cause);
+ }
+ }
+
+ private static RuntimeException mapCause(Throwable cause) {
if (cause instanceof IgniteInternalException) {
return (IgniteInternalException) cause;
- } else if (cause instanceof IllegalArgumentException || cause
instanceof ConfigurationValidationException) {
- return new InvalidArgumentClusterInitializationException(cause);
} else if (cause instanceof IgniteException) {
return (RuntimeException) cause;
} else {
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/InvalidArgumentClusterRenameException.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/InvalidArgumentClusterRenameException.java
new file mode 100644
index 00000000000..0d89f7dd091
--- /dev/null
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/InvalidArgumentClusterRenameException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.rest.cluster.exception;
+
+/**
+ * Exception that is thrown when the wrong arguments are passed to the rename
cluster method.
+ */
+public class InvalidArgumentClusterRenameException extends RuntimeException {
+ public InvalidArgumentClusterRenameException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/InvalidArgumentClusterRenameExceptionHandler.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/InvalidArgumentClusterRenameExceptionHandler.java
new file mode 100644
index 00000000000..11eb822c34c
--- /dev/null
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/InvalidArgumentClusterRenameExceptionHandler.java
@@ -0,0 +1,44 @@
+/*
+ * 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.rest.cluster.exception.handler;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.server.exceptions.ExceptionHandler;
+import jakarta.inject.Singleton;
+import org.apache.ignite.internal.rest.api.Problem;
+import
org.apache.ignite.internal.rest.cluster.exception.InvalidArgumentClusterRenameException;
+import org.apache.ignite.internal.rest.constants.HttpCode;
+import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
+
+/**
+ * Handles {@link InvalidArgumentClusterRenameException} and represents it as
a rest response.
+ */
+@Singleton
+@Requires(classes = {InvalidArgumentClusterRenameException.class,
ExceptionHandler.class})
+public class InvalidArgumentClusterRenameExceptionHandler implements
+ ExceptionHandler<InvalidArgumentClusterRenameException, HttpResponse<?
extends Problem>> {
+
+ @Override
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
InvalidArgumentClusterRenameException exception) {
+ return HttpProblemResponse.from(
+
Problem.fromHttpCode(HttpCode.BAD_REQUEST).detail(exception.getCause().getMessage())
+ );
+ }
+}