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