From 2b8a5716d994bef3c4f4c33899cbc558ec06c0db Mon Sep 17 00:00:00 2001 From: yoon6yo Date: Mon, 6 Jul 2026 18:33:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20AcceptInfoResponseDTO=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=ED=8F=AC=EB=A7=B7=EC=9D=84=20config-server=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#254)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gid 숫자 배열 → groups [{gid, name}] 객체 배열로 교체 - gpu_required bool → gpu_nodes [{node_name, num_gpu, cpu_limit, memory_limit}] 로 교체 - ConfigRequestService에 NodeRepository 주입 및 노드 조회 로직 추가 - cpu_limit: cpuCoreCount * 1000 + "m", memory_limit: memorySizeGB * 1024 + "Mi" 변환 --- .../dto/response/AcceptInfoResponseDTO.java | 61 ++++++-- .../service/ConfigRequestService.java | 12 +- .../service/ConfigRequestServiceTest.java | 145 +++++++++++++++--- 3 files changed, 182 insertions(+), 36 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/AcceptInfoResponseDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/AcceptInfoResponseDTO.java index 2d2fb4f..8f7430c 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/AcceptInfoResponseDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/AcceptInfoResponseDTO.java @@ -1,5 +1,6 @@ package DGU_AI_LAB.admin_be.domain.requests.dto.response; +import DGU_AI_LAB.admin_be.domain.nodes.entity.Node; import DGU_AI_LAB.admin_be.domain.portRequests.entity.PortRequests; import DGU_AI_LAB.admin_be.domain.requests.entity.Request; import io.swagger.v3.oas.annotations.media.Schema; @@ -15,15 +16,37 @@ public record AcceptInfoResponseDTO( String username, @Schema(description = "컨테이너 이미지 (이름:버전)", example = "cuda:11.8") String image, - @Schema(description = "Ubuntu GID 목록", example = "[1005, 1006]") - List gid, + @Schema(description = "그룹 목록 (GID + 그룹명)") + List groups, @Schema(description = "볼륨 크기 (GiB)", example = "20") Long volume_size, - @Schema(description = "GPU 필요 여부", example = "true") - Boolean gpu_required, + @Schema(description = "GPU 노드 목록") + List gpu_nodes, @Schema(description = "추가 포트 목록") List additional_ports ) { + @Schema(description = "그룹 정보") + @Builder + public record GroupDTO( + @Schema(description = "Ubuntu GID", example = "10004") + Long gid, + @Schema(description = "그룹명", example = "ailab") + String name + ) {} + + @Schema(description = "GPU 노드 정보") + @Builder + public record GpuNodeDTO( + @Schema(description = "노드명", example = "farm2") + String node_name, + @Schema(description = "GPU 수", example = "2") + int num_gpu, + @Schema(description = "CPU 제한 (k8s 포맷)", example = "4000m") + String cpu_limit, + @Schema(description = "메모리 제한 (k8s 포맷)", example = "8192Mi") + String memory_limit + ) {} + @Schema(description = "추가 포트 정보") @Builder public record AdditionalPortDTO( @@ -33,26 +56,38 @@ public record AdditionalPortDTO( String usage_purpose ) {} - public static AcceptInfoResponseDTO fromEntity(Request request, List portRequests) { + public static AcceptInfoResponseDTO fromEntity(Request request, List portRequests, List nodes) { var image = request.getContainerImage(); + List groupDTOList = request.getRequestGroups().stream() + .map(rg -> GroupDTO.builder() + .gid(rg.getGroup().getUbuntuGid()) + .name(rg.getGroup().getGroupName()) + .build()) + .toList(); + + List gpuNodeDTOList = nodes.stream() + .map(node -> GpuNodeDTO.builder() + .node_name(node.getNodeId()) + .num_gpu(node.getNumberGpu()) + .cpu_limit(node.getCpuCoreCount() * 1000 + "m") + .memory_limit(node.getMemorySizeGB() * 1024 + "Mi") + .build()) + .toList(); + List additionalPortDTOList = portRequests.stream() .map(portRequest -> AdditionalPortDTO.builder() .internal_port(portRequest.getInternalPort()) .usage_purpose(portRequest.getUsagePurpose()) - .build() - ).toList(); + .build()) + .toList(); return AcceptInfoResponseDTO.builder() .username(request.getUbuntuUsername()) .image(image.getImageName() + ":" + image.getImageVersion()) - .gid( - request.getRequestGroups().stream() - .map(rg -> rg.getGroup().getUbuntuGid()) - .toList() - ) + .groups(groupDTOList) .volume_size(request.getVolumeSizeGiB()) - .gpu_required(true) + .gpu_nodes(gpuNodeDTOList) .additional_ports(additionalPortDTOList) .build(); } diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java index be27849..d90d879 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java @@ -1,5 +1,7 @@ package DGU_AI_LAB.admin_be.domain.requests.service; +import DGU_AI_LAB.admin_be.domain.nodes.entity.Node; +import DGU_AI_LAB.admin_be.domain.nodes.repository.NodeRepository; import DGU_AI_LAB.admin_be.domain.portRequests.entity.PortRequests; import DGU_AI_LAB.admin_be.domain.portRequests.repository.PortRequestRepository; import DGU_AI_LAB.admin_be.domain.requests.dto.response.AcceptInfoResponseDTO; @@ -21,6 +23,7 @@ public class ConfigRequestService { private final RequestRepository requestRepository; private final PortRequestRepository portRequestRepository; + private final NodeRepository nodeRepository; /** ubuntu username 중복 검사 */ @Transactional(readOnly = true) @@ -29,10 +32,10 @@ public boolean isUbuntuUsernameAvailable(String username) { } /** config server용 acceptinfo */ + @Transactional(readOnly = true) public AcceptInfoResponseDTO getAcceptInfo(String username) { log.info("사용자 승인 정보 조회를 시작합니다. username: {}", username); - // 사용자 요청 정보 조회 Request request = requestRepository.findByUbuntuUsername(username) .orElseThrow(() -> { log.warn("사용자 '{}'에 대한 승인 정보가 존재하지 않습니다.", username); @@ -41,12 +44,13 @@ public AcceptInfoResponseDTO getAcceptInfo(String username) { log.debug("사용자 '{}'의 요청 정보를 성공적으로 찾았습니다. 요청 ID: {}", username, request.getRequestId()); - // 포트 요청 정보 조회 List portRequests = portRequestRepository.findByRequestRequestId(request.getRequestId()); log.debug("요청 '{}'에 속한 포트 요청 {}개를 조회했습니다.", request.getRequestId(), portRequests.size()); - // 응답 DTO 생성 및 반환 - AcceptInfoResponseDTO response = AcceptInfoResponseDTO.fromEntity(request, portRequests); + List nodes = nodeRepository.findAllByResourceGroup(request.getResourceGroup()); + log.debug("리소스 그룹 소속 노드 {}개를 조회했습니다.", nodes.size()); + + AcceptInfoResponseDTO response = AcceptInfoResponseDTO.fromEntity(request, portRequests, nodes); log.info("사용자 '{}'에 대한 AcceptInfoResponseDTO 생성을 완료했습니다.", username); return response; diff --git a/src/test/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestServiceTest.java b/src/test/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestServiceTest.java index 82a4d98..1d95797 100644 --- a/src/test/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestServiceTest.java +++ b/src/test/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestServiceTest.java @@ -1,11 +1,16 @@ package DGU_AI_LAB.admin_be.domain.requests.service; import DGU_AI_LAB.admin_be.domain.containerImage.entity.ContainerImage; +import DGU_AI_LAB.admin_be.domain.groups.entity.Group; +import DGU_AI_LAB.admin_be.domain.nodes.entity.Node; +import DGU_AI_LAB.admin_be.domain.nodes.repository.NodeRepository; import DGU_AI_LAB.admin_be.domain.portRequests.entity.PortRequests; import DGU_AI_LAB.admin_be.domain.portRequests.repository.PortRequestRepository; import DGU_AI_LAB.admin_be.domain.requests.dto.response.AcceptInfoResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.entity.Request; +import DGU_AI_LAB.admin_be.domain.requests.entity.RequestGroup; import DGU_AI_LAB.admin_be.domain.requests.repository.RequestRepository; +import DGU_AI_LAB.admin_be.domain.resourceGroups.entity.ResourceGroup; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,6 +21,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -26,12 +32,122 @@ class ConfigRequestServiceTest { @Mock private RequestRepository requestRepository; @Mock private PortRequestRepository portRequestRepository; + @Mock private NodeRepository nodeRepository; private ConfigRequestService service; @BeforeEach void setUp() { - service = new ConfigRequestService(requestRepository, portRequestRepository); + service = new ConfigRequestService(requestRepository, portRequestRepository, nodeRepository); + } + + private Request mockRequest(long requestId, String username, ResourceGroup resourceGroup) { + ContainerImage image = mock(ContainerImage.class); + when(image.getImageName()).thenReturn("cuda"); + when(image.getImageVersion()).thenReturn("11.8"); + + Request request = mock(Request.class); + when(request.getRequestId()).thenReturn(requestId); + when(request.getUbuntuUsername()).thenReturn(username); + when(request.getContainerImage()).thenReturn(image); + when(request.getRequestGroups()).thenReturn(new LinkedHashSet<>()); + when(request.getVolumeSizeGiB()).thenReturn(20L); + when(request.getResourceGroup()).thenReturn(resourceGroup); + + when(requestRepository.findByUbuntuUsername(username)).thenReturn(Optional.of(request)); + when(portRequestRepository.findByRequestRequestId(requestId)).thenReturn(List.of()); + when(nodeRepository.findAllByResourceGroup(resourceGroup)).thenReturn(List.of()); + + return request; + } + + @Test + @DisplayName("groups 필드는 GID와 그룹명을 함께 반환한다") + void getAcceptInfo_returnsGroupsWithGidAndName() { + // Given + String username = "testuser"; + ResourceGroup resourceGroup = mock(ResourceGroup.class); + + ContainerImage image = mock(ContainerImage.class); + when(image.getImageName()).thenReturn("cuda"); + when(image.getImageVersion()).thenReturn("11.8"); + + Group group1 = mock(Group.class); + when(group1.getUbuntuGid()).thenReturn(10004L); + when(group1.getGroupName()).thenReturn("hyrn"); + + Group group2 = mock(Group.class); + when(group2.getUbuntuGid()).thenReturn(2001L); + when(group2.getGroupName()).thenReturn("ailab"); + + RequestGroup rg1 = mock(RequestGroup.class); + when(rg1.getGroup()).thenReturn(group1); + + RequestGroup rg2 = mock(RequestGroup.class); + when(rg2.getGroup()).thenReturn(group2); + + Request request = mock(Request.class); + when(request.getRequestId()).thenReturn(1L); + when(request.getUbuntuUsername()).thenReturn(username); + when(request.getContainerImage()).thenReturn(image); + when(request.getRequestGroups()).thenReturn(new LinkedHashSet<>(Set.of(rg1, rg2))); + when(request.getVolumeSizeGiB()).thenReturn(20L); + when(request.getResourceGroup()).thenReturn(resourceGroup); + + when(requestRepository.findByUbuntuUsername(username)).thenReturn(Optional.of(request)); + when(portRequestRepository.findByRequestRequestId(1L)).thenReturn(List.of()); + when(nodeRepository.findAllByResourceGroup(resourceGroup)).thenReturn(List.of()); + + // When + AcceptInfoResponseDTO result = service.getAcceptInfo(username); + + // Then + assertThat(result.groups()).hasSize(2); + assertThat(result.groups()).extracting(AcceptInfoResponseDTO.GroupDTO::gid) + .containsExactlyInAnyOrder(10004L, 2001L); + assertThat(result.groups()).extracting(AcceptInfoResponseDTO.GroupDTO::name) + .containsExactlyInAnyOrder("hyrn", "ailab"); + } + + @Test + @DisplayName("gpu_nodes는 노드명, GPU 수, k8s 포맷 CPU/메모리 제한을 반환한다") + void getAcceptInfo_returnsGpuNodesWithK8sFormat() { + // Given + String username = "testuser"; + ResourceGroup resourceGroup = mock(ResourceGroup.class); + + Node node = mock(Node.class); + when(node.getNodeId()).thenReturn("farm2"); + when(node.getNumberGpu()).thenReturn(2); + when(node.getCpuCoreCount()).thenReturn(4); + when(node.getMemorySizeGB()).thenReturn(8); + + ContainerImage image = mock(ContainerImage.class); + when(image.getImageName()).thenReturn("cuda"); + when(image.getImageVersion()).thenReturn("11.8"); + + Request request = mock(Request.class); + when(request.getRequestId()).thenReturn(2L); + when(request.getUbuntuUsername()).thenReturn(username); + when(request.getContainerImage()).thenReturn(image); + when(request.getRequestGroups()).thenReturn(new LinkedHashSet<>()); + when(request.getVolumeSizeGiB()).thenReturn(20L); + when(request.getResourceGroup()).thenReturn(resourceGroup); + + when(requestRepository.findByUbuntuUsername(username)).thenReturn(Optional.of(request)); + when(portRequestRepository.findByRequestRequestId(2L)).thenReturn(List.of()); + when(nodeRepository.findAllByResourceGroup(resourceGroup)).thenReturn(List.of(node)); + + // When + AcceptInfoResponseDTO result = service.getAcceptInfo(username); + + // Then + assertThat(result.gpu_nodes()).hasSize(1); + AcceptInfoResponseDTO.GpuNodeDTO gpuNode = result.gpu_nodes().get(0); + assertThat(gpuNode.node_name()).isEqualTo("farm2"); + assertThat(gpuNode.num_gpu()).isEqualTo(2); + assertThat(gpuNode.cpu_limit()).isEqualTo("4000m"); + assertThat(gpuNode.memory_limit()).isEqualTo("8192Mi"); } @Test @@ -39,6 +155,7 @@ void setUp() { void getAcceptInfo_returnsAdditionalPortsFromPortRequests() { // Given String username = "testuser"; + ResourceGroup resourceGroup = mock(ResourceGroup.class); ContainerImage image = mock(ContainerImage.class); when(image.getImageName()).thenReturn("containerssh-guest"); @@ -50,10 +167,11 @@ void getAcceptInfo_returnsAdditionalPortsFromPortRequests() { when(request.getContainerImage()).thenReturn(image); when(request.getRequestGroups()).thenReturn(new LinkedHashSet<>()); when(request.getVolumeSizeGiB()).thenReturn(20L); + when(request.getResourceGroup()).thenReturn(resourceGroup); when(requestRepository.findByUbuntuUsername(username)).thenReturn(Optional.of(request)); + when(nodeRepository.findAllByResourceGroup(resourceGroup)).thenReturn(List.of()); - // additional ports: 사용자가 신청한 포트 (tensorboard, 포트 번호는 시스템 할당) PortRequests tensorboard = mock(PortRequests.class); when(tensorboard.getInternalPort()).thenReturn(6006); when(tensorboard.getUsagePurpose()).thenReturn("tensorboard"); @@ -86,6 +204,7 @@ void getAcceptInfo_returnsAdditionalPortsFromPortRequests() { void getAcceptInfo_additionalPortsDoNotContainExternalPort() { // Given String username = "testuser2"; + ResourceGroup resourceGroup = mock(ResourceGroup.class); ContainerImage image = mock(ContainerImage.class); when(image.getImageName()).thenReturn("cuda"); @@ -97,13 +216,14 @@ void getAcceptInfo_additionalPortsDoNotContainExternalPort() { when(request.getContainerImage()).thenReturn(image); when(request.getRequestGroups()).thenReturn(new LinkedHashSet<>()); when(request.getVolumeSizeGiB()).thenReturn(10L); + when(request.getResourceGroup()).thenReturn(resourceGroup); when(requestRepository.findByUbuntuUsername(username)).thenReturn(Optional.of(request)); + when(nodeRepository.findAllByResourceGroup(resourceGroup)).thenReturn(List.of()); PortRequests portReq = mock(PortRequests.class); when(portReq.getInternalPort()).thenReturn(6006); when(portReq.getUsagePurpose()).thenReturn("tensorboard"); - // portNumber(외부포트)는 PortRequests에 있지만 AdditionalPortDTO에는 포함되지 않음 when(portRequestRepository.findByRequestRequestId(2L)) .thenReturn(List.of(portReq)); @@ -111,12 +231,11 @@ void getAcceptInfo_additionalPortsDoNotContainExternalPort() { // When AcceptInfoResponseDTO result = service.getAcceptInfo(username); - // Then - AdditionalPortDTO는 internal_port, usage_purpose만 갖는다 + // Then assertThat(result.additional_ports()).hasSize(1); AcceptInfoResponseDTO.AdditionalPortDTO portDTO = result.additional_ports().get(0); assertThat(portDTO.internal_port()).isEqualTo(6006); assertThat(portDTO.usage_purpose()).isEqualTo("tensorboard"); - // AdditionalPortDTO 구조상 external_port 필드 자체가 존재하지 않음을 타입으로 보장 } @Test @@ -124,20 +243,8 @@ void getAcceptInfo_additionalPortsDoNotContainExternalPort() { void getAcceptInfo_noAdditionalPorts_returnsEmptyList() { // Given String username = "testuser3"; - - ContainerImage image = mock(ContainerImage.class); - when(image.getImageName()).thenReturn("cuda"); - when(image.getImageVersion()).thenReturn("11.8"); - - Request request = mock(Request.class); - when(request.getRequestId()).thenReturn(3L); - when(request.getUbuntuUsername()).thenReturn(username); - when(request.getContainerImage()).thenReturn(image); - when(request.getRequestGroups()).thenReturn(new LinkedHashSet<>()); - when(request.getVolumeSizeGiB()).thenReturn(10L); - - when(requestRepository.findByUbuntuUsername(username)).thenReturn(Optional.of(request)); - when(portRequestRepository.findByRequestRequestId(3L)).thenReturn(List.of()); + ResourceGroup resourceGroup = mock(ResourceGroup.class); + mockRequest(3L, username, resourceGroup); // When AcceptInfoResponseDTO result = service.getAcceptInfo(username);