Skip to content
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ dependencies {
// smtp
implementation 'org.springframework.boot:spring-boot-starter-mail'

// kubernetes
implementation 'io.fabric8:kubernetes-client:6.10.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public List<UserServerResponseDTO> getUserServers(Long userId, Status status) {
if (resourceGroup != null) {
resourceGroupName = resourceGroup.getDescription();

List<Node> nodesInGroup = nodeRepository.findByRsgroupId(resourceGroup.getRsgroupId());
List<Node> nodesInGroup = nodeRepository.findAllByResourceGroup(resourceGroup);
if (!nodesInGroup.isEmpty()) {
Node representativeNode = nodesInGroup.get(0);
cpuCoreCount = representativeNode.getCpuCoreCount();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package DGU_AI_LAB.admin_be.domain.gpus.dto.response;

import DGU_AI_LAB.admin_be.domain.gpus.repository.GpuRepository;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

Expand All @@ -13,6 +14,8 @@ public record GpuTypeResponseDTO(
@Schema(description = "GPU RAM 크기 (GB)", example = "24")
Integer ramGb,

String description,

@Schema(description = "GPU가 속한 리소스 그룹명 (같은 스펙 묶음)", example = "RTX 3090")
String resourceGroupName,

Expand Down Expand Up @@ -40,4 +43,13 @@ public static GpuTypeResponseDTO fromQueryResult(Object[] queryResult) {
.isAvailable(true) // 현재는 항상 true로 가정
.build();
}

public static GpuTypeResponseDTO fromSummary(GpuRepository.GpuSummary s) {
return GpuTypeResponseDTO.builder()
.gpuModel(s.getGpuModel())
.ramGb(s.getRamGb())
.description(s.getDescription())
.availableNodes(s.getNodeCount())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,38 @@
@Repository
public interface GpuRepository extends JpaRepository<Gpu, Long> {

@Query("SELECT g.gpuModel, g.ramGb, rg.description, COUNT(DISTINCT n.nodeId) " +
"FROM Gpu g JOIN g.node n JOIN ResourceGroup rg ON n.rsgroupId = rg.rsgroupId " +
"GROUP BY g.gpuModel, g.ramGb, rg.description")
List<Object[]> findGpuSummary();
@Query("SELECT DISTINCT n.cpuCoreCount, n.memorySizeGB " +
"FROM Gpu g JOIN g.node n " +
"WHERE g.gpuModel = :gpuModel")
List<Object[]> findNodeSpecsByGpuModel(String gpuModel);
// GPU 요약 프로젝션
interface GpuSummary {
String getGpuModel();
Integer getRamGb();
String getDescription();
Long getNodeCount();
}

@Query("""
SELECT g.gpuModel AS gpuModel,
g.ramGb AS ramGb,
rg.description AS description,
COUNT(DISTINCT n.nodeId) AS nodeCount
FROM Gpu g
JOIN g.node n
JOIN n.resourceGroup rg
GROUP BY g.gpuModel, g.ramGb, rg.description
""")
List<GpuSummary> findGpuSummary();

// GPU 모델별 노드 사양 조회
interface NodeSpec {
Integer getCpuCoreCount();
Integer getMemorySizeGB();
}

@Query("""
SELECT DISTINCT n.cpuCoreCount AS cpuCoreCount,
n.memorySizeGB AS memorySizeGB
FROM Gpu g
JOIN g.node n
WHERE g.gpuModel = :gpuModel
""")
List<NodeSpec> findNodeSpecsByGpuModel(String gpuModel);
}
23 changes: 17 additions & 6 deletions src/main/java/DGU_AI_LAB/admin_be/domain/nodes/entity/Node.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package DGU_AI_LAB.admin_be.domain.nodes.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import DGU_AI_LAB.admin_be.domain.gpus.entity.Gpu;
import DGU_AI_LAB.admin_be.domain.resourceGroups.entity.ResourceGroup;
import jakarta.persistence.*;
import lombok.*;

import java.util.LinkedHashSet;
import java.util.Set;

@Entity
@Table(name = "nodes")
@Getter
Expand All @@ -22,12 +24,21 @@ public class Node {
@Column(name = "node_id", length = 100)
private String nodeId;

@Column(name = "rsgroup_id", nullable = false)
private Integer rsgroupId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rsgroup_id", nullable = false)
private ResourceGroup resourceGroup;

@Column(name = "memory_size_GB", nullable = false)
private Integer memorySizeGB;

@Column(name = "CPU_core_count", nullable = false)
private Integer cpuCoreCount;

@OneToMany(mappedBy = "node", fetch = FetchType.LAZY)
@Builder.Default
private Set<Gpu> gpus = new LinkedHashSet<>();

public int getNumberGpu() {
return gpus == null ? 0 : gpus.size();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package DGU_AI_LAB.admin_be.domain.nodes.repository;

import DGU_AI_LAB.admin_be.domain.nodes.entity.Node;
import DGU_AI_LAB.admin_be.domain.resourceGroups.entity.ResourceGroup;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface NodeRepository extends JpaRepository<Node, String> {
List<Node> findByRsgroupId(Integer rsgroupId);
List<Node> findAllByResourceGroup(ResourceGroup resourceGroup);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package DGU_AI_LAB.admin_be.domain.pod.controller;

import DGU_AI_LAB.admin_be.domain.pod.controller.docs.PodApi;
import DGU_AI_LAB.admin_be.error.ErrorCode;
import DGU_AI_LAB.admin_be.error.exception.BusinessException;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.*;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import DGU_AI_LAB.admin_be.domain.pod.dto.response.PodResponseDTO;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/pods")
public class PodController implements PodApi {

private final KubernetesClient client;

// 전체 pod 목록 조회
@GetMapping
public List<String> getPods() {
return client.pods()
.inNamespace("default")
.list()
.getItems()
.stream()
.map(pod -> pod.getMetadata().getName())
.collect(Collectors.toList());
}

// 단일 pod 정보 조회
@GetMapping("/{podName}")
public PodResponseDTO getPodDetail(@PathVariable String podName) {
Pod pod = client.pods()
.inNamespace("default")
.withName(podName)
.get();

if (pod == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND);
}

return PodResponseDTO.fromEntity(pod);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package DGU_AI_LAB.admin_be.domain.pod.controller.docs;

import DGU_AI_LAB.admin_be.domain.pod.dto.response.PodResponseDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;

import java.util.List;

@Tag(name = "Kubernetes Pods", description = "쿠버네티스 Pod 조회 API")
public interface PodApi {

@Operation(
summary = "전체 Pod 목록 조회"
)
@ApiResponse(
responseCode = "200",
description = "조회 성공"
)
@ApiResponse(
responseCode = "500",
description = "서버 오류",
content = @Content
)
List<String> getPods();

@Operation(
summary = "단일 pod 정보 조회"
)
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content = @Content(schema = @Schema(implementation = PodResponseDTO.class))
)
@ApiResponse(
responseCode = "404",
description = "해당 이름의 Pod를 찾을 수 없음",
content = @Content
)
@ApiResponse(
responseCode = "500",
description = "서버 오류",
content = @Content
)
PodResponseDTO getPodDetail(
@Parameter(description = "조회할 Pod의 이름")
String podName
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package DGU_AI_LAB.admin_be.domain.pod.dto.response;

import io.fabric8.kubernetes.api.model.Pod;
import lombok.Builder;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Builder
public record PodResponseDTO(
String name,
String namespace,
String status,
String creationTimestamp,
Map<String, String> labels,
Map<String, String> annotations,
List<ContainerDTO> containers,
List<VolumeDTO> volumes,
String hostIP,
String nodeName
) {

@Builder
public record ContainerDTO(
String name,
String image
) {
}

@Builder
public record VolumeDTO(
String name
) {
}

public static PodResponseDTO fromEntity(Pod pod) {
List<VolumeDTO> volumes = pod.getSpec() != null && pod.getSpec().getVolumes() != null
? pod.getSpec().getVolumes().stream()
.map(volume -> VolumeDTO.builder()
.name(volume.getName())
.build())
.collect(Collectors.toList())
: List.of();

List<ContainerDTO> containers = pod.getSpec() != null && pod.getSpec().getContainers() != null
? pod.getSpec().getContainers().stream()
.map(container -> ContainerDTO.builder()
.name(container.getName())
.image(container.getImage())
.build())
.collect(Collectors.toList())
: List.of();

String hostIP = pod.getStatus() != null ? pod.getStatus().getHostIP() : null;
String nodeName = pod.getSpec() != null ? pod.getSpec().getNodeName() : null;

return PodResponseDTO.builder()
.name(pod.getMetadata().getName())
.namespace(pod.getMetadata().getNamespace())
.status(pod.getStatus() != null ? pod.getStatus().getPhase() : "Unknown")
.creationTimestamp(pod.getMetadata().getCreationTimestamp())
.labels(pod.getMetadata().getLabels())
.annotations(pod.getMetadata().getAnnotations())
.containers(containers)
.volumes(volumes)
.hostIP(hostIP)
.nodeName(nodeName)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package DGU_AI_LAB.admin_be.domain.requests.controller;

import DGU_AI_LAB.admin_be.domain.requests.controller.docs.AcceptInfoApi;
import DGU_AI_LAB.admin_be.domain.requests.dto.response.AcceptInfoResponseDTO;
import DGU_AI_LAB.admin_be.domain.requests.service.RequestService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("api/acceptinfo")
public class AcceptInfoController implements AcceptInfoApi {
private final RequestService requestService;

@GetMapping("/{username}")
public ResponseEntity<AcceptInfoResponseDTO> getAcceptInfo(@PathVariable String username) {
AcceptInfoResponseDTO response = requestService.getAcceptInfo(username);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ public ResponseEntity<List<SaveRequestResponseDTO>> getMyRequests(
return ResponseEntity.ok(requestService.getRequestsByUserId(user.getUserId()));
}

@GetMapping("/check-username")
public ResponseEntity<?> checkUbuntuUsername(@RequestParam String username) {
boolean available = requestService.isUbuntuUsernameAvailable(username);
return ResponseEntity.ok().body(
java.util.Map.of(
"available", available
)
);
}

/*@PostMapping("/modify")
public ResponseEntity<Void> requestModification(@RequestBody ModifyRequestDTO dto) {
requestService.requestModification(dto);
Expand Down
Loading