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())
+        );
+    }
+}


Reply via email to