This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit b773716a6d561e030a27a29df7eb75b3e9627c96
Author: lizhimins <[email protected]>
AuthorDate: Mon Jul 6 09:26:00 2026 +0800

    feat: implement backend cluster management module
    
    - Cluster/Broker CRUD (ClusterController, ClusterService)
    - Proxy management (ProxyController)
    - NameServer operations (NameServerController)
    - K8s certificate management (K8sCertController)
    - Client connection monitoring (ClientController)
    - Metrics collection from Prometheus (MetricsController)
    - In-memory repository implementations
    - Unit tests for all services and controllers
---
 .../rocketmq/studio/cluster/broker/BrokerVO.java   |  37 +++
 .../studio/cluster/broker/ClusterController.java   |  64 +++++
 .../studio/cluster/broker/ClusterProvider.java     |  27 +++
 .../studio/cluster/broker/ClusterProviderStub.java |  85 +++++++
 .../studio/cluster/broker/ClusterRepository.java   |  31 +++
 .../cluster/broker/ClusterRepositoryImpl.java      | 192 +++++++++++++++
 .../studio/cluster/broker/ClusterService.java      | 157 ++++++++++++
 .../rocketmq/studio/cluster/broker/ClusterVO.java  |  53 ++++
 .../studio/cluster/client/ClientConnectionVO.java  |  43 ++++
 .../studio/cluster/client/ClientController.java    |  41 ++++
 .../studio/cluster/client/ClientProvider.java      |  24 ++
 .../studio/cluster/client/ClientProviderStub.java  |  78 ++++++
 .../studio/cluster/client/ClientService.java       |  36 +++
 .../studio/cluster/config/ClusterConfigVO.java     |  40 +++
 .../studio/cluster/config/UpdateConfigDTO.java     |  38 +++
 .../rocketmq/studio/cluster/k8s/CreateCertDTO.java |  37 +++
 .../rocketmq/studio/cluster/k8s/DeleteCertDTO.java |  30 +++
 .../cluster/k8s/InMemoryK8sCertRepository.java     | 119 +++++++++
 .../studio/cluster/k8s/K8sCertController.java      |  61 +++++
 .../studio/cluster/k8s/K8sCertRepository.java      |  32 +++
 .../studio/cluster/k8s/K8sCertService.java         | 126 ++++++++++
 .../com/rocketmq/studio/cluster/k8s/K8sCertVO.java |  47 ++++
 .../rocketmq/studio/cluster/k8s/RenewCertDTO.java  |  30 +++
 .../rocketmq/studio/cluster/k8s/UpdateCertDTO.java |  38 +++
 .../studio/cluster/metrics/MetricDataVO.java       |  33 +++
 .../studio/cluster/metrics/MetricQueryDTO.java     |  33 +++
 .../studio/cluster/metrics/MetricsController.java  |  37 +++
 .../studio/cluster/metrics/MetricsService.java     |  35 +++
 .../studio/cluster/metrics/MetricsSource.java      |  23 ++
 .../cluster/metrics/PrometheusMetricsSource.java   |  68 ++++++
 .../cluster/nameserver/CreateNameServerDTO.java    |  32 +++
 .../cluster/nameserver/DeleteNameServerDTO.java    |  31 +++
 .../cluster/nameserver/NameServerController.java   |  65 +++++
 .../studio/cluster/nameserver/NameServerVO.java    |  32 +++
 .../cluster/nameserver/RestartNameServerDTO.java   |  31 +++
 .../cluster/nameserver/UpdateNameServerDTO.java    |  32 +++
 .../cluster/nameserver/UpgradeNameServerDTO.java   |  32 +++
 .../studio/cluster/proxy/ProxyController.java      |  41 ++++
 .../com/rocketmq/studio/cluster/proxy/ProxyVO.java |  35 +++
 .../studio/cluster/proxy/RestartProxyDTO.java      |  31 +++
 .../cluster/broker/ClusterControllerTest.java      | 165 +++++++++++++
 .../studio/cluster/broker/ClusterServiceTest.java  | 252 +++++++++++++++++++
 .../studio/cluster/k8s/K8sCertControllerTest.java  | 157 ++++++++++++
 .../studio/cluster/k8s/K8sCertServiceTest.java     | 268 +++++++++++++++++++++
 .../studio/cluster/metrics/MetricsServiceTest.java | 156 ++++++++++++
 45 files changed, 3055 insertions(+)

diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/BrokerVO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/BrokerVO.java
new file mode 100644
index 0000000..ffc9f4d
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/BrokerVO.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.common.domain.enums.BrokerStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BrokerVO {
+    private String name;
+    private String addr;
+    private String version;
+    private BrokerStatus status;
+    private double diskUsage;
+    private int tpsIn;
+    private int tpsOut;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterController.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterController.java
new file mode 100644
index 0000000..5669d73
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterController.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/clusters")
+@RequiredArgsConstructor
+public class ClusterController {
+
+    private final ClusterService clusterService;
+
+    @GetMapping
+    public Result<List<ClusterVO>> listClusters() {
+        return Result.ok(clusterService.listClusters());
+    }
+
+    @GetMapping("/{id}")
+    public Result<ClusterVO> getCluster(@PathVariable String id) {
+        return Result.ok(clusterService.getCluster(id));
+    }
+
+    @PostMapping("/config/update")
+    public Result<ClusterVO> updateClusterConfig(@RequestBody UpdateConfigDTO 
command) {
+        return Result.ok(clusterService.updateClusterConfig(command));
+    }
+
+    @PostMapping("/{clusterId}/brokers/{name}/restart")
+    public Result<Map<String, Object>> restartBroker(@PathVariable String 
clusterId,
+                                                     @PathVariable String 
name) {
+        boolean success = clusterService.restartBroker(clusterId, name);
+        return Result.ok(Map.of(
+                "success", success,
+                "message", "Broker restart initiated for " + name
+        ));
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProvider.java 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProvider.java
new file mode 100644
index 0000000..bd8ed97
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProvider.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 com.rocketmq.studio.cluster.broker;
+
+
+import java.util.List;
+
+public interface ClusterProvider {
+
+    List<ClusterVO> discoverClusters();
+
+    ClusterVO refreshClusterDetail(String clusterId);
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProviderStub.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProviderStub.java
new file mode 100644
index 0000000..e5988fe
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProviderStub.java
@@ -0,0 +1,85 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.proxy.ProxyVO;
+
+import com.rocketmq.studio.common.domain.enums.BrokerStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class ClusterProviderStub implements ClusterProvider {
+
+    @Override
+    public List<ClusterVO> discoverClusters() {
+        return List.of(buildSampleCluster());
+    }
+
+    @Override
+    public ClusterVO refreshClusterDetail(String clusterId) {
+        return buildSampleCluster();
+    }
+
+    private ClusterVO buildSampleCluster() {
+        return ClusterVO.builder()
+                .name("rmq-cluster-01")
+                .brokers(List.of(
+                        BrokerVO.builder()
+                                .name("broker-a")
+                                .addr("10.0.0.1:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(45.2)
+                                .tpsIn(1200)
+                                .tpsOut(800)
+                                .build(),
+                        BrokerVO.builder()
+                                .name("broker-b")
+                                .addr("10.0.0.2:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(38.7)
+                                .tpsIn(980)
+                                .tpsOut(750)
+                                .build()
+                ))
+                .proxies(List.of(
+                        ProxyVO.builder()
+                                .addr("10.0.0.10:8081")
+                                .status(ClusterStatus.healthy)
+                                .connections(156)
+                                .grpcPort(8081)
+                                .remotingPort(10911)
+                                .build()
+                ))
+                .nameServers(List.of(
+                        NameServerVO.builder()
+                                .addr("10.0.0.20:9876")
+                                .status(ClusterStatus.healthy)
+                                .build(),
+                        NameServerVO.builder()
+                                .addr("10.0.0.21:9876")
+                                .status(ClusterStatus.healthy)
+                                .build()
+                ))
+                .build();
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepository.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepository.java
new file mode 100644
index 0000000..6e02ac8
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepository.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface ClusterRepository {
+
+    List<ClusterVO> findAll();
+
+    Optional<ClusterVO> findById(String id);
+
+    void updateConfig(String clusterId, ClusterConfigVO config);
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
new file mode 100644
index 0000000..bd6db12
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
@@ -0,0 +1,192 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.proxy.ProxyVO;
+
+import com.rocketmq.studio.common.domain.enums.BrokerStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Repository
+public class ClusterRepositoryImpl implements ClusterRepository {
+
+    private final Map<String, ClusterVO> store = new ConcurrentHashMap<>();
+
+    public ClusterRepositoryImpl() {
+        initStubData();
+    }
+
+    @Override
+    public List<ClusterVO> findAll() {
+        return new ArrayList<>(store.values());
+    }
+
+    @Override
+    public Optional<ClusterVO> findById(String id) {
+        return Optional.ofNullable(store.get(id));
+    }
+
+    @Override
+    public void updateConfig(String clusterId, ClusterConfigVO config) {
+        ClusterVO cluster = store.get(clusterId);
+        if (cluster != null) {
+            cluster.setConfig(config);
+            cluster.setUpdatedAt(LocalDateTime.now());
+            log.info("Config updated for cluster: {}", clusterId);
+        }
+    }
+
+    private void initStubData() {
+        ClusterConfigVO config = ClusterConfigVO.builder()
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .maxMessageSize(4194304)
+                .msgTraceTopicName("RMQ_SYS_TRACE_TOPIC")
+                .autoCreateTopicEnable(true)
+                .autoCreateSubscriptionGroup(true)
+                .deleteWhen("04")
+                .fileReservedTime(72)
+                .flushDiskType(FlushDiskType.ASYNC_FLUSH)
+                .brokerPermission(6)
+                .build();
+
+        ClusterVO cluster1 = ClusterVO.builder()
+                .name("rmq-cluster-prod")
+                .nsClusterName("rmq-cluster-prod-ns")
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .endpoint("10.0.0.1:9876")
+                .status(ClusterStatus.healthy)
+                .version("5.2.0")
+                .brokers(List.of(
+                        BrokerVO.builder()
+                                .name("broker-a")
+                                .addr("10.0.0.1:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(45.2)
+                                .tpsIn(1200)
+                                .tpsOut(800)
+                                .build(),
+                        BrokerVO.builder()
+                                .name("broker-b")
+                                .addr("10.0.0.2:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(38.7)
+                                .tpsIn(980)
+                                .tpsOut(750)
+                                .build()
+                ))
+                .proxies(List.of(
+                        ProxyVO.builder()
+                                .addr("10.0.0.10:8081")
+                                .status(ClusterStatus.healthy)
+                                .connections(156)
+                                .grpcPort(8081)
+                                .remotingPort(10911)
+                                .build()
+                ))
+                .nameServers(List.of(
+                        NameServerVO.builder()
+                                .addr("10.0.0.20:9876")
+                                .status(ClusterStatus.healthy)
+                                .build(),
+                        NameServerVO.builder()
+                                .addr("10.0.0.21:9876")
+                                .status(ClusterStatus.healthy)
+                                .build()
+                ))
+                .config(config)
+                .topicCount(128)
+                .groupCount(45)
+                .tpsHistory(List.of(1200, 1350, 1100, 1450, 1280, 1500, 1380, 
1420, 1300, 1550))
+                .build();
+        cluster1.setId("cluster-001");
+        cluster1.setCreatedAt(LocalDateTime.now().minusDays(30));
+        cluster1.setUpdatedAt(LocalDateTime.now());
+
+        ClusterVO cluster2 = ClusterVO.builder()
+                .name("rmq-cluster-staging")
+                .nsClusterName("rmq-cluster-staging-ns")
+                .type(ClusterType.V5_PROXY_LOCAL)
+                .endpoint("10.1.0.1:9876")
+                .status(ClusterStatus.warning)
+                .version("5.1.4")
+                .brokers(List.of(
+                        BrokerVO.builder()
+                                .name("broker-staging-0")
+                                .addr("10.1.0.1:10911")
+                                .version("5.1.4")
+                                .status(BrokerStatus.running)
+                                .diskUsage(72.1)
+                                .tpsIn(320)
+                                .tpsOut(210)
+                                .build()
+                ))
+                .proxies(List.of(
+                        ProxyVO.builder()
+                                .addr("10.1.0.10:8081")
+                                .status(ClusterStatus.warning)
+                                .connections(23)
+                                .grpcPort(8081)
+                                .remotingPort(10911)
+                                .build()
+                ))
+                .nameServers(List.of(
+                        NameServerVO.builder()
+                                .addr("10.1.0.20:9876")
+                                .status(ClusterStatus.healthy)
+                                .build()
+                ))
+                .config(ClusterConfigVO.builder()
+                        .writeQueueNums(8)
+                        .readQueueNums(8)
+                        .maxMessageSize(4194304)
+                        .msgTraceTopicName("RMQ_SYS_TRACE_TOPIC")
+                        .autoCreateTopicEnable(true)
+                        .autoCreateSubscriptionGroup(false)
+                        .deleteWhen("04")
+                        .fileReservedTime(48)
+                        .flushDiskType(FlushDiskType.SYNC_FLUSH)
+                        .brokerPermission(6)
+                        .build())
+                .topicCount(32)
+                .groupCount(12)
+                .tpsHistory(List.of(320, 280, 350, 310, 290, 340, 300, 330, 
310, 350))
+                .build();
+        cluster2.setId("cluster-002");
+        cluster2.setCreatedAt(LocalDateTime.now().minusDays(15));
+        cluster2.setUpdatedAt(LocalDateTime.now().minusHours(3));
+
+        store.put(cluster1.getId(), cluster1);
+        store.put(cluster2.getId(), cluster2);
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java
new file mode 100644
index 0000000..544e3c8
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java
@@ -0,0 +1,157 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+import com.rocketmq.studio.cluster.nameserver.CreateNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.DeleteNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.nameserver.RestartNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.UpdateNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.UpgradeNameServerDTO;
+import com.rocketmq.studio.cluster.proxy.RestartProxyDTO;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ClusterService {
+
+    private final ClusterRepository clusterRepository;
+    private final ClusterProvider clusterProvider;
+
+    public List<ClusterVO> listClusters() {
+        log.info("Listing all clusters");
+        return clusterRepository.findAll();
+    }
+
+    public ClusterVO getCluster(String id) {
+        log.info("Getting cluster detail: {}", id);
+        return clusterRepository.findById(id)
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + id));
+    }
+
+    public ClusterVO updateClusterConfig(UpdateConfigDTO command) {
+        log.info("Updating cluster config for: {}", command.getId());
+        ClusterVO cluster = clusterRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + command.getId()));
+
+        ClusterConfigVO config = cluster.getConfig();
+        if (config == null) {
+            config = new ClusterConfigVO();
+        }
+
+        if (command.getFlushDiskType() != null) {
+            
config.setFlushDiskType(FlushDiskType.valueOf(command.getFlushDiskType()));
+        }
+        if (command.getAutoCreateTopicEnable() != null) {
+            
config.setAutoCreateTopicEnable(command.getAutoCreateTopicEnable());
+        }
+        if (command.getAutoCreateSubscriptionGroup() != null) {
+            
config.setAutoCreateSubscriptionGroup(command.getAutoCreateSubscriptionGroup());
+        }
+        if (command.getMaxMessageSize() != null) {
+            config.setMaxMessageSize(command.getMaxMessageSize());
+        }
+        if (command.getFileReservedTime() != null) {
+            config.setFileReservedTime(command.getFileReservedTime());
+        }
+        if (command.getWriteQueueNums() != null) {
+            config.setWriteQueueNums(command.getWriteQueueNums());
+        }
+        if (command.getReadQueueNums() != null) {
+            config.setReadQueueNums(command.getReadQueueNums());
+        }
+        if (command.getBrokerPermission() != null) {
+            config.setBrokerPermission(command.getBrokerPermission());
+        }
+
+        cluster.setConfig(config);
+        clusterRepository.updateConfig(command.getId(), config);
+        log.info("Cluster config updated successfully for: {}", 
command.getId());
+        return cluster;
+    }
+
+    public boolean restartBroker(String clusterId, String brokerName) {
+        log.info("Restarting broker: {} in cluster: {}", brokerName, 
clusterId);
+        clusterRepository.findById(clusterId)
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + clusterId));
+        log.info("Broker restart initiated for: {} in cluster: {}", 
brokerName, clusterId);
+        return true;
+    }
+
+    public NameServerVO createNameServer(CreateNameServerDTO command) {
+        log.info("Creating NameServer for cluster: {}", 
command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + command.getClusterId()));
+        NameServerVO ns = NameServerVO.builder()
+                .addr(command.getAddr())
+                .status(ClusterStatus.healthy)
+                .build();
+        log.info("NameServer created: {}", command.getAddr());
+        return ns;
+    }
+
+    public void updateNameServer(UpdateNameServerDTO command) {
+        log.info("Updating NameServer: {} in cluster: {}", command.getAddr(), 
command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + command.getClusterId()));
+        log.info("NameServer updated: {}", command.getAddr());
+    }
+
+    public boolean restartNameServer(RestartNameServerDTO command) {
+        log.info("Restarting NameServer: {} in cluster: {}", 
command.getAddr(), command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + command.getClusterId()));
+        log.info("NameServer restart initiated: {}", command.getAddr());
+        return true;
+    }
+
+    public boolean upgradeNameServer(UpgradeNameServerDTO command) {
+        log.info("Upgrading NameServer: {} to version: {} in cluster: {}",
+                command.getAddr(), command.getTargetVersion(), 
command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + command.getClusterId()));
+        log.info("NameServer upgrade initiated: {}", command.getAddr());
+        return true;
+    }
+
+    public boolean deleteNameServer(DeleteNameServerDTO command) {
+        log.info("Deleting NameServer: {} from cluster: {}", 
command.getAddr(), command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + command.getClusterId()));
+        log.info("NameServer deleted: {}", command.getAddr());
+        return true;
+    }
+
+    public boolean restartProxy(RestartProxyDTO command) {
+        log.info("Restarting Proxy: {} in cluster: {}", command.getAddr(), 
command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + command.getClusterId()));
+        log.info("Proxy restart initiated: {}", command.getAddr());
+        return true;
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterVO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterVO.java
new file mode 100644
index 0000000..e7cf64e
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterVO.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.proxy.ProxyVO;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class ClusterVO extends BaseEntity {
+    private String name;
+    private String nsClusterName;
+    private ClusterType type;
+    private String endpoint;
+    private ClusterStatus status;
+    private String version;
+    private List<BrokerVO> brokers;
+    private List<ProxyVO> proxies;
+    private List<NameServerVO> nameServers;
+    private ClusterConfigVO config;
+    private int topicCount;
+    private int groupCount;
+    private List<Integer> tpsHistory;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientConnectionVO.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientConnectionVO.java
new file mode 100644
index 0000000..b9ef5a3
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientConnectionVO.java
@@ -0,0 +1,43 @@
+/*
+ * 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 com.rocketmq.studio.cluster.client;
+
+import com.rocketmq.studio.common.domain.enums.ClientLanguage;
+import com.rocketmq.studio.common.domain.enums.ClientType;
+import com.rocketmq.studio.common.domain.enums.Protocol;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClientConnectionVO {
+    private String clientId;
+    private ClientType type;
+    private String groupOrTopic;
+    private Protocol protocol;
+    private String address;
+    private ClientLanguage language;
+    private String version;
+    private LocalDateTime connectedAt;
+    private String clusterName;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientController.java 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientController.java
new file mode 100644
index 0000000..c711599
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientController.java
@@ -0,0 +1,41 @@
+/*
+ * 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 com.rocketmq.studio.cluster.client;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/clients")
+@RequiredArgsConstructor
+public class ClientController {
+
+    private final ClientService clientService;
+
+    @GetMapping
+    public Result<List<ClientConnectionVO>> listConnections(
+            @RequestParam(required = false) String clusterId,
+            @RequestParam(required = false) String type) {
+        return Result.ok(clientService.listConnections(clusterId, type));
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProvider.java 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProvider.java
new file mode 100644
index 0000000..9010a7d
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProvider.java
@@ -0,0 +1,24 @@
+/*
+ * 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 com.rocketmq.studio.cluster.client;
+
+
+import java.util.List;
+
+public interface ClientProvider {
+    List<ClientConnectionVO> findConnections(String clusterId, String type);
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProviderStub.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProviderStub.java
new file mode 100644
index 0000000..27aa848
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProviderStub.java
@@ -0,0 +1,78 @@
+/*
+ * 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 com.rocketmq.studio.cluster.client;
+
+import com.rocketmq.studio.common.domain.enums.ClientLanguage;
+import com.rocketmq.studio.common.domain.enums.ClientType;
+import com.rocketmq.studio.common.domain.enums.Protocol;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Component
+public class ClientProviderStub implements ClientProvider {
+
+    private final List<ClientConnectionVO> stubData = Arrays.asList(
+            ClientConnectionVO.builder()
+                    .clientId("producer-001")
+                    .type(ClientType.Producer)
+                    .groupOrTopic("order-topic")
+                    .protocol(Protocol.gRPC)
+                    .address("192.168.1.10:56789")
+                    .language(ClientLanguage.Java)
+                    .version("5.1.0")
+                    .connectedAt(LocalDateTime.now().minusHours(2))
+                    .clusterName("production-cluster")
+                    .build(),
+            ClientConnectionVO.builder()
+                    .clientId("consumer-001")
+                    .type(ClientType.Consumer)
+                    .groupOrTopic("order-consumer-group")
+                    .protocol(Protocol.gRPC)
+                    .address("192.168.1.11:56790")
+                    .language(ClientLanguage.Java)
+                    .version("5.1.0")
+                    .connectedAt(LocalDateTime.now().minusHours(1))
+                    .clusterName("production-cluster")
+                    .build(),
+            ClientConnectionVO.builder()
+                    .clientId("consumer-002")
+                    .type(ClientType.Consumer)
+                    .groupOrTopic("payment-consumer-group")
+                    .protocol(Protocol.Remoting)
+                    .address("192.168.1.12:56791")
+                    .language(ClientLanguage.Go)
+                    .version("5.0.0")
+                    .connectedAt(LocalDateTime.now().minusMinutes(30))
+                    .clusterName("staging-cluster")
+                    .build()
+    );
+
+    @Override
+    public List<ClientConnectionVO> findConnections(String clusterId, String 
type) {
+        log.debug("Stub: finding connections, clusterId={}, type={}", 
clusterId, type);
+        return stubData.stream()
+                .filter(c -> clusterId == null || 
clusterId.equals(c.getClusterName()))
+                .filter(c -> type == null || 
type.equalsIgnoreCase(c.getType().name()))
+                .collect(Collectors.toList());
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientService.java 
b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientService.java
new file mode 100644
index 0000000..e4b45c1
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientService.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.client;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ClientService {
+
+    private final ClientProvider clientProvider;
+
+    public List<ClientConnectionVO> listConnections(String clusterId, String 
type) {
+        log.info("Listing client connections, clusterId={}, type={}", 
clusterId, type);
+        return clientProvider.findConnections(clusterId, type);
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/config/ClusterConfigVO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/config/ClusterConfigVO.java
new file mode 100644
index 0000000..2e0afe9
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/config/ClusterConfigVO.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.rocketmq.studio.cluster.config;
+
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClusterConfigVO {
+    private int writeQueueNums;
+    private int readQueueNums;
+    private int maxMessageSize;
+    private String msgTraceTopicName;
+    private boolean autoCreateTopicEnable;
+    private boolean autoCreateSubscriptionGroup;
+    private String deleteWhen;
+    private int fileReservedTime;
+    private FlushDiskType flushDiskType;
+    private int brokerPermission;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/config/UpdateConfigDTO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/config/UpdateConfigDTO.java
new file mode 100644
index 0000000..4544c93
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/config/UpdateConfigDTO.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpdateConfigDTO {
+    private String id;
+    private String flushDiskType;
+    private Boolean autoCreateTopicEnable;
+    private Boolean autoCreateSubscriptionGroup;
+    private Integer maxMessageSize;
+    private Integer fileReservedTime;
+    private Integer writeQueueNums;
+    private Integer readQueueNums;
+    private Integer brokerPermission;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java
new file mode 100644
index 0000000..75044b8
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateCertDTO {
+    private String name;
+    private String namespace;
+    private String cluster;
+    private String type;
+    private String issuer;
+    private List<String> san;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
new file mode 100644
index 0000000..e679077
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeleteCertDTO {
+    private String id;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
new file mode 100644
index 0000000..5936a2a
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
@@ -0,0 +1,119 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Component
+public class InMemoryK8sCertRepository implements K8sCertRepository {
+
+    private final Map<String, K8sCertVO> store = new ConcurrentHashMap<>();
+
+    public InMemoryK8sCertRepository() {
+        initStubData();
+    }
+
+    @Override
+    public List<K8sCertVO> findAll() {
+        return new ArrayList<>(store.values());
+    }
+
+    @Override
+    public Optional<K8sCertVO> findById(String id) {
+        return Optional.ofNullable(store.get(id));
+    }
+
+    @Override
+    public K8sCertVO save(K8sCertVO cert) {
+        store.put(cert.getId(), cert);
+        log.info("Saved certificate: {} (id={})", cert.getName(), 
cert.getId());
+        return cert;
+    }
+
+    @Override
+    public void deleteById(String id) {
+        store.remove(id);
+        log.info("Deleted certificate: {}", id);
+    }
+
+    private void initStubData() {
+        LocalDateTime now = LocalDateTime.now();
+
+        K8sCertVO cert1 = K8sCertVO.builder()
+                .name("rmq-proxy-tls")
+                .namespace("rocketmq")
+                .cluster("rmq-cluster-prod")
+                .type(CertType.TLS)
+                .issuer("letsencrypt-prod")
+                .notBefore(now.minusDays(30))
+                .notAfter(now.plusDays(60))
+                .status(CertStatus.valid)
+                .daysRemaining(60)
+                .san(List.of("proxy.rmq.local", "*.proxy.rmq.local"))
+                .build();
+        cert1.setId("cert-001");
+        cert1.setCreatedAt(now.minusDays(30));
+        cert1.setUpdatedAt(now.minusDays(30));
+
+        K8sCertVO cert2 = K8sCertVO.builder()
+                .name("rmq-broker-mtls")
+                .namespace("rocketmq")
+                .cluster("rmq-cluster-prod")
+                .type(CertType.mTLS)
+                .issuer("internal-ca")
+                .notBefore(now.minusDays(350))
+                .notAfter(now.plusDays(15))
+                .status(CertStatus.expiring)
+                .daysRemaining(15)
+                .san(List.of("broker-a.rmq.local", "broker-b.rmq.local"))
+                .build();
+        cert2.setId("cert-002");
+        cert2.setCreatedAt(now.minusDays(350));
+        cert2.setUpdatedAt(now.minusDays(350));
+
+        K8sCertVO cert3 = K8sCertVO.builder()
+                .name("rmq-staging-sa")
+                .namespace("rocketmq-staging")
+                .cluster("rmq-cluster-staging")
+                .type(CertType.ServiceAccount)
+                .issuer("k8s-self-signed")
+                .notBefore(now.minusDays(400))
+                .notAfter(now.minusDays(35))
+                .status(CertStatus.expired)
+                .daysRemaining(-35)
+                .san(List.of("sa.rmq-staging.local"))
+                .build();
+        cert3.setId("cert-003");
+        cert3.setCreatedAt(now.minusDays(400));
+        cert3.setUpdatedAt(now.minusDays(400));
+
+        store.put(cert1.getId(), cert1);
+        store.put(cert2.getId(), cert2);
+        store.put(cert3.getId(), cert3);
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java
new file mode 100644
index 0000000..43bc0f8
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/k8s-certs")
+@RequiredArgsConstructor
+public class K8sCertController {
+
+    private final K8sCertService k8sCertService;
+
+    @GetMapping
+    public Result<List<K8sCertVO>> listCerts() {
+        return Result.ok(k8sCertService.listCerts());
+    }
+
+    @PostMapping("/create")
+    public Result<K8sCertVO> createCert(@RequestBody CreateCertDTO command) {
+        return Result.ok(k8sCertService.createCert(command));
+    }
+
+    @PostMapping("/update")
+    public Result<K8sCertVO> updateCert(@RequestBody UpdateCertDTO command) {
+        return Result.ok(k8sCertService.updateCert(command));
+    }
+
+    @PostMapping("/renew")
+    public Result<K8sCertVO> renewCert(@RequestBody RenewCertDTO command) {
+        return Result.ok(k8sCertService.renewCert(command));
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> deleteCert(@RequestBody DeleteCertDTO command) {
+        k8sCertService.deleteCert(command);
+        return Result.ok();
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertRepository.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertRepository.java
new file mode 100644
index 0000000..ac54a05
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertRepository.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+
+import java.util.List;
+import java.util.Optional;
+
+public interface K8sCertRepository {
+
+    List<K8sCertVO> findAll();
+
+    Optional<K8sCertVO> findById(String id);
+
+    K8sCertVO save(K8sCertVO cert);
+
+    void deleteById(String id);
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertService.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertService.java
new file mode 100644
index 0000000..c55f9c0
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertService.java
@@ -0,0 +1,126 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class K8sCertService {
+
+    private final K8sCertRepository k8sCertRepository;
+
+    public List<K8sCertVO> listCerts() {
+        log.info("Listing all K8s certificates");
+        return k8sCertRepository.findAll();
+    }
+
+    public K8sCertVO createCert(CreateCertDTO command) {
+        log.info("Creating K8s certificate: {}", command.getName());
+
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime notAfter = now.plusYears(1);
+
+        K8sCertVO cert = K8sCertVO.builder()
+                .name(command.getName())
+                .namespace(command.getNamespace())
+                .cluster(command.getCluster())
+                .type(CertType.valueOf(command.getType()))
+                .issuer(command.getIssuer())
+                .notBefore(now)
+                .notAfter(notAfter)
+                .status(CertStatus.valid)
+                .daysRemaining((int) ChronoUnit.DAYS.between(now, notAfter))
+                .san(command.getSan())
+                .build();
+        cert.setId(UUID.randomUUID().toString());
+        cert.setCreatedAt(now);
+        cert.setUpdatedAt(now);
+
+        K8sCertVO saved = k8sCertRepository.save(cert);
+        log.info("K8s certificate created: {} (id={})", saved.getName(), 
saved.getId());
+        return saved;
+    }
+
+    public K8sCertVO updateCert(UpdateCertDTO command) {
+        log.info("Updating K8s certificate: {}", command.getId());
+        K8sCertVO cert = k8sCertRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Certificate not 
found: " + command.getId()));
+
+        if (command.getName() != null) {
+            cert.setName(command.getName());
+        }
+        if (command.getNamespace() != null) {
+            cert.setNamespace(command.getNamespace());
+        }
+        if (command.getCluster() != null) {
+            cert.setCluster(command.getCluster());
+        }
+        if (command.getType() != null) {
+            cert.setType(CertType.valueOf(command.getType()));
+        }
+        if (command.getIssuer() != null) {
+            cert.setIssuer(command.getIssuer());
+        }
+        if (command.getSan() != null) {
+            cert.setSan(command.getSan());
+        }
+        cert.setUpdatedAt(LocalDateTime.now());
+
+        K8sCertVO saved = k8sCertRepository.save(cert);
+        log.info("K8s certificate updated: {} (id={})", saved.getName(), 
saved.getId());
+        return saved;
+    }
+
+    public K8sCertVO renewCert(RenewCertDTO command) {
+        log.info("Renewing K8s certificate: {}", command.getId());
+        K8sCertVO cert = k8sCertRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Certificate not 
found: " + command.getId()));
+
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime notAfter = now.plusYears(1);
+
+        cert.setNotBefore(now);
+        cert.setNotAfter(notAfter);
+        cert.setStatus(CertStatus.valid);
+        cert.setDaysRemaining((int) ChronoUnit.DAYS.between(now, notAfter));
+        cert.setUpdatedAt(now);
+
+        K8sCertVO saved = k8sCertRepository.save(cert);
+        log.info("K8s certificate renewed: {} (id={}), new expiry: {}", 
saved.getName(), saved.getId(), notAfter);
+        return saved;
+    }
+
+    public void deleteCert(DeleteCertDTO command) {
+        log.info("Deleting K8s certificate: {}", command.getId());
+        k8sCertRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Certificate not 
found: " + command.getId()));
+        k8sCertRepository.deleteById(command.getId());
+        log.info("K8s certificate deleted: {}", command.getId());
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertVO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertVO.java
new file mode 100644
index 0000000..1d04d7d
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertVO.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 com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class K8sCertVO extends BaseEntity {
+    private String name;
+    private String namespace;
+    private String cluster;
+    private CertType type;
+    private String issuer;
+    private LocalDateTime notBefore;
+    private LocalDateTime notAfter;
+    private CertStatus status;
+    private int daysRemaining;
+    private List<String> san;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java
new file mode 100644
index 0000000..d087711
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RenewCertDTO {
+    private String id;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
new file mode 100644
index 0000000..f46b0a0
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpdateCertDTO {
+    private String id;
+    private String name;
+    private String namespace;
+    private String cluster;
+    private String type;
+    private String issuer;
+    private List<String> san;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricDataVO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricDataVO.java
new file mode 100644
index 0000000..06fbf70
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricDataVO.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.metrics;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MetricDataVO {
+    private String metric;
+    private List<long[]> values;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricQueryDTO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricQueryDTO.java
new file mode 100644
index 0000000..0884ea4
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricQueryDTO.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.metrics;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MetricQueryDTO {
+    private String metric;
+    private long start;
+    private long end;
+    private String step;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsController.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsController.java
new file mode 100644
index 0000000..9015c01
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsController.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/metrics")
+@RequiredArgsConstructor
+public class MetricsController {
+
+    private final MetricsService metricsService;
+
+    @PostMapping("/query")
+    public Result<MetricDataVO> query(@RequestBody MetricQueryDTO query) {
+        return Result.ok(metricsService.query(query));
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsService.java 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsService.java
new file mode 100644
index 0000000..0a38127
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsService.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MetricsService {
+
+    private final MetricsSource metricsSource;
+
+    public MetricDataVO query(MetricQueryDTO query) {
+        log.info("Querying metrics: metric={}, start={}, end={}, step={}",
+                query.getMetric(), query.getStart(), query.getEnd(), 
query.getStep());
+        return metricsSource.query(query);
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsSource.java 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsSource.java
new file mode 100644
index 0000000..b5fac27
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsSource.java
@@ -0,0 +1,23 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+
+public interface MetricsSource {
+    MetricDataVO query(MetricQueryDTO query);
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
new file mode 100644
index 0000000..cc48fe5
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
@@ -0,0 +1,68 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Component
+public class PrometheusMetricsSource implements MetricsSource {
+
+    @Override
+    public MetricDataVO query(MetricQueryDTO query) {
+        log.info("Querying Prometheus: metric={}, start={}, end={}, step={}",
+                query.getMetric(), query.getStart(), query.getEnd(), 
query.getStep());
+
+        // Stub: generate sample data points
+        List<long[]> values = new ArrayList<>();
+        long stepSeconds = parseStep(query.getStep());
+        long start = query.getStart();
+        long end = query.getEnd();
+
+        for (long ts = start; ts <= end; ts += stepSeconds) {
+            values.add(new long[]{ts, (long) (Math.random() * 100)});
+        }
+
+        return MetricDataVO.builder()
+                .metric(query.getMetric())
+                .values(values)
+                .build();
+    }
+
+    private long parseStep(String step) {
+        if (step == null || step.isEmpty()) {
+            return 60;
+        }
+        try {
+            if (step.endsWith("s")) {
+                return Long.parseLong(step.substring(0, step.length() - 1));
+            } else if (step.endsWith("m")) {
+                return Long.parseLong(step.substring(0, step.length() - 1)) * 
60;
+            } else if (step.endsWith("h")) {
+                return Long.parseLong(step.substring(0, step.length() - 1)) * 
3600;
+            }
+            return Long.parseLong(step);
+        } catch (NumberFormatException e) {
+            log.warn("Failed to parse step '{}', defaulting to 60s", step);
+            return 60;
+        }
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/CreateNameServerDTO.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/CreateNameServerDTO.java
new file mode 100644
index 0000000..234e4f3
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/CreateNameServerDTO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateNameServerDTO {
+    private String clusterId;
+    private String addr;
+    private String version;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/DeleteNameServerDTO.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/DeleteNameServerDTO.java
new file mode 100644
index 0000000..dd8c8d4
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/DeleteNameServerDTO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeleteNameServerDTO {
+    private String clusterId;
+    private String addr;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerController.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerController.java
new file mode 100644
index 0000000..a25e021
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerController.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.nameserver;
+
+import com.rocketmq.studio.cluster.broker.ClusterService;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/nameservers")
+@RequiredArgsConstructor
+public class NameServerController {
+
+    private final ClusterService clusterService;
+
+    @PostMapping("/create")
+    public Result<NameServerVO> createNameServer(@RequestBody 
CreateNameServerDTO command) {
+        return Result.ok(clusterService.createNameServer(command));
+    }
+
+    @PostMapping("/update")
+    public Result<Void> updateNameServer(@RequestBody UpdateNameServerDTO 
command) {
+        clusterService.updateNameServer(command);
+        return Result.ok();
+    }
+
+    @PostMapping("/restart")
+    public Result<Map<String, Boolean>> restartNameServer(@RequestBody 
RestartNameServerDTO command) {
+        boolean success = clusterService.restartNameServer(command);
+        return Result.ok(Map.of("success", success));
+    }
+
+    @PostMapping("/upgrade")
+    public Result<Map<String, Boolean>> upgradeNameServer(@RequestBody 
UpgradeNameServerDTO command) {
+        boolean success = clusterService.upgradeNameServer(command);
+        return Result.ok(Map.of("success", success));
+    }
+
+    @PostMapping("/delete")
+    public Result<Map<String, Boolean>> deleteNameServer(@RequestBody 
DeleteNameServerDTO command) {
+        boolean success = clusterService.deleteNameServer(command);
+        return Result.ok(Map.of("success", success));
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerVO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerVO.java
new file mode 100644
index 0000000..f84e0b9
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerVO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class NameServerVO {
+    private String addr;
+    private ClusterStatus status;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/RestartNameServerDTO.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/RestartNameServerDTO.java
new file mode 100644
index 0000000..6a04de9
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/RestartNameServerDTO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RestartNameServerDTO {
+    private String clusterId;
+    private String addr;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpdateNameServerDTO.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpdateNameServerDTO.java
new file mode 100644
index 0000000..a15e62f
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpdateNameServerDTO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpdateNameServerDTO {
+    private String clusterId;
+    private String addr;
+    private String version;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpgradeNameServerDTO.java
 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpgradeNameServerDTO.java
new file mode 100644
index 0000000..d9af150
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpgradeNameServerDTO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpgradeNameServerDTO {
+    private String clusterId;
+    private String addr;
+    private String targetVersion;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyController.java 
b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyController.java
new file mode 100644
index 0000000..719b361
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyController.java
@@ -0,0 +1,41 @@
+/*
+ * 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 com.rocketmq.studio.cluster.proxy;
+
+import com.rocketmq.studio.cluster.broker.ClusterService;
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/proxies")
+@RequiredArgsConstructor
+public class ProxyController {
+
+    private final ClusterService clusterService;
+
+    @PostMapping("/restart")
+    public Result<Map<String, Boolean>> restartProxy(@RequestBody 
RestartProxyDTO command) {
+        boolean success = clusterService.restartProxy(command);
+        return Result.ok(Map.of("success", success));
+    }
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyVO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyVO.java
new file mode 100644
index 0000000..15371f7
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyVO.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.cluster.proxy;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProxyVO {
+    private String addr;
+    private ClusterStatus status;
+    private int connections;
+    private int grpcPort;
+    private int remotingPort;
+}
diff --git 
a/server/src/main/java/com/rocketmq/studio/cluster/proxy/RestartProxyDTO.java 
b/server/src/main/java/com/rocketmq/studio/cluster/proxy/RestartProxyDTO.java
new file mode 100644
index 0000000..aad98de
--- /dev/null
+++ 
b/server/src/main/java/com/rocketmq/studio/cluster/proxy/RestartProxyDTO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.proxy;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RestartProxyDTO {
+    private String clusterId;
+    private String addr;
+}
diff --git 
a/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterControllerTest.java
 
b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterControllerTest.java
new file mode 100644
index 0000000..395e811
--- /dev/null
+++ 
b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterControllerTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import 
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(ClusterController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class ClusterControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @MockBean
+    private ClusterService clusterService;
+
+    @Test
+    void listClustersShouldReturnAllClusters() throws Exception {
+        ClusterVO cluster1 = buildCluster("cluster-1", "production-cluster", 
ClusterStatus.healthy);
+        ClusterVO cluster2 = buildCluster("cluster-2", "staging-cluster", 
ClusterStatus.warning);
+        when(clusterService.listClusters()).thenReturn(Arrays.asList(cluster1, 
cluster2));
+
+        mockMvc.perform(get("/api/clusters"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(2))
+                .andExpect(jsonPath("$.data[0].id").value("cluster-1"))
+                
.andExpect(jsonPath("$.data[0].name").value("production-cluster"))
+                .andExpect(jsonPath("$.data[0].status").value("healthy"))
+                .andExpect(jsonPath("$.data[1].id").value("cluster-2"))
+                
.andExpect(jsonPath("$.data[1].name").value("staging-cluster"));
+    }
+
+    @Test
+    void listClustersShouldReturnEmptyArrayWhenNoClusters() throws Exception {
+        
when(clusterService.listClusters()).thenReturn(Collections.emptyList());
+
+        mockMvc.perform(get("/api/clusters"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(0));
+    }
+
+    @Test
+    void getClusterShouldReturnClusterDetail() throws Exception {
+        ClusterVO cluster = buildCluster("cluster-1", "production-cluster", 
ClusterStatus.healthy);
+        cluster.setConfig(ClusterConfigVO.builder()
+                .flushDiskType(FlushDiskType.SYNC_FLUSH)
+                .writeQueueNums(8)
+                .readQueueNums(8)
+                .maxMessageSize(4194304)
+                .autoCreateTopicEnable(true)
+                .build());
+        when(clusterService.getCluster("cluster-1")).thenReturn(cluster);
+
+        mockMvc.perform(get("/api/clusters/cluster-1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cluster-1"))
+                .andExpect(jsonPath("$.data.name").value("production-cluster"))
+                .andExpect(jsonPath("$.data.status").value("healthy"))
+                .andExpect(jsonPath("$.data.type").value("V5_PROXY_CLUSTER"))
+                
.andExpect(jsonPath("$.data.config.flushDiskType").value("SYNC_FLUSH"))
+                .andExpect(jsonPath("$.data.config.writeQueueNums").value(8));
+    }
+
+    @Test
+    void updateConfigShouldReturnUpdatedCluster() throws Exception {
+        ClusterVO updated = buildCluster("cluster-1", "production-cluster", 
ClusterStatus.healthy);
+        updated.setConfig(ClusterConfigVO.builder()
+                .flushDiskType(FlushDiskType.SYNC_FLUSH)
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .build());
+        
when(clusterService.updateClusterConfig(any(UpdateConfigDTO.class))).thenReturn(updated);
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .build();
+
+        mockMvc.perform(post("/api/clusters/config/update")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(command)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cluster-1"))
+                
.andExpect(jsonPath("$.data.config.flushDiskType").value("SYNC_FLUSH"))
+                .andExpect(jsonPath("$.data.config.writeQueueNums").value(16))
+                .andExpect(jsonPath("$.data.config.readQueueNums").value(16));
+    }
+
+    @Test
+    void restartBrokerShouldReturnSuccess() throws Exception {
+        when(clusterService.restartBroker("cluster-1", 
"broker-0")).thenReturn(true);
+
+        
mockMvc.perform(post("/api/clusters/cluster-1/brokers/broker-0/restart"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.success").value(true))
+                .andExpect(jsonPath("$.data.message").value("Broker restart 
initiated for broker-0"));
+    }
+
+    private ClusterVO buildCluster(String id, String name, ClusterStatus 
status) {
+        ClusterVO cluster = ClusterVO.builder()
+                .name(name)
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .endpoint("10.0.0.1:9876")
+                .status(status)
+                .version("5.1.0")
+                .brokers(Collections.emptyList())
+                .proxies(Collections.emptyList())
+                .nameServers(Collections.emptyList())
+                .topicCount(10)
+                .groupCount(5)
+                .build();
+        cluster.setId(id);
+        return cluster;
+    }
+}
diff --git 
a/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java
 
b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java
new file mode 100644
index 0000000..a476f1a
--- /dev/null
+++ 
b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ClusterServiceTest {
+
+    @Mock
+    private ClusterRepository clusterRepository;
+
+    @Mock
+    private ClusterProvider clusterProvider;
+
+    @InjectMocks
+    private ClusterService clusterService;
+
+    private ClusterVO sampleCluster;
+
+    @BeforeEach
+    void setUp() {
+        sampleCluster = ClusterVO.builder()
+                .name("test-cluster")
+                .nsClusterName("ns-test-cluster")
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .endpoint("10.0.0.1:9876")
+                .status(ClusterStatus.healthy)
+                .version("5.1.0")
+                .brokers(Collections.emptyList())
+                .proxies(Collections.emptyList())
+                .nameServers(Collections.emptyList())
+                .config(ClusterConfigVO.builder()
+                        .flushDiskType(FlushDiskType.ASYNC_FLUSH)
+                        .writeQueueNums(8)
+                        .readQueueNums(8)
+                        .maxMessageSize(4194304)
+                        .autoCreateTopicEnable(true)
+                        .autoCreateSubscriptionGroup(true)
+                        .fileReservedTime(72)
+                        .brokerPermission(6)
+                        .build())
+                .topicCount(10)
+                .groupCount(5)
+                .build();
+        sampleCluster.setId("cluster-1");
+    }
+
+    @Test
+    void listClustersShouldReturnAllClusters() {
+        ClusterVO secondCluster = ClusterVO.builder()
+                .name("second-cluster")
+                .status(ClusterStatus.warning)
+                .build();
+        secondCluster.setId("cluster-2");
+
+        
when(clusterRepository.findAll()).thenReturn(Arrays.asList(sampleCluster, 
secondCluster));
+
+        List<ClusterVO> result = clusterService.listClusters();
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getName()).isEqualTo("test-cluster");
+        assertThat(result.get(1).getName()).isEqualTo("second-cluster");
+        verify(clusterRepository).findAll();
+    }
+
+    @Test
+    void listClustersShouldReturnEmptyListWhenNoClusters() {
+        when(clusterRepository.findAll()).thenReturn(Collections.emptyList());
+
+        List<ClusterVO> result = clusterService.listClusters();
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void getClusterShouldReturnClusterWhenFound() {
+        
when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        ClusterVO result = clusterService.getCluster("cluster-1");
+
+        assertThat(result).isNotNull();
+        assertThat(result.getId()).isEqualTo("cluster-1");
+        assertThat(result.getName()).isEqualTo("test-cluster");
+        assertThat(result.getStatus()).isEqualTo(ClusterStatus.healthy);
+        assertThat(result.getType()).isEqualTo(ClusterType.V5_PROXY_CLUSTER);
+    }
+
+    @Test
+    void getClusterShouldThrowWhenNotFound() {
+        
when(clusterRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> clusterService.getCluster("nonexistent"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Cluster not found: nonexistent")
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
+    }
+
+    @Test
+    void updateConfigShouldUpdateFlushDiskType() {
+        
when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        
assertThat(result.getConfig().getFlushDiskType()).isEqualTo(FlushDiskType.SYNC_FLUSH);
+        verify(clusterRepository).updateConfig(eq("cluster-1"), 
any(ClusterConfigVO.class));
+    }
+
+    @Test
+    void updateConfigShouldUpdateMultipleFields() {
+        
when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .autoCreateTopicEnable(false)
+                .autoCreateSubscriptionGroup(false)
+                .maxMessageSize(8388608)
+                .fileReservedTime(168)
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .brokerPermission(4)
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        ClusterConfigVO config = result.getConfig();
+        
assertThat(config.getFlushDiskType()).isEqualTo(FlushDiskType.SYNC_FLUSH);
+        assertThat(config.isAutoCreateTopicEnable()).isFalse();
+        assertThat(config.isAutoCreateSubscriptionGroup()).isFalse();
+        assertThat(config.getMaxMessageSize()).isEqualTo(8388608);
+        assertThat(config.getFileReservedTime()).isEqualTo(168);
+        assertThat(config.getWriteQueueNums()).isEqualTo(16);
+        assertThat(config.getReadQueueNums()).isEqualTo(16);
+        assertThat(config.getBrokerPermission()).isEqualTo(4);
+    }
+
+    @Test
+    void updateConfigShouldPreserveExistingValuesWhenCommandFieldsAreNull() {
+        
when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        ClusterConfigVO config = result.getConfig();
+        
assertThat(config.getFlushDiskType()).isEqualTo(FlushDiskType.SYNC_FLUSH);
+        assertThat(config.getWriteQueueNums()).isEqualTo(8);
+        assertThat(config.getReadQueueNums()).isEqualTo(8);
+        assertThat(config.isAutoCreateTopicEnable()).isTrue();
+    }
+
+    @Test
+    void updateConfigShouldThrowWhenClusterNotFound() {
+        
when(clusterRepository.findById("missing")).thenReturn(Optional.empty());
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("missing")
+                .flushDiskType("SYNC_FLUSH")
+                .build();
+
+        assertThatThrownBy(() -> clusterService.updateClusterConfig(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Cluster not found: missing");
+    }
+
+    @Test
+    void updateConfigShouldCreateConfigWhenNull() {
+        ClusterVO clusterWithNullConfig = ClusterVO.builder()
+                .name("null-config-cluster")
+                .status(ClusterStatus.healthy)
+                .config(null)
+                .build();
+        clusterWithNullConfig.setId("cluster-nc");
+
+        
when(clusterRepository.findById("cluster-nc")).thenReturn(Optional.of(clusterWithNullConfig));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-nc")
+                .flushDiskType("ASYNC_FLUSH")
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        assertThat(result.getConfig()).isNotNull();
+        
assertThat(result.getConfig().getFlushDiskType()).isEqualTo(FlushDiskType.ASYNC_FLUSH);
+    }
+
+    @Test
+    void restartBrokerShouldReturnTrue() {
+        
when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        boolean result = clusterService.restartBroker("cluster-1", "broker-0");
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    void restartBrokerShouldThrowWhenClusterNotFound() {
+        
when(clusterRepository.findById("missing")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> clusterService.restartBroker("missing", 
"broker-0"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Cluster not found: missing");
+    }
+}
diff --git 
a/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
 
b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
new file mode 100644
index 0000000..69cca77
--- /dev/null
+++ 
b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import 
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(K8sCertController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class K8sCertControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @MockBean
+    private K8sCertService k8sCertService;
+
+    @Test
+    void listCertsShouldReturnAllCerts() throws Exception {
+        K8sCertVO cert1 = buildCert("cert-1", "rocketmq-tls", CertType.TLS, 
CertStatus.valid);
+        K8sCertVO cert2 = buildCert("cert-2", "broker-mtls", CertType.mTLS, 
CertStatus.expiring);
+        when(k8sCertService.listCerts()).thenReturn(Arrays.asList(cert1, 
cert2));
+
+        mockMvc.perform(get("/api/k8s-certs"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(2))
+                .andExpect(jsonPath("$.data[0].id").value("cert-1"))
+                .andExpect(jsonPath("$.data[0].name").value("rocketmq-tls"))
+                .andExpect(jsonPath("$.data[0].type").value("TLS"))
+                .andExpect(jsonPath("$.data[0].status").value("valid"))
+                .andExpect(jsonPath("$.data[1].id").value("cert-2"))
+                .andExpect(jsonPath("$.data[1].name").value("broker-mtls"));
+    }
+
+    @Test
+    void listCertsShouldReturnEmptyArrayWhenNoCerts() throws Exception {
+        when(k8sCertService.listCerts()).thenReturn(Collections.emptyList());
+
+        mockMvc.perform(get("/api/k8s-certs"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(0));
+    }
+
+    @Test
+    void createCertShouldReturnCreatedCert() throws Exception {
+        K8sCertVO createdCert = buildCert("cert-new", "new-cert", 
CertType.TLS, CertStatus.valid);
+        createdCert.setNamespace("default");
+        createdCert.setCluster("test-cluster");
+        createdCert.setIssuer("vault");
+        createdCert.setSan(List.of("svc.example.com"));
+        
when(k8sCertService.createCert(any(CreateCertDTO.class))).thenReturn(createdCert);
+
+        CreateCertDTO command = CreateCertDTO.builder()
+                .name("new-cert")
+                .namespace("default")
+                .cluster("test-cluster")
+                .type("TLS")
+                .issuer("vault")
+                .san(List.of("svc.example.com"))
+                .build();
+
+        mockMvc.perform(post("/api/k8s-certs/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(command)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cert-new"))
+                .andExpect(jsonPath("$.data.name").value("new-cert"))
+                .andExpect(jsonPath("$.data.namespace").value("default"))
+                .andExpect(jsonPath("$.data.cluster").value("test-cluster"))
+                .andExpect(jsonPath("$.data.type").value("TLS"))
+                .andExpect(jsonPath("$.data.status").value("valid"))
+                .andExpect(jsonPath("$.data.san[0]").value("svc.example.com"));
+    }
+
+    @Test
+    void createCertShouldAcceptMinimalCommand() throws Exception {
+        K8sCertVO createdCert = buildCert("cert-min", "minimal-cert", 
CertType.TLS, CertStatus.valid);
+        
when(k8sCertService.createCert(any(CreateCertDTO.class))).thenReturn(createdCert);
+
+        String json = """
+                {
+                    "name": "minimal-cert",
+                    "namespace": "default",
+                    "cluster": "test",
+                    "type": "TLS",
+                    "issuer": "test-issuer"
+                }
+                """;
+
+        mockMvc.perform(post("/api/k8s-certs/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(json))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cert-min"));
+    }
+
+    private K8sCertVO buildCert(String id, String name, CertType type, 
CertStatus status) {
+        K8sCertVO cert = K8sCertVO.builder()
+                .name(name)
+                .namespace("mq-system")
+                .cluster("prod-cluster")
+                .type(type)
+                .issuer("letsencrypt")
+                .notBefore(LocalDateTime.of(2025, 1, 1, 0, 0))
+                .notAfter(LocalDateTime.of(2026, 1, 1, 0, 0))
+                .status(status)
+                .daysRemaining(180)
+                .san(List.of("example.com"))
+                .build();
+        cert.setId(id);
+        return cert;
+    }
+}
diff --git 
a/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java 
b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
new file mode 100644
index 0000000..d886c0e
--- /dev/null
+++ 
b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
@@ -0,0 +1,268 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class K8sCertServiceTest {
+
+    @Mock
+    private K8sCertRepository k8sCertRepository;
+
+    @InjectMocks
+    private K8sCertService k8sCertService;
+
+    private K8sCertVO sampleCert;
+
+    @BeforeEach
+    void setUp() {
+        sampleCert = K8sCertVO.builder()
+                .name("rocketmq-tls")
+                .namespace("mq-system")
+                .cluster("prod-cluster")
+                .type(CertType.TLS)
+                .issuer("letsencrypt")
+                .notBefore(LocalDateTime.of(2025, 1, 1, 0, 0))
+                .notAfter(LocalDateTime.of(2026, 1, 1, 0, 0))
+                .status(CertStatus.valid)
+                .daysRemaining(180)
+                .san(Arrays.asList("rocketmq.example.com", 
"*.rocketmq.example.com"))
+                .build();
+        sampleCert.setId("cert-1");
+    }
+
+    @Test
+    void listCertsShouldReturnAllCerts() {
+        K8sCertVO secondCert = K8sCertVO.builder()
+                .name("broker-mtls")
+                .type(CertType.mTLS)
+                .status(CertStatus.expiring)
+                .build();
+        secondCert.setId("cert-2");
+
+        when(k8sCertRepository.findAll()).thenReturn(Arrays.asList(sampleCert, 
secondCert));
+
+        List<K8sCertVO> result = k8sCertService.listCerts();
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getName()).isEqualTo("rocketmq-tls");
+        assertThat(result.get(0).getType()).isEqualTo(CertType.TLS);
+        assertThat(result.get(1).getName()).isEqualTo("broker-mtls");
+        assertThat(result.get(1).getType()).isEqualTo(CertType.mTLS);
+        verify(k8sCertRepository).findAll();
+    }
+
+    @Test
+    void listCertsShouldReturnEmptyListWhenNoCerts() {
+        when(k8sCertRepository.findAll()).thenReturn(Collections.emptyList());
+
+        List<K8sCertVO> result = k8sCertService.listCerts();
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void createCertShouldCreateAndSaveCert() {
+        CreateCertDTO command = CreateCertDTO.builder()
+                .name("new-tls-cert")
+                .namespace("default")
+                .cluster("test-cluster")
+                .type("TLS")
+                .issuer("vault")
+                .san(List.of("svc.example.com"))
+                .build();
+
+        
when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> {
+            K8sCertVO cert = invocation.getArgument(0);
+            if (cert.getId() == null) {
+                cert.setId("generated-id");
+            }
+            return cert;
+        });
+
+        K8sCertVO result = k8sCertService.createCert(command);
+
+        assertThat(result.getName()).isEqualTo("new-tls-cert");
+        assertThat(result.getNamespace()).isEqualTo("default");
+        assertThat(result.getCluster()).isEqualTo("test-cluster");
+        assertThat(result.getType()).isEqualTo(CertType.TLS);
+        assertThat(result.getIssuer()).isEqualTo("vault");
+        assertThat(result.getStatus()).isEqualTo(CertStatus.valid);
+        assertThat(result.getSan()).containsExactly("svc.example.com");
+        assertThat(result.getNotBefore()).isNotNull();
+        assertThat(result.getNotAfter()).isAfter(result.getNotBefore());
+        assertThat(result.getDaysRemaining()).isGreaterThan(0);
+        verify(k8sCertRepository).save(any(K8sCertVO.class));
+    }
+
+    @Test
+    void createCertShouldSetCorrectValidityPeriod() {
+        CreateCertDTO command = CreateCertDTO.builder()
+                .name("validity-test")
+                .namespace("default")
+                .cluster("test-cluster")
+                .type("TLS")
+                .issuer("test-issuer")
+                .build();
+
+        ArgumentCaptor<K8sCertVO> captor = 
ArgumentCaptor.forClass(K8sCertVO.class);
+        when(k8sCertRepository.save(captor.capture())).thenAnswer(invocation 
-> invocation.getArgument(0));
+
+        k8sCertService.createCert(command);
+
+        K8sCertVO saved = captor.getValue();
+        assertThat(saved.getNotAfter()).isAfter(saved.getNotBefore());
+        long expectedDays = 
java.time.temporal.ChronoUnit.DAYS.between(saved.getNotBefore(), 
saved.getNotAfter());
+        assertThat(saved.getDaysRemaining()).isEqualTo((int) expectedDays);
+    }
+
+    @Test
+    void updateCertShouldUpdateFieldsWhenFound() {
+        
when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+        
when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        UpdateCertDTO command = UpdateCertDTO.builder()
+                .id("cert-1")
+                .name("updated-name")
+                .namespace("new-namespace")
+                .cluster("new-cluster")
+                .type("mTLS")
+                .issuer("new-issuer")
+                .san(List.of("new.example.com"))
+                .build();
+
+        K8sCertVO result = k8sCertService.updateCert(command);
+
+        assertThat(result.getName()).isEqualTo("updated-name");
+        assertThat(result.getNamespace()).isEqualTo("new-namespace");
+        assertThat(result.getCluster()).isEqualTo("new-cluster");
+        assertThat(result.getType()).isEqualTo(CertType.mTLS);
+        assertThat(result.getIssuer()).isEqualTo("new-issuer");
+        assertThat(result.getSan()).containsExactly("new.example.com");
+        assertThat(result.getUpdatedAt()).isNotNull();
+        verify(k8sCertRepository).save(any(K8sCertVO.class));
+    }
+
+    @Test
+    void updateCertShouldPreserveExistingFieldsWhenCommandFieldsAreNull() {
+        
when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+        
when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        UpdateCertDTO command = UpdateCertDTO.builder()
+                .id("cert-1")
+                .name("only-name-changed")
+                .build();
+
+        K8sCertVO result = k8sCertService.updateCert(command);
+
+        assertThat(result.getName()).isEqualTo("only-name-changed");
+        assertThat(result.getNamespace()).isEqualTo("mq-system");
+        assertThat(result.getCluster()).isEqualTo("prod-cluster");
+        assertThat(result.getType()).isEqualTo(CertType.TLS);
+        assertThat(result.getIssuer()).isEqualTo("letsencrypt");
+    }
+
+    @Test
+    void updateCertShouldThrowWhenNotFound() {
+        
when(k8sCertRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        UpdateCertDTO command = UpdateCertDTO.builder()
+                .id("nonexistent")
+                .name("wont-work")
+                .build();
+
+        assertThatThrownBy(() -> k8sCertService.updateCert(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Certificate not found: nonexistent")
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
+    }
+
+    @Test
+    void renewCertShouldRenewCertValidity() {
+        sampleCert.setStatus(CertStatus.expired);
+        sampleCert.setDaysRemaining(0);
+
+        
when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+        
when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        RenewCertDTO command = RenewCertDTO.builder().id("cert-1").build();
+
+        K8sCertVO result = k8sCertService.renewCert(command);
+
+        assertThat(result.getStatus()).isEqualTo(CertStatus.valid);
+        assertThat(result.getDaysRemaining()).isGreaterThan(0);
+        assertThat(result.getNotBefore()).isNotNull();
+        assertThat(result.getNotAfter()).isAfter(result.getNotBefore());
+        assertThat(result.getUpdatedAt()).isNotNull();
+        verify(k8sCertRepository).save(any(K8sCertVO.class));
+    }
+
+    @Test
+    void renewCertShouldThrowWhenNotFound() {
+        
when(k8sCertRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        RenewCertDTO command = 
RenewCertDTO.builder().id("nonexistent").build();
+
+        assertThatThrownBy(() -> k8sCertService.renewCert(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Certificate not found: nonexistent");
+    }
+
+    @Test
+    void deleteCertShouldDeleteWhenFound() {
+        
when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+
+        DeleteCertDTO command = DeleteCertDTO.builder().id("cert-1").build();
+
+        k8sCertService.deleteCert(command);
+
+        verify(k8sCertRepository).deleteById("cert-1");
+    }
+
+    @Test
+    void deleteCertShouldThrowWhenNotFound() {
+        
when(k8sCertRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        DeleteCertDTO command = 
DeleteCertDTO.builder().id("nonexistent").build();
+
+        assertThatThrownBy(() -> k8sCertService.deleteCert(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Certificate not found: nonexistent");
+    }
+}
diff --git 
a/server/src/test/java/com/rocketmq/studio/cluster/metrics/MetricsServiceTest.java
 
b/server/src/test/java/com/rocketmq/studio/cluster/metrics/MetricsServiceTest.java
new file mode 100644
index 0000000..b689775
--- /dev/null
+++ 
b/server/src/test/java/com/rocketmq/studio/cluster/metrics/MetricsServiceTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MetricsServiceTest {
+
+    @Mock
+    private MetricsSource metricsSource;
+
+    @InjectMocks
+    private MetricsService metricsService;
+
+    @Test
+    void queryShouldReturnMetricData() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("cpu_usage")
+                .start(1700000000L)
+                .end(1700003600L)
+                .step("1m")
+                .build();
+        List<long[]> values = Arrays.asList(
+                new long[]{1700000000L, 45},
+                new long[]{1700000060L, 52},
+                new long[]{1700000120L, 48}
+        );
+        MetricDataVO data = 
MetricDataVO.builder().metric("cpu_usage").values(values).build();
+        when(metricsSource.query(query)).thenReturn(data);
+
+        MetricDataVO result = metricsService.query(query);
+
+        assertThat(result.getMetric()).isEqualTo("cpu_usage");
+        assertThat(result.getValues()).hasSize(3);
+        assertThat(result.getValues().get(0)[0]).isEqualTo(1700000000L);
+        assertThat(result.getValues().get(0)[1]).isEqualTo(45);
+        assertThat(result.getValues().get(1)[1]).isEqualTo(52);
+        assertThat(result.getValues().get(2)[1]).isEqualTo(48);
+        verify(metricsSource).query(query);
+    }
+
+    @Test
+    void queryShouldReturnEmptyValuesWhenNoData() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("disk_io")
+                .start(1700000000L)
+                .end(1700003600L)
+                .step("5m")
+                .build();
+        MetricDataVO data = 
MetricDataVO.builder().metric("disk_io").values(Collections.emptyList()).build();
+        when(metricsSource.query(query)).thenReturn(data);
+
+        MetricDataVO result = metricsService.query(query);
+
+        assertThat(result.getMetric()).isEqualTo("disk_io");
+        assertThat(result.getValues()).isEmpty();
+    }
+
+    @Test
+    void queryShouldPassQueryDirectlyToSource() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("tps")
+                .start(1700000000L)
+                .end(1700086400L)
+                .step("1h")
+                .build();
+        MetricDataVO data = 
MetricDataVO.builder().metric("tps").values(Collections.emptyList()).build();
+        when(metricsSource.query(any(MetricQueryDTO.class))).thenReturn(data);
+
+        metricsService.query(query);
+
+        verify(metricsSource).query(query);
+    }
+
+    @Test
+    void queryShouldHandleVariousStepSizes() {
+        MetricQueryDTO query15s = 
MetricQueryDTO.builder().metric("cpu").start(1L).end(2L).step("15s").build();
+        MetricQueryDTO query1h = 
MetricQueryDTO.builder().metric("cpu").start(1L).end(2L).step("1h").build();
+        MetricDataVO data = 
MetricDataVO.builder().metric("cpu").values(Collections.emptyList()).build();
+        when(metricsSource.query(any(MetricQueryDTO.class))).thenReturn(data);
+
+        MetricDataVO result15s = metricsService.query(query15s);
+        MetricDataVO result1h = metricsService.query(query1h);
+
+        assertThat(result15s).isNotNull();
+        assertThat(result1h).isNotNull();
+        verify(metricsSource).query(query15s);
+        verify(metricsSource).query(query1h);
+    }
+
+    @Test
+    void queryShouldReturnMultipleDataPoints() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("memory_usage")
+                .start(1700000000L)
+                .end(1700003600L)
+                .step("1m")
+                .build();
+        List<long[]> values = Arrays.asList(
+                new long[]{1700000000L, 72},
+                new long[]{1700000060L, 73},
+                new long[]{1700000120L, 71},
+                new long[]{1700000180L, 74},
+                new long[]{1700000240L, 75}
+        );
+        MetricDataVO data = 
MetricDataVO.builder().metric("memory_usage").values(values).build();
+        when(metricsSource.query(query)).thenReturn(data);
+
+        MetricDataVO result = metricsService.query(query);
+
+        assertThat(result.getMetric()).isEqualTo("memory_usage");
+        assertThat(result.getValues()).hasSize(5);
+    }
+
+    @Test
+    void queryShouldPreserveMetricName() {
+        String[] metrics = {"rocketmq_tps", "rocketmq_latency_p99", 
"broker_disk_usage", "consumer_lag"};
+        for (String metricName : metrics) {
+            MetricQueryDTO query = 
MetricQueryDTO.builder().metric(metricName).start(1L).end(2L).step("1m").build();
+            MetricDataVO data = 
MetricDataVO.builder().metric(metricName).values(Collections.emptyList()).build();
+            when(metricsSource.query(query)).thenReturn(data);
+
+            MetricDataVO result = metricsService.query(query);
+
+            assertThat(result.getMetric()).isEqualTo(metricName);
+        }
+    }
+}


Reply via email to