diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/domain/TestResource.java b/backend/framework/domain/src/main/java/io/metersphere/system/domain/TestResource.java index 1412140a54..096072da57 100644 --- a/backend/framework/domain/src/main/java/io/metersphere/system/domain/TestResource.java +++ b/backend/framework/domain/src/main/java/io/metersphere/system/domain/TestResource.java @@ -19,9 +19,8 @@ public class TestResource implements Serializable { private String testResourcePoolId; @Schema(title = "资源节点状态", requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank(message = "{test_resource.status.not_blank}", groups = {Created.class}) - @Size(min = 1, max = 20, message = "{test_resource.status.length_range}", groups = {Created.class, Updated.class}) - private String status; + @NotNull(message = "{test_resource.enable.not_blank}", groups = {Created.class}) + private Boolean enable; @Schema(title = "创建时间") private Long createTime; @@ -32,5 +31,9 @@ public class TestResource implements Serializable { @Schema(title = "资源节点配置") private byte[] configuration; + @Schema(title = "是否删除", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "{test_resource_pool.deleted.not_blank}", groups = {Created.class}) + private Boolean deleted; + private static final long serialVersionUID = 1L; } \ No newline at end of file diff --git a/backend/framework/sdk/pom.xml b/backend/framework/sdk/pom.xml index 20013cbf32..3508361cbc 100644 --- a/backend/framework/sdk/pom.xml +++ b/backend/framework/sdk/pom.xml @@ -419,6 +419,20 @@ ${otp-java.version} - + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + diff --git a/backend/framework/sdk/src/test/java/base/BaseTest.java b/backend/framework/sdk/src/test/java/base/BaseTest.java new file mode 100644 index 0000000000..31c57d0be8 --- /dev/null +++ b/backend/framework/sdk/src/test/java/base/BaseTest.java @@ -0,0 +1,41 @@ +package base; + +import com.jayway.jsonpath.JsonPath; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public abstract class BaseTest { + @Resource + private MockMvc mockMvc; + protected static String sessionId; + protected static String csrfToken; + + @BeforeEach + public void login() throws Exception { + if (StringUtils.isAnyBlank(sessionId, csrfToken)) { + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/login") + .content("{\"username\":\"admin\",\"password\":\"metersphere\"}") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + sessionId = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.data.sessionId"); + csrfToken = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.data.csrfToken"); + } + } +} diff --git a/backend/services/system-setting/pom.xml b/backend/services/system-setting/pom.xml index eb613d9e37..283df44ed6 100644 --- a/backend/services/system-setting/pom.xml +++ b/backend/services/system-setting/pom.xml @@ -18,7 +18,14 @@ metersphere-sdk ${revision} - + + io.metersphere + metersphere-sdk + ${revision} + tests + test-jar + test + diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/consul/CacheNode.java b/backend/services/system-setting/src/main/java/io/metersphere/system/consul/CacheNode.java new file mode 100644 index 0000000000..a836c9aa58 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/consul/CacheNode.java @@ -0,0 +1,9 @@ +package io.metersphere.system.consul; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CacheNode { +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/consul/CacheNodeAspect.java b/backend/services/system-setting/src/main/java/io/metersphere/system/consul/CacheNodeAspect.java new file mode 100644 index 0000000000..39a66281d7 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/consul/CacheNodeAspect.java @@ -0,0 +1,25 @@ +package io.metersphere.system.consul; + +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class CacheNodeAspect { + /** + * 定义切点 @Pointcut 在注解的位置切入代码 + */ + @Pointcut("@annotation(io.metersphere.system.consul.CacheNode)") + public void cacheNodes() { + } + + @After("cacheNodes()") + @Async + public void after() { + // microService.getForResultHolder(MicroServiceName.PERFORMANCE_TEST, "/performance/update/cache"); + } + +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/TestResourcePoolController.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/TestResourcePoolController.java new file mode 100644 index 0000000000..e88b70caf5 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/TestResourcePoolController.java @@ -0,0 +1,49 @@ +package io.metersphere.system.controller; + +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import io.metersphere.sdk.util.PageUtils; +import io.metersphere.sdk.util.Pager; +import io.metersphere.system.consul.CacheNode; +import io.metersphere.system.dto.TestResourcePoolDTO; +import io.metersphere.system.request.QueryResourcePoolRequest; +import io.metersphere.system.service.TestResourcePoolService; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RequestMapping("/test/resource/pool") +@RestController +public class TestResourcePoolController { + + @Resource + private TestResourcePoolService testResourcePoolService; + + @PostMapping("/add") + public TestResourcePoolDTO addTestResourcePool(@RequestBody TestResourcePoolDTO testResourcePoolDTO) { + return testResourcePoolService.addTestResourcePool(testResourcePoolDTO); + } + + @GetMapping("/delete/{poolId}") + @CacheNode // 把监控节点缓存起来 + public void deleteTestResourcePool(@PathVariable(value = "poolId") String testResourcePoolId) { + testResourcePoolService.deleteTestResourcePool(testResourcePoolId); + } + + @PostMapping("/update") + @CacheNode // 把监控节点缓存起来 + public void updateTestResourcePool(@RequestBody TestResourcePoolDTO testResourcePoolDTO) { + testResourcePoolService.updateTestResourcePool(testResourcePoolDTO); + } + + @PostMapping("/page") + public Pager> listResourcePools( @RequestBody QueryResourcePoolRequest request) { + Page page = PageHelper.startPage(request.getCurrent(),request.getPageSize(), true); + return PageUtils.setPageInfo(page, testResourcePoolService.listResourcePools(request)); + } + + + +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ResourcePoolTypeEnum.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ResourcePoolTypeEnum.java new file mode 100644 index 0000000000..18cf647380 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/ResourcePoolTypeEnum.java @@ -0,0 +1,8 @@ +package io.metersphere.system.dto; + +public enum ResourcePoolTypeEnum { + /** + * node controller 资源池 + */ + NODE, K8S +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/TestResourcePoolDTO.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/TestResourcePoolDTO.java new file mode 100644 index 0000000000..485940e765 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/TestResourcePoolDTO.java @@ -0,0 +1,15 @@ +package io.metersphere.system.dto; + +import io.metersphere.system.domain.TestResource; +import io.metersphere.system.domain.TestResourcePool; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestResourcePoolDTO extends TestResourcePool { + private List testResources; + +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/request/QueryResourcePoolRequest.java b/backend/services/system-setting/src/main/java/io/metersphere/system/request/QueryResourcePoolRequest.java new file mode 100644 index 0000000000..51350bdf7e --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/request/QueryResourcePoolRequest.java @@ -0,0 +1,12 @@ +package io.metersphere.system.request; + +import io.metersphere.sdk.dto.BasePageRequest; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class QueryResourcePoolRequest extends BasePageRequest { + private String name; + private Boolean enable; +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/KubernetesResourcePoolService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/KubernetesResourcePoolService.java new file mode 100644 index 0000000000..9875f7479f --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/KubernetesResourcePoolService.java @@ -0,0 +1,9 @@ +package io.metersphere.system.service; + + +import io.metersphere.system.dto.TestResourcePoolDTO; + +public interface KubernetesResourcePoolService { + + boolean validate(TestResourcePoolDTO testResourcePool); +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/NodeResourcePoolService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/NodeResourcePoolService.java new file mode 100644 index 0000000000..233b593be1 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/NodeResourcePoolService.java @@ -0,0 +1,23 @@ +package io.metersphere.system.service; + + +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.dto.TestResourcePoolDTO; +import org.apache.commons.collections4.CollectionUtils; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(rollbackFor = Exception.class) +public class NodeResourcePoolService { + + public boolean validate(TestResourcePoolDTO testResourcePool) { + if (CollectionUtils.isEmpty(testResourcePool.getTestResources())) { + throw new MSException(Translator.get("no_nodes_message")); + } + //校验节点 + return true; + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/TestResourcePoolService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/TestResourcePoolService.java new file mode 100644 index 0000000000..39d482dee5 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/TestResourcePoolService.java @@ -0,0 +1,131 @@ +package io.metersphere.system.service; + +import groovy.util.logging.Slf4j; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.CommonBeanFactory; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.domain.TestResource; +import io.metersphere.system.domain.TestResourceExample; +import io.metersphere.system.domain.TestResourcePool; +import io.metersphere.system.domain.TestResourcePoolExample; +import io.metersphere.system.dto.ResourcePoolTypeEnum; +import io.metersphere.system.dto.TestResourcePoolDTO; +import io.metersphere.system.mapper.TestResourceMapper; +import io.metersphere.system.mapper.TestResourcePoolMapper; +import io.metersphere.system.request.QueryResourcePoolRequest; +import jakarta.annotation.Resource; +import org.apache.commons.beanutils.BeanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +@Transactional +public class TestResourcePoolService { + + @Resource + private TestResourcePoolMapper testResourcePoolMapper; + + @Resource + private TestResourceMapper testResourceMapper; + + public TestResourcePoolDTO addTestResourcePool(TestResourcePoolDTO testResourcePoolDTO) { + checkTestResourcePool(testResourcePoolDTO); + testResourcePoolDTO.setId(UUID.randomUUID().toString()); + testResourcePoolDTO.setCreateTime(System.currentTimeMillis()); + testResourcePoolDTO.setUpdateTime(System.currentTimeMillis()); + testResourcePoolDTO.setEnable(true); + testResourcePoolDTO.setDeleted(false); + if (testResourcePoolDTO.getUiTest() != null && testResourcePoolDTO.getUiTest() && StringUtils.isBlank(testResourcePoolDTO.getGrid())) { + throw new MSException("Please add ui grid"); + } + validateTestResourcePool(testResourcePoolDTO); + testResourcePoolMapper.insertSelective(testResourcePoolDTO); + return testResourcePoolDTO; + } + + public void deleteTestResourcePool(String testResourcePoolId) { + TestResourcePool testResourcePool = testResourcePoolMapper.selectByPrimaryKey(testResourcePoolId); + if (testResourcePool == null) { + throw new MSException("Resource Pool not found."); + } + testResourcePool.setUpdateTime(System.currentTimeMillis()); + testResourcePool.setEnable(false); + testResourcePool.setDeleted(true); + testResourcePoolMapper.updateByPrimaryKeySelective(testResourcePool); + } + + public void updateTestResourcePool(TestResourcePoolDTO testResourcePoolDTO) { + checkTestResourcePool(testResourcePoolDTO); + testResourcePoolDTO.setUpdateTime(System.currentTimeMillis()); + validateTestResourcePool(testResourcePoolDTO); + testResourcePoolMapper.updateByPrimaryKeySelective(testResourcePoolDTO); + } + + public List listResourcePools(QueryResourcePoolRequest request) { + TestResourcePoolExample example = new TestResourcePoolExample(); + TestResourcePoolExample.Criteria criteria = example.createCriteria(); + if (StringUtils.isNotBlank(request.getName())) { + criteria.andNameLike(StringUtils.wrapIfMissing(request.getName(), "%")); + } + if (request.getEnable() != null) { + criteria.andEnableEqualTo(request.getEnable()); + } + criteria.andDeletedEqualTo(false); + example.setOrderByClause("update_time desc"); + List testResourcePools = testResourcePoolMapper.selectByExample(example); + List testResourcePoolDTOS = new ArrayList<>(); + testResourcePools.forEach(pool -> { + TestResourceExample resourceExample = new TestResourceExample(); + resourceExample.createCriteria().andTestResourcePoolIdEqualTo(pool.getId()); + resourceExample.setOrderByClause("create_time"); + List testResources = testResourceMapper.selectByExampleWithBLOBs(resourceExample); + TestResourcePoolDTO testResourcePoolDTO = new TestResourcePoolDTO(); + try { + BeanUtils.copyProperties(testResourcePoolDTO, pool); + testResourcePoolDTO.setTestResources(testResources); + testResourcePoolDTOS.add(testResourcePoolDTO); + } catch (IllegalAccessException | InvocationTargetException e) { + LogUtils.error(e.getMessage(), e); + } + }); + return new ArrayList<>(); + } + + public void checkTestResourcePool(TestResourcePoolDTO testResourcePoolDTO) { + String resourcePoolName = testResourcePoolDTO.getName(); + if (StringUtils.isBlank(resourcePoolName)) { + throw new MSException(Translator.get("test_resource_pool_name_is_null")); + } + TestResourcePoolExample example = new TestResourcePoolExample(); + TestResourcePoolExample.Criteria criteria = example.createCriteria(); + criteria.andNameEqualTo(resourcePoolName); + if (StringUtils.isNotBlank(testResourcePoolDTO.getId())) { + criteria.andIdNotEqualTo(testResourcePoolDTO.getId()); + } + criteria.andDeletedEqualTo(false); + if (testResourcePoolMapper.countByExample(example) > 0) { + throw new MSException(Translator.get("test_resource_pool_name_already_exists")); + } + } + + private boolean validateTestResourcePool(TestResourcePoolDTO testResourcePool) { + if (StringUtils.equalsIgnoreCase(testResourcePool.getType(), ResourcePoolTypeEnum.K8S.name())) { + KubernetesResourcePoolService resourcePoolService = CommonBeanFactory.getBean(KubernetesResourcePoolService.class); + if (resourcePoolService == null) { + return false; + } + return resourcePoolService.validate(testResourcePool); + } + NodeResourcePoolService resourcePoolService = CommonBeanFactory.getBean(NodeResourcePoolService.class); + return resourcePoolService.validate(testResourcePool); + } + +} diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/TestResourcePoolControllerTest.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/TestResourcePoolControllerTest.java new file mode 100644 index 0000000000..55f0f2de8d --- /dev/null +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/TestResourcePoolControllerTest.java @@ -0,0 +1,131 @@ +package io.metersphere.system.controller; + +import base.BaseTest; +import io.metersphere.sdk.constants.SessionConstants; +import io.metersphere.sdk.util.JSON; +import io.metersphere.system.domain.TestResource; +import io.metersphere.system.dto.ResourcePoolTypeEnum; +import io.metersphere.system.dto.TestResourcePoolDTO; +import io.metersphere.system.request.QueryResourcePoolRequest; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.*; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +@SpringBootTest +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class TestResourcePoolControllerTest extends BaseTest { + + @Resource + private MockMvc mockMvc; + + @Test + @Order(1) + void addTestResourcePool() throws Exception { + TestResourcePoolDTO testResourcePoolDTO = new TestResourcePoolDTO(); + testResourcePoolDTO.setName("test_pool_1"); + testResourcePoolDTO.setType(ResourcePoolTypeEnum.NODE.name()); + setResources(testResourcePoolDTO); + mockMvc.perform(MockMvcRequestBuilders.post("/test/resource/pool/add") + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .content(JSON.toJSONString(testResourcePoolDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } + + @Test + @Order(2) + void addUiTestResourcePoolFiled() throws Exception { + TestResourcePoolDTO testResourcePoolDTO = new TestResourcePoolDTO(); + testResourcePoolDTO.setName("test_pool_filed"); + testResourcePoolDTO.setType(ResourcePoolTypeEnum.NODE.name()); + testResourcePoolDTO.setUiTest(true); + mockMvc.perform(MockMvcRequestBuilders.post("/test/resource/pool/add") + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .content(JSON.toJSONString(testResourcePoolDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is5xxServerError()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + + } + + @Test + @Order(3) + void updateTestResourcePool() throws Exception { + TestResourcePoolDTO testResourcePoolDTO = new TestResourcePoolDTO(); + testResourcePoolDTO.setName("test_pool"); + testResourcePoolDTO.setType(ResourcePoolTypeEnum.NODE.name()); + setResources(testResourcePoolDTO); + mockMvc.perform(MockMvcRequestBuilders.post("/test/resource/pool/update") + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .content(JSON.toJSONString(testResourcePoolDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } + + private static void setResources(TestResourcePoolDTO testResourcePoolDTO) { + TestResource testResource = new TestResource(); + testResource.setId(UUID.randomUUID().toString()); + testResource.setTestResourcePoolId(UUID.randomUUID().toString()); + testResource.setEnable(true); + testResource.setDeleted(false); + List testResources = new ArrayList<>(); + testResources.add(testResource); + testResourcePoolDTO.setTestResources(testResources); + } + + @Test + @Order(4) + void updateTestResourcePoolFiled() throws Exception { + TestResourcePoolDTO testResourcePoolDTO = new TestResourcePoolDTO(); + testResourcePoolDTO.setType(ResourcePoolTypeEnum.NODE.name()); + mockMvc.perform(MockMvcRequestBuilders.post("/test/resource/pool/update") + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .content(JSON.toJSONString(testResourcePoolDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is5xxServerError()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } + + @Test + @Order(5) + void listResourcePools() throws Exception { + QueryResourcePoolRequest request = new QueryResourcePoolRequest(); + request.setCurrent(1); + request.setPageSize(5); + request.setName("test_pool"); + mockMvc.perform(MockMvcRequestBuilders.post("/test/resource/pool/page") + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .content(JSON.toJSONString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } + + @Test + @Order(6) + void deleteTestResourcePool() { + + } + + +} \ No newline at end of file