Skip to content

Commit e190631

Browse files
authored
Merge pull request #139 from CSID-DGU/develop
[deploy] develop -> main
2 parents cceb511 + 597cb1f commit e190631

20 files changed

Lines changed: 417 additions & 66 deletions

src/main/java/DGU_AI_LAB/admin_be/domain/alarm/service/AlarmService.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package DGU_AI_LAB.admin_be.domain.alarm.service;
22

3+
import DGU_AI_LAB.admin_be.domain.requests.entity.Request;
34
import DGU_AI_LAB.admin_be.error.ErrorCode;
45
import DGU_AI_LAB.admin_be.error.exception.BusinessException;
56
import lombok.RequiredArgsConstructor;
@@ -22,6 +23,11 @@ public class AlarmService {
2223

2324
@Value("${slack-webhook-url.monitoring}")
2425
private String defaultWebhookUrl;
26+
@Value("${slack-webhook-url.farm-admin}")
27+
private String farmAdminWebhookUrl;
28+
@Value("${slack-webhook-url.lab-admin}")
29+
private String labAdminWebhookUrl;
30+
2531

2632
@Value("${slack.bot-token}")
2733
private String botToken;
@@ -168,4 +174,45 @@ public void sendAllAlerts(String username, String email, String subject, String
168174
sendDMAlert(username, email, message);
169175
sendMailAlert(email, subject, message);
170176
}
177+
178+
public void sendNewRequestNotification(Request request) {
179+
String serverName = request.getResourceGroup().getServerName();
180+
String targetWebhookUrl;
181+
182+
// serverName에 따라 사용할 다른 채널로 전송
183+
if ("FARM".equalsIgnoreCase(serverName)) {
184+
targetWebhookUrl = farmAdminWebhookUrl;
185+
} else if ("LAB".equalsIgnoreCase(serverName)) {
186+
targetWebhookUrl = labAdminWebhookUrl;
187+
} else {
188+
// FARM이나 LAB이 아닌 잘못된 입력값이 있을 경우, 기본 모니터링 채널로 전송
189+
log.warn("알 수 없는 serverName '{}'에 대한 요청 알림입니다. 기본 채널로 전송합니다.", serverName);
190+
targetWebhookUrl = defaultWebhookUrl;
191+
}
192+
193+
// 슬랙 메시지 내용을 생성합니다.
194+
String message = String.format(
195+
"🔔 새로운 서버 사용 신청이 도착했습니다! 🔔\n" +
196+
"------------------------------------------\n" +
197+
"▶ 신청자: %s (%s)\n" +
198+
"▶ 신청 서버: %s\n" +
199+
"▶ Ubuntu 사용자 이름: %s\n" +
200+
"▶ 요청 이미지: %s:%s\n" +
201+
"▶ 요청 볼륨: %dGiB\n" +
202+
"------------------------------------------\n" +
203+
"관리자 페이지에서 확인 후 승인해 주세요.",
204+
request.getUser().getName(),
205+
request.getUser().getStudentId(),
206+
serverName,
207+
request.getUbuntuUsername(),
208+
request.getContainerImage().getImageName(),
209+
request.getContainerImage().getImageVersion(),
210+
request.getVolumeSizeGiB()
211+
);
212+
213+
sendSlackAlert(message, targetWebhookUrl);
214+
}
215+
216+
217+
171218
}

src/main/java/DGU_AI_LAB/admin_be/domain/portRequests/entity/PortRequests.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ public class PortRequests extends BaseTimeEntity {
2828
@Max(65535)
2929
private Integer portNumber;
3030

31+
@Column(name = "internal_port", nullable = false)
32+
@Min(1)
33+
@Max(65535)
34+
private Integer internalPort;
35+
3136
@Column(name = "usage_purpose", nullable = false, length = 1000)
3237
private String usagePurpose;
3338

3439
@Column(name = "is_active", nullable = false)
3540
@Builder.Default
36-
private Boolean isActive = true;
41+
private Boolean isActive = false;
3742

3843
@ManyToOne(fetch = FetchType.LAZY)
3944
@JoinColumn(name = "request_id", nullable = false)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package DGU_AI_LAB.admin_be.domain.portRequests.repository;
2+
3+
import DGU_AI_LAB.admin_be.domain.portRequests.entity.PortRequests;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
7+
import org.springframework.stereotype.Repository;
8+
9+
import java.util.List;
10+
11+
@Repository
12+
public interface PortRequestRepository extends JpaRepository<PortRequests, Long> {
13+
14+
List<PortRequests> findByRequestRequestId(Long requestId);
15+
16+
List<PortRequests> findByResourceGroupRsgroupId(Integer resourceGroupId);
17+
18+
boolean existsByPortNumberAndResourceGroupRsgroupId(Integer portNumber, Integer resourceGroupId);
19+
20+
@Query("SELECT p.portNumber FROM PortRequests p WHERE p.resourceGroup.rsgroupId = :resourceGroupId ORDER BY p.portNumber ASC")
21+
List<Integer> findPortNumbersByResourceGroupRsgroupIdOrderByPortNumberAsc(@Param("resourceGroupId") Integer resourceGroupId);
22+
23+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package DGU_AI_LAB.admin_be.domain.portRequests.service;
2+
3+
import DGU_AI_LAB.admin_be.domain.portRequests.entity.PortRequests;
4+
import DGU_AI_LAB.admin_be.domain.portRequests.repository.PortRequestRepository;
5+
import DGU_AI_LAB.admin_be.domain.requests.entity.Request;
6+
import DGU_AI_LAB.admin_be.domain.resourceGroups.entity.ResourceGroup;
7+
import DGU_AI_LAB.admin_be.error.ErrorCode;
8+
import DGU_AI_LAB.admin_be.error.exception.BusinessException;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
import java.util.List;
15+
16+
@Slf4j
17+
@Service
18+
@RequiredArgsConstructor
19+
@Transactional(readOnly = true)
20+
public class PortRequestService {
21+
22+
private final PortRequestRepository portRequestRepository;
23+
24+
private static final int PORT_RANGE_START = 10000;
25+
private static final int PORT_RANGE_END = 20000;
26+
27+
@Transactional
28+
public PortRequests createPortRequest(Request request, ResourceGroup resourceGroup,
29+
Integer internalPort, String usagePurpose) {
30+
31+
// Auto-assign external port number from range 10000-20000
32+
Integer assignedPortNumber = findNextAvailablePort(resourceGroup.getRsgroupId());
33+
34+
if (assignedPortNumber == null) {
35+
throw new BusinessException(ErrorCode.NO_AVAILABLE_PORT);
36+
}
37+
38+
PortRequests portRequest = PortRequests.builder()
39+
.request(request)
40+
.resourceGroup(resourceGroup)
41+
.portNumber(assignedPortNumber)
42+
.internalPort(internalPort)
43+
.usagePurpose(usagePurpose)
44+
.isActive(false) // Initially inactive until approved
45+
.build();
46+
47+
return portRequestRepository.save(portRequest);
48+
}
49+
50+
private Integer findNextAvailablePort(Integer resourceGroupId) {
51+
// Get all used port numbers in ascending order
52+
List<Integer> usedPorts = portRequestRepository.findPortNumbersByResourceGroupRsgroupIdOrderByPortNumberAsc(resourceGroupId);
53+
54+
// Find the first available port in range 10000-20000
55+
for (int port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
56+
if (!usedPorts.contains(port)) {
57+
return port;
58+
}
59+
}
60+
61+
// No available ports in range
62+
return null;
63+
}
64+
65+
public List<PortRequests> getPortRequestsByRequestId(Long requestId) {
66+
return portRequestRepository.findByRequestRequestId(requestId);
67+
}
68+
69+
public List<PortRequests> getPortRequestsByResourceGroupId(Integer resourceGroupId) {
70+
return portRequestRepository.findByResourceGroupRsgroupId(resourceGroupId);
71+
}
72+
73+
@Transactional
74+
public void activatePortRequest(Long portRequestId) {
75+
PortRequests portRequest = portRequestRepository.findById(portRequestId)
76+
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));
77+
78+
log.info("Port request {} activated", portRequestId);
79+
}
80+
}

src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestChangeController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ public class AdminRequestChangeController implements AdminRequestChangeApi {
2727
* 변경 요청 목록 조회 (관리자용)
2828
* PENDING 상태의 ChangeRequest 목록을 반환합니다.
2929
*/
30-
@GetMapping("/change")
30+
@GetMapping
3131
public ResponseEntity<SuccessResponse<?>> getChangeRequests() {
3232
List<ChangeRequestResponseDTO> changeRequests = adminRequestQueryService.getChangeRequests();
3333
return ResponseEntity.ok((SuccessResponse<?>) changeRequests);
3434
}
3535

36-
@PatchMapping("/change/approve")
36+
@PatchMapping("/approve")
3737
public ResponseEntity<SuccessResponse<?>> approveModification(
3838
@AuthenticationPrincipal(expression = "userId") Long adminId,
3939
@RequestBody @Valid ApproveModificationDTO dto
@@ -43,7 +43,7 @@ public ResponseEntity<SuccessResponse<?>> approveModification(
4343
}
4444

4545

46-
@PatchMapping("/change/reject")
46+
@PatchMapping("/reject")
4747
public ResponseEntity<SuccessResponse<?>> rejectModification(
4848
@AuthenticationPrincipal(expression = "userId") Long adminId,
4949
@RequestBody @Valid RejectModificationDTO dto

src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/RequestController.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import DGU_AI_LAB.admin_be.domain.requests.service.RequestCommandService;
88
import DGU_AI_LAB.admin_be.domain.requests.service.RequestQueryService;
99
import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails;
10+
import DGU_AI_LAB.admin_be.global.common.SuccessResponse; // Import SuccessResponse
1011
import jakarta.validation.Valid;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.http.ResponseEntity;
@@ -20,42 +21,48 @@
2021
@RequestMapping("/api/requests")
2122
public class RequestController implements RequestApi {
2223

23-
private final RequestQueryService requestService;
24+
private final RequestQueryService requestQueryService;
2425
private final RequestCommandService requestCommandService;
2526

2627
/**
2728
* 사용 신청 생성
2829
*/
2930
@PostMapping
30-
public ResponseEntity<SaveRequestResponseDTO> createRequest(
31-
@AuthenticationPrincipal(expression = "userId") Long userId,
32-
@RequestBody @Valid SaveRequestRequestDTO dto
31+
public ResponseEntity<SuccessResponse<?>> createRequest(@AuthenticationPrincipal(expression = "userId") Long userId,
32+
@RequestBody @Valid SaveRequestRequestDTO dto
3333
) {
3434
SaveRequestResponseDTO body = requestCommandService.createRequest(userId, dto);
35-
return ResponseEntity.ok(body);
35+
return SuccessResponse.created(body);
3636
}
3737

3838
/**
3939
* 사용 신청 변경 (저장공간 크기, 만료기한, 사용자가 속한 그룹, 리소스 그룹, 도커 이미지)
4040
*/
4141
@PostMapping("/{requestId}/change")
42-
public ResponseEntity<Void> createChangeRequest(
43-
@AuthenticationPrincipal(expression = "userId") Long userId,
44-
@PathVariable Long requestId,
45-
@RequestBody @Valid ModifyRequestDTO dto
42+
public ResponseEntity<SuccessResponse<?>> createChangeRequest(@AuthenticationPrincipal(expression = "userId") Long userId,
43+
@PathVariable Long requestId,
44+
@RequestBody @Valid ModifyRequestDTO dto
4645
) {
4746
requestCommandService.createModificationRequest(userId, requestId, dto);
48-
return ResponseEntity.ok().build();
47+
return SuccessResponse.ok(null);
4948
}
5049

5150
/**
5251
* 나의 사용 신청 조회
5352
*/
5453
@GetMapping("/my")
55-
public ResponseEntity<List<SaveRequestResponseDTO>> getMyRequests(
56-
@AuthenticationPrincipal CustomUserDetails user
54+
public ResponseEntity<SuccessResponse<?>> getMyRequests(@AuthenticationPrincipal CustomUserDetails user
5755
) {
58-
return ResponseEntity.ok(requestService.getRequestsByUserId(user.getUserId()));
56+
List<SaveRequestResponseDTO> body = requestQueryService.getRequestsByUserId(user.getUserId());
57+
return SuccessResponse.ok(body);
5958
}
6059

61-
}
60+
/**
61+
* 나의 사용 신청에 대한 모든 ubuntu_username 조회
62+
*/
63+
@GetMapping("/fulfilled-usernames")
64+
public ResponseEntity<SuccessResponse<?>> getAllFulfilledUsernames() {
65+
List<String> usernames = requestQueryService.getAllFulfilledUsernames();
66+
return SuccessResponse.ok(usernames);
67+
}
68+
}

src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/RequestApi.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import DGU_AI_LAB.admin_be.domain.requests.dto.request.SaveRequestRequestDTO;
44
import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO;
55
import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails;
6+
import DGU_AI_LAB.admin_be.global.common.SuccessResponse;
67
import io.swagger.v3.oas.annotations.Operation;
78
import io.swagger.v3.oas.annotations.Parameter;
89
import io.swagger.v3.oas.annotations.media.ArraySchema;
@@ -14,21 +15,25 @@
1415
import jakarta.validation.Valid;
1516
import org.springframework.http.ResponseEntity;
1617

17-
import java.util.List;
18-
1918
@Tag(name = "2. 서버 사용 신청", description = "서버 사용 신청 API")
2019
public interface RequestApi {
2120

2221
@Operation(
2322
summary = "서버 사용 신청 생성",
24-
description = "로그인된 사용자의 인증 정보를 바탕으로 서버 사용 신청을 생성합니다."
23+
description = "로그인된 사용자의 인증 정보를 바탕으로 서버 사용 신청을 생성합니다." +
24+
"신청하면서 저장할 때 사용자가 평문으로 입력하고 그걸 클라이언트에서 Base64로 인코딩해서 API 요청 (POST 서버 사용 신청 생성)\n" +
25+
"평문 -> Base64 [클라이언트]\n" +
26+
"Base64 -> sha-512 [서버]\n" +
27+
"이렇게 해서 [인프라] -> /api/auth/users/password (ssh 로그인) -> [서버]\n" +
28+
"평문 -> Base64 [인프라]\n" +
29+
"Base64 -> [서버] 인증 (sha-512) -> OK!"
2530
)
2631
@ApiResponse(
2732
responseCode = "200",
2833
description = "신청 생성 성공",
2934
content = @Content(schema = @Schema(implementation = SaveRequestResponseDTO.class))
3035
)
31-
ResponseEntity<SaveRequestResponseDTO> createRequest(
36+
ResponseEntity<SuccessResponse<?>> createRequest(
3237
@Parameter(hidden = true, description = "인증된 사용자 ID")
3338
Long userId,
3439
@RequestBody(description = "서버 사용 신청 DTO", required = true)
@@ -44,8 +49,18 @@ ResponseEntity<SaveRequestResponseDTO> createRequest(
4449
description = "조회 성공",
4550
content = @Content(array = @ArraySchema(schema = @Schema(implementation = SaveRequestResponseDTO.class)))
4651
)
47-
ResponseEntity<List<SaveRequestResponseDTO>> getMyRequests(
52+
ResponseEntity<SuccessResponse<?>> getMyRequests(
4853
@Parameter(hidden = true, description = "인증된 사용자")
4954
CustomUserDetails user
5055
);
56+
@Operation(
57+
summary = "승인 완료된 모든 Ubuntu 사용자 이름 조회",
58+
description = "[그룹 생성 시 사용] 현재 시스템에서 사용 승인(FULFILLED)이 완료된 모든 요청의 Ubuntu 사용자 이름 목록을 조회합니다."
59+
)
60+
@ApiResponse(
61+
responseCode = "200",
62+
description = "조회 성공. data 필드에 사용자 이름 문자열 배열이 포함됩니다.",
63+
content = @Content(schema = @Schema(implementation = SuccessResponse.class))
64+
)
65+
ResponseEntity<SuccessResponse<?>> getAllFulfilledUsernames();
5166
}

src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ApproveRequestDTO.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import io.swagger.v3.oas.annotations.media.Schema;
44
import jakarta.validation.constraints.NotNull;
55

6-
import java.time.LocalDateTime;
7-
86
@Schema(description = "관리자용 요청 승인 요청 DTO")
97
public record ApproveRequestDTO(
108

@@ -24,10 +22,6 @@ public record ApproveRequestDTO(
2422
@NotNull(message = "볼륨 크기는 필수로 입력해야 합니다.")
2523
Long volumeSizeGiB,
2624

27-
@Schema(description = "만료일", example = "2025-12-31T23:59:59")
28-
@NotNull(message = "만료일은 필수로 입력해야 합니다.")
29-
LocalDateTime expiresAt,
30-
3125
@Schema(description = "관리자 승인 코멘트 (선택 사항)", example = "사용 목적에 따라 리소스를 할당함")
3226
String adminComment
3327
) {}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package DGU_AI_LAB.admin_be.domain.requests.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.Max;
5+
import jakarta.validation.constraints.Min;
6+
import jakarta.validation.constraints.NotNull;
7+
import lombok.Builder;
8+
9+
@Builder
10+
public record PortRequestDTO(
11+
@Schema(description = "내부 포트 번호 (컨테이너 포트)", example = "3000")
12+
@NotNull(message = "Internal port cannot be null")
13+
@Min(value = 1, message = "Internal port must be between 1 and 65535")
14+
@Max(value = 65535, message = "Internal port must be between 1 and 65535")
15+
Integer internalPort,
16+
17+
@Schema(description = "포트 사용 목적", example = "웹 서버 포트")
18+
String usagePurpose
19+
) {
20+
}

0 commit comments

Comments
 (0)