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 f14471f7b80bb11cce1b4d927177e30363d8d003 Author: aias00 <[email protected]> AuthorDate: Thu Jul 23 22:51:05 2026 -0700 feat: add ops backend endpoints (#504) Add ops page backend endpoints: NameServer address management, VIP Channel, and TLS switches. --- .../com/rocketmq/studio/ops/OpsController.java | 63 ++++++++++++ .../java/com/rocketmq/studio/ops/OpsHomeVO.java | 32 ++++++ .../com/rocketmq/studio/ops/OpsNameServerDTO.java | 25 +++++ .../java/com/rocketmq/studio/ops/OpsService.java | 76 ++++++++++++++ .../java/com/rocketmq/studio/ops/OpsTlsDTO.java | 25 +++++ .../com/rocketmq/studio/ops/OpsVipChannelDTO.java | 25 +++++ .../com/rocketmq/studio/ops/OpsControllerTest.java | 112 +++++++++++++++++++++ .../com/rocketmq/studio/ops/OpsServiceTest.java | 72 +++++++++++++ 8 files changed, 430 insertions(+) diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsController.java b/server/src/main/java/com/rocketmq/studio/ops/OpsController.java new file mode 100644 index 00000000..5bfb8a23 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/OpsController.java @@ -0,0 +1,63 @@ +/* + * 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.ops; + +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; + +@RestController +@RequestMapping("/api/ops") +@RequiredArgsConstructor +public class OpsController { + + private final OpsService opsService; + + @GetMapping("/homePage") + public Result<OpsHomeVO> homePage() { + return Result.ok(opsService.getHomePage()); + } + + @PostMapping("/updateNameSvrAddr") + public Result<Void> updateNameSvrAddr(@RequestBody OpsNameServerDTO request) { + opsService.updateNameServer(request.getNamesrvAddr()); + return Result.ok(); + } + + @PostMapping("/addNameSvrAddr") + public Result<Void> addNameSvrAddr(@RequestBody OpsNameServerDTO request) { + opsService.addNameServer(request.getNamesrvAddr()); + return Result.ok(); + } + + @PostMapping("/updateIsVIPChannel") + public Result<Void> updateIsVIPChannel(@RequestBody OpsVipChannelDTO request) { + opsService.updateVipChannel(request.isUseVIPChannel()); + return Result.ok(); + } + + @PostMapping("/updateUseTLS") + public Result<Void> updateUseTLS(@RequestBody OpsTlsDTO request) { + opsService.updateUseTLS(request.isUseTLS()); + return Result.ok(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsHomeVO.java b/server/src/main/java/com/rocketmq/studio/ops/OpsHomeVO.java new file mode 100644 index 00000000..ec7893a1 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/OpsHomeVO.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.ops; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class OpsHomeVO { + private List<String> namesvrAddrList; + private boolean useVIPChannel; + private boolean useTLS; + private String currentNamesrv; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsNameServerDTO.java b/server/src/main/java/com/rocketmq/studio/ops/OpsNameServerDTO.java new file mode 100644 index 00000000..a520cd75 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/OpsNameServerDTO.java @@ -0,0 +1,25 @@ +/* + * 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.ops; + +import lombok.Data; + +@Data +public class OpsNameServerDTO { + private String namesrvAddr; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsService.java b/server/src/main/java/com/rocketmq/studio/ops/OpsService.java new file mode 100644 index 00000000..aa7e7123 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/OpsService.java @@ -0,0 +1,76 @@ +/* + * 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.ops; + +import com.rocketmq.studio.common.exception.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Slf4j +@Service +public class OpsService { + + private final Set<String> namesrvAddrs = new LinkedHashSet<>(List.of("127.0.0.1:9876")); + private String currentNamesrv = "127.0.0.1:9876"; + private boolean useVIPChannel = true; + private boolean useTLS; + + public synchronized OpsHomeVO getHomePage() { + return OpsHomeVO.builder() + .namesvrAddrList(new ArrayList<>(namesrvAddrs)) + .currentNamesrv(currentNamesrv) + .useVIPChannel(useVIPChannel) + .useTLS(useTLS) + .build(); + } + + public synchronized void updateNameServer(String namesrvAddr) { + String normalized = normalizeNameServer(namesrvAddr); + namesrvAddrs.add(normalized); + currentNamesrv = normalized; + log.info("Updated current NameServer address to {}", normalized); + } + + public synchronized void addNameServer(String namesrvAddr) { + String normalized = normalizeNameServer(namesrvAddr); + namesrvAddrs.add(normalized); + log.info("Added NameServer address {}", normalized); + } + + public synchronized void updateVipChannel(boolean enabled) { + useVIPChannel = enabled; + log.info("Updated VIP channel setting to {}", enabled); + } + + public synchronized void updateUseTLS(boolean enabled) { + useTLS = enabled; + log.info("Updated TLS setting to {}", enabled); + } + + private String normalizeNameServer(String namesrvAddr) { + if (namesrvAddr == null || namesrvAddr.trim().isEmpty()) { + throw new BusinessException(400, "namesrvAddr is required"); + } + return namesrvAddr.trim(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsTlsDTO.java b/server/src/main/java/com/rocketmq/studio/ops/OpsTlsDTO.java new file mode 100644 index 00000000..1ed42ee2 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/OpsTlsDTO.java @@ -0,0 +1,25 @@ +/* + * 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.ops; + +import lombok.Data; + +@Data +public class OpsTlsDTO { + private boolean useTLS; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsVipChannelDTO.java b/server/src/main/java/com/rocketmq/studio/ops/OpsVipChannelDTO.java new file mode 100644 index 00000000..612b8f38 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/OpsVipChannelDTO.java @@ -0,0 +1,25 @@ +/* + * 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.ops; + +import lombok.Data; + +@Data +public class OpsVipChannelDTO { + private boolean useVIPChannel; +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/OpsControllerTest.java b/server/src/test/java/com/rocketmq/studio/ops/OpsControllerTest.java new file mode 100644 index 00000000..cbc4582b --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/OpsControllerTest.java @@ -0,0 +1,112 @@ +/* + * 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.ops; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +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(OpsController.class) +@AutoConfigureMockMvc(addFilters = false) +class OpsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private OpsService opsService; + + @Test + void homePageShouldReturnOpsSettings() throws Exception { + OpsHomeVO home = OpsHomeVO.builder() + .namesvrAddrList(List.of("127.0.0.1:9876", "10.0.0.1:9876")) + .currentNamesrv("127.0.0.1:9876") + .useVIPChannel(true) + .useTLS(false) + .build(); + when(opsService.getHomePage()).thenReturn(home); + + mockMvc.perform(get("/api/ops/homePage")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.namesvrAddrList[0]").value("127.0.0.1:9876")) + .andExpect(jsonPath("$.data.currentNamesrv").value("127.0.0.1:9876")) + .andExpect(jsonPath("$.data.useVIPChannel").value(true)) + .andExpect(jsonPath("$.data.useTLS").value(false)); + } + + @Test + void updateNameSvrAddrShouldDelegateToService() throws Exception { + mockMvc.perform(post("/api/ops/updateNameSvrAddr") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("namesrvAddr", "10.0.0.1:9876")))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + verify(opsService).updateNameServer(eq("10.0.0.1:9876")); + } + + @Test + void addNameSvrAddrShouldDelegateToService() throws Exception { + mockMvc.perform(post("/api/ops/addNameSvrAddr") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("namesrvAddr", "10.0.0.2:9876")))) + .andExpect(status().isOk()); + + verify(opsService).addNameServer(eq("10.0.0.2:9876")); + } + + @Test + void updateVipChannelShouldDelegateToService() throws Exception { + mockMvc.perform(post("/api/ops/updateIsVIPChannel") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("useVIPChannel", false)))) + .andExpect(status().isOk()); + + verify(opsService).updateVipChannel(false); + } + + @Test + void updateUseTlsShouldDelegateToService() throws Exception { + mockMvc.perform(post("/api/ops/updateUseTLS") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("useTLS", true)))) + .andExpect(status().isOk()); + + verify(opsService).updateUseTLS(true); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/OpsServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/OpsServiceTest.java new file mode 100644 index 00000000..dd2dc23c --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/OpsServiceTest.java @@ -0,0 +1,72 @@ +/* + * 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.ops; + +import com.rocketmq.studio.common.exception.BusinessException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OpsServiceTest { + + private final OpsService opsService = new OpsService(); + + @Test + void getHomePageShouldReturnDefaultSettings() { + OpsHomeVO home = opsService.getHomePage(); + + assertThat(home.getNamesvrAddrList()).containsExactly("127.0.0.1:9876"); + assertThat(home.getCurrentNamesrv()).isEqualTo("127.0.0.1:9876"); + assertThat(home.isUseVIPChannel()).isTrue(); + assertThat(home.isUseTLS()).isFalse(); + } + + @Test + void addAndUpdateNameServerShouldMaintainUniqueAddressList() { + opsService.addNameServer(" 10.0.0.1:9876 "); + opsService.addNameServer("10.0.0.1:9876"); + opsService.updateNameServer("10.0.0.2:9876"); + + OpsHomeVO home = opsService.getHomePage(); + assertThat(home.getNamesvrAddrList()) + .containsExactly("127.0.0.1:9876", "10.0.0.1:9876", "10.0.0.2:9876"); + assertThat(home.getCurrentNamesrv()).isEqualTo("10.0.0.2:9876"); + } + + @Test + void togglesShouldUpdateHomePageSettings() { + opsService.updateVipChannel(false); + opsService.updateUseTLS(true); + + OpsHomeVO home = opsService.getHomePage(); + assertThat(home.isUseVIPChannel()).isFalse(); + assertThat(home.isUseTLS()).isTrue(); + } + + @Test + void nameServerOperationsShouldRejectBlankAddress() { + assertThatThrownBy(() -> opsService.addNameServer(" ")) + .isInstanceOf(BusinessException.class) + .hasMessage("namesrvAddr is required") + .satisfies(ex -> assertThat(((BusinessException) ex).getCode()).isEqualTo(400)); + assertThatThrownBy(() -> opsService.updateNameServer(null)) + .isInstanceOf(BusinessException.class) + .hasMessage("namesrvAddr is required"); + } +}
