From a496d2de4c5dbaedab4c16a0e6db5fe10d170154 Mon Sep 17 00:00:00 2001 From: Jeongrae Kim <102293576+Jeong-Rae@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:51:28 +0000 Subject: [PATCH 01/19] =?UTF-8?q?Lab=EC=97=90=20Vo=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../lab/config/LabExternalEndpoints.java | 30 +++++++ .../lab/config/LabInfrastructureDefaults.java | 11 +++ .../domain/lab/controller/LabController.java | 33 ++------ .../domain/lab/dto/PShellCreateResponse.java | 16 +++- .../lab/factory/KubernetesPodFactory.java | 25 +++--- .../lab/factory/KubernetesServiceFactory.java | 26 +++--- .../master/domain/lab/factory/PodFactory.java | 4 +- .../domain/lab/factory/ServiceFactory.java | 4 +- .../service/KubernetesResourceManager.java | 59 +++++++------- .../lab/service/LabAssignmentService.java | 20 +++-- .../master/domain/lab/service/LabService.java | 81 ++++++++++++------- .../lab/utils/KubernetesResourceNaming.java | 27 ------- .../domain/lab/vo/KubernetesResourceName.java | 37 +++++++++ .../master/domain/lab/vo/LabResourceId.java | 31 +++++++ .../domain/lab/vo/LabResourceLabels.java | 17 ++++ .../master/domain/lab/vo/LabUserUid.java | 22 +++++ .../master/domain/lab/vo/Label.java | 7 -- .../master/domain/lab/vo/StageCode.java | 30 +++++++ .../master/domain/player/domain/Player.java | 18 +++++ .../master/domain/player/domain/PlayerId.java | 16 ++++ .../player/service/PlayerDetailsService.java | 2 +- .../player/service/PlayerProfileService.java | 6 +- .../stage/service/StageSearchService.java | 7 +- .../service/impl/StageSearchServiceImpl.java | 13 +-- .../master/global/data/Label.java | 28 +++++++ .../master/global/data/Labels.java | 48 +++++++++++ .../codequistify/master/global/data/Pair.java | 14 ++++ .../master/global/data/UrlQuery.java | 37 +++++++++ 29 files changed, 496 insertions(+), 176 deletions(-) create mode 100644 src/main/java/org/codequistify/master/domain/lab/config/LabExternalEndpoints.java create mode 100644 src/main/java/org/codequistify/master/domain/lab/config/LabInfrastructureDefaults.java delete mode 100644 src/main/java/org/codequistify/master/domain/lab/utils/KubernetesResourceNaming.java create mode 100644 src/main/java/org/codequistify/master/domain/lab/vo/KubernetesResourceName.java create mode 100644 src/main/java/org/codequistify/master/domain/lab/vo/LabResourceId.java create mode 100644 src/main/java/org/codequistify/master/domain/lab/vo/LabResourceLabels.java create mode 100644 src/main/java/org/codequistify/master/domain/lab/vo/LabUserUid.java delete mode 100644 src/main/java/org/codequistify/master/domain/lab/vo/Label.java create mode 100644 src/main/java/org/codequistify/master/domain/lab/vo/StageCode.java create mode 100644 src/main/java/org/codequistify/master/domain/player/domain/PlayerId.java create mode 100644 src/main/java/org/codequistify/master/global/data/Label.java create mode 100644 src/main/java/org/codequistify/master/global/data/Labels.java create mode 100644 src/main/java/org/codequistify/master/global/data/Pair.java create mode 100644 src/main/java/org/codequistify/master/global/data/UrlQuery.java diff --git a/build.gradle b/build.gradle index 74dc8a87..b6cc7b4d 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,9 @@ version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '17' + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } configurations { diff --git a/src/main/java/org/codequistify/master/domain/lab/config/LabExternalEndpoints.java b/src/main/java/org/codequistify/master/domain/lab/config/LabExternalEndpoints.java new file mode 100644 index 00000000..c308b711 --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/lab/config/LabExternalEndpoints.java @@ -0,0 +1,30 @@ +package org.codequistify.master.domain.lab.config; + +import org.codequistify.master.global.data.UrlQuery; + +public final class LabExternalEndpoints { + private static final String LAB_DOMAIN = "lab.pol.or.kr"; + private static final String HTTPS_SCHEME = "https://"; + private static final String WSS_SCHEME = "wss://"; + private static final String GRADE_PATH = "/grade"; + private static final String COMPOSE_PATH = "/compose"; + + private LabExternalEndpoints() { + } + + public static String websocketHost() { + return WSS_SCHEME + LAB_DOMAIN; + } + + public static String httpsHost() { + return HTTPS_SCHEME + LAB_DOMAIN; + } + + public static String gradeUrl(UrlQuery query) { + return httpsHost() + GRADE_PATH + query.value(); + } + + public static String composeUrl(UrlQuery query) { + return httpsHost() + COMPOSE_PATH + query.value(); + } +} diff --git a/src/main/java/org/codequistify/master/domain/lab/config/LabInfrastructureDefaults.java b/src/main/java/org/codequistify/master/domain/lab/config/LabInfrastructureDefaults.java new file mode 100644 index 00000000..60edcff3 --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/lab/config/LabInfrastructureDefaults.java @@ -0,0 +1,11 @@ +package org.codequistify.master.domain.lab.config; + +public final class LabInfrastructureDefaults { + public static final String LAB_NAMESPACE = "default"; + public static final int LAB_SERVICE_PORT = 8080; + public static final int LAB_READINESS_PORT = 8080; + public static final String LAB_READINESS_PATH = "/health"; + + private LabInfrastructureDefaults() { + } +} diff --git a/src/main/java/org/codequistify/master/domain/lab/controller/LabController.java b/src/main/java/org/codequistify/master/domain/lab/controller/LabController.java index 9f240503..cc62497e 100644 --- a/src/main/java/org/codequistify/master/domain/lab/controller/LabController.java +++ b/src/main/java/org/codequistify/master/domain/lab/controller/LabController.java @@ -3,12 +3,11 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.codequistify.master.domain.lab.config.LabExternalEndpoints; import org.codequistify.master.domain.lab.dto.PShellCreateResponse; import org.codequistify.master.domain.lab.dto.PShellExistsResponse; import org.codequistify.master.domain.lab.service.LabService; import org.codequistify.master.domain.player.domain.Player; -import org.codequistify.master.domain.stage.domain.Stage; -import org.codequistify.master.domain.stage.service.impl.StageSearchServiceImpl; import org.codequistify.master.global.aspect.LogExecutionTime; import org.codequistify.master.global.aspect.LogMonitoring; import org.codequistify.master.global.lock.LockManager; @@ -29,10 +28,9 @@ @Tag(name = "Lab") public class LabController { private final LabService labService; - private final StageSearchServiceImpl stageSearchService; private final LockManager lockManager; - private final String LAB_HOST = "wss://lab.pol.or.kr"; + private final String LAB_HOST = LabExternalEndpoints.websocketHost(); private final Logger LOGGER = LoggerFactory.getLogger(LabController.class); @@ -54,15 +52,8 @@ public ResponseEntity applyPShell(@AuthenticationPrincipal ReentrantLock lock = lockManager.getLock(player.getId(), stageId); if (lock.tryLock()) { try { - Stage stage = stageSearchService.getStageById(stageId); - - labService.deleteSyncStageOnKubernetes(player, stage); // 동기 삭제 - labService.createStageOnKubernetes(player, stage); - - PShellCreateResponse response = PShellCreateResponse - .of(LAB_HOST, player.getUid(), stage.getStageImage().name().toLowerCase()); - - labService.waitForPodReadiness(player, stage); + PShellCreateResponse response = labService + .recreateStageOnKubernetes(LAB_HOST, stageId, player.getUid()); return ResponseEntity .status(HttpStatus.OK) @@ -93,10 +84,7 @@ public ResponseEntity applyPShell(@AuthenticationPrincipal @LogMonitoring public ResponseEntity getPShellAccessUrl(@AuthenticationPrincipal Player player, @PathVariable(name = "stage_id") Long stageId) { - Stage stage = stageSearchService.getStageById(stageId); - - PShellCreateResponse response = PShellCreateResponse - .of(LAB_HOST, player.getUid(), stage.getStageImage().name().toLowerCase()); + PShellCreateResponse response = labService.getPShellAccessUrl(LAB_HOST, stageId, player.getUid()); return ResponseEntity .status(HttpStatus.OK) @@ -115,16 +103,7 @@ public ResponseEntity getPShellAccessUrl(@AuthenticationPr @GetMapping("/lab/terminal/existence/{stage_id}") public ResponseEntity checkPShellExistence(@AuthenticationPrincipal Player player, @PathVariable(name = "stage_id") Long stageId) { - Stage stage = stageSearchService.getStageById(stageId); - - boolean stageExists = labService.existsStageOnKubernetes(player, stage); - - PShellExistsResponse response = new PShellExistsResponse( - player.getUid(), - stageId, - stage.getStageImage().name(), - stageExists - ); + PShellExistsResponse response = labService.checkPShellExistence(stageId, player.getUid()); return ResponseEntity .status(HttpStatus.OK) diff --git a/src/main/java/org/codequistify/master/domain/lab/dto/PShellCreateResponse.java b/src/main/java/org/codequistify/master/domain/lab/dto/PShellCreateResponse.java index b6ce0c98..4609e274 100644 --- a/src/main/java/org/codequistify/master/domain/lab/dto/PShellCreateResponse.java +++ b/src/main/java/org/codequistify/master/domain/lab/dto/PShellCreateResponse.java @@ -1,14 +1,22 @@ package org.codequistify.master.domain.lab.dto; +import org.codequistify.master.domain.lab.vo.LabUserUid; +import org.codequistify.master.domain.lab.vo.StageCode; +import org.codequistify.master.global.data.Pair; +import org.codequistify.master.global.data.UrlQuery; + public record PShellCreateResponse( String url, String query ) { - public static PShellCreateResponse of (String url, String uid, String stageImageName) { - String query = "?uid=" + uid.toLowerCase() + "&stage=" + stageImageName; + public static PShellCreateResponse of(String url, LabUserUid uid, StageCode stageCode) { + UrlQuery query = UrlQuery.from( + Pair.of("uid", uid.value()), + Pair.of("stage", stageCode.lowercase()) + ); return new PShellCreateResponse( - url+query, - query + url + query.value(), + query.value() ); } diff --git a/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesPodFactory.java b/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesPodFactory.java index f9a59dea..e50abca9 100644 --- a/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesPodFactory.java +++ b/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesPodFactory.java @@ -3,8 +3,11 @@ import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; -import org.codequistify.master.domain.lab.utils.KubernetesResourceNaming; -import org.codequistify.master.domain.stage.domain.Stage; +import org.codequistify.master.domain.lab.config.LabInfrastructureDefaults; +import org.codequistify.master.domain.lab.vo.KubernetesResourceName; +import org.codequistify.master.domain.lab.vo.LabResourceId; +import org.codequistify.master.domain.lab.vo.LabResourceLabels; +import org.codequistify.master.global.data.Labels; import org.codequistify.master.domain.stage.domain.StageImageType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,17 +19,15 @@ public class KubernetesPodFactory implements PodFactory { private final static Long ACTIVE_DEADLINE = 10_800L; @Override - public Pod create(Stage stage, int port, String uid) { - StageImageType stageImage = stage.getStageImage(); - String podName = KubernetesResourceNaming.getPodName(stageImage.name(), uid); + public Pod create(LabResourceId resourceId, int port) { + StageImageType stageImage = resourceId.stage().getStageImage(); + KubernetesResourceName resourceName = resourceId.resourceName(); + Labels labels = LabResourceLabels.standard(resourceId); return new PodBuilder() .withNewMetadata() - .withName(podName) - .addToLabels("app", "pol") - .addToLabels("tire", "term") - .addToLabels("player", uid) - .addToLabels("stage", stageImage.name().toLowerCase()) + .withName(resourceName.podName()) + .withLabels(labels.toSingleValueMap()) .endMetadata() .withNewSpec() .addNewContainer() @@ -37,8 +38,8 @@ public Pod create(Stage stage, int port, String uid) { .endPort() .withNewReadinessProbe()// agent 준비 확인 .withNewHttpGet() - .withPath("/health") - .withPort(new IntOrString(8080)) + .withPath(LabInfrastructureDefaults.LAB_READINESS_PATH) + .withPort(new IntOrString(LabInfrastructureDefaults.LAB_READINESS_PORT)) .endHttpGet() .withInitialDelaySeconds(9) .withPeriodSeconds(2) diff --git a/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesServiceFactory.java b/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesServiceFactory.java index efe710db..5ea67611 100644 --- a/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesServiceFactory.java +++ b/src/main/java/org/codequistify/master/domain/lab/factory/KubernetesServiceFactory.java @@ -3,26 +3,25 @@ import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; -import org.codequistify.master.domain.lab.utils.KubernetesResourceNaming; -import org.codequistify.master.domain.stage.domain.Stage; +import org.codequistify.master.domain.lab.vo.KubernetesResourceName; +import org.codequistify.master.domain.lab.vo.LabResourceId; +import org.codequistify.master.domain.lab.vo.LabResourceLabels; +import org.codequistify.master.global.data.Labels; import org.codequistify.master.domain.stage.domain.StageImageType; import org.springframework.stereotype.Component; @Component public class KubernetesServiceFactory implements ServiceFactory{ - private final static Long ACTIVE_DEADLINE = 10_800L; @Override - public Service create(Stage stage, int port, String uid) { - StageImageType stageImage = stage.getStageImage(); - String serviceName = KubernetesResourceNaming.getServiceName(stageImage.name(), uid); + public Service create(LabResourceId resourceId, int port) { + StageImageType stageImage = resourceId.stage().getStageImage(); + KubernetesResourceName resourceName = resourceId.resourceName(); + Labels labels = LabResourceLabels.standard(resourceId); return new ServiceBuilder() .withNewMetadata() - .withName(serviceName) - .addToLabels("app", "pol") - .addToLabels("tire", "term") - .addToLabels("player", uid) - .addToLabels("stage", stageImage.name().toLowerCase()) + .withName(resourceName.serviceName()) + .withLabels(labels.toSingleValueMap()) .endMetadata() .withNewSpec() .withType("ClusterIP") @@ -32,10 +31,7 @@ public Service create(Stage stage, int port, String uid) { .withPort(port) .withTargetPort(new IntOrString(port)) .endPort() - .addToSelector("app", "pol") - .addToSelector("tire", "term") - .addToSelector("player", uid) - .addToSelector("stage", stageImage.name().toLowerCase()) + .withSelector(labels.toSingleValueMap()) .endSpec().build(); } diff --git a/src/main/java/org/codequistify/master/domain/lab/factory/PodFactory.java b/src/main/java/org/codequistify/master/domain/lab/factory/PodFactory.java index a531e696..a64894cd 100644 --- a/src/main/java/org/codequistify/master/domain/lab/factory/PodFactory.java +++ b/src/main/java/org/codequistify/master/domain/lab/factory/PodFactory.java @@ -1,8 +1,8 @@ package org.codequistify.master.domain.lab.factory; import io.fabric8.kubernetes.api.model.Pod; -import org.codequistify.master.domain.stage.domain.Stage; +import org.codequistify.master.domain.lab.vo.LabResourceId; public interface PodFactory { - Pod create(Stage stage, int port, String uid); + Pod create(LabResourceId resourceId, int port); } diff --git a/src/main/java/org/codequistify/master/domain/lab/factory/ServiceFactory.java b/src/main/java/org/codequistify/master/domain/lab/factory/ServiceFactory.java index a4b46be8..ee13079a 100644 --- a/src/main/java/org/codequistify/master/domain/lab/factory/ServiceFactory.java +++ b/src/main/java/org/codequistify/master/domain/lab/factory/ServiceFactory.java @@ -1,8 +1,8 @@ package org.codequistify.master.domain.lab.factory; import io.fabric8.kubernetes.api.model.Service; -import org.codequistify.master.domain.stage.domain.Stage; +import org.codequistify.master.domain.lab.vo.LabResourceId; public interface ServiceFactory { - Service create(Stage stage, int port, String uid); + Service create(LabResourceId resourceId, int port); } diff --git a/src/main/java/org/codequistify/master/domain/lab/service/KubernetesResourceManager.java b/src/main/java/org/codequistify/master/domain/lab/service/KubernetesResourceManager.java index 64695a2d..887a82a8 100644 --- a/src/main/java/org/codequistify/master/domain/lab/service/KubernetesResourceManager.java +++ b/src/main/java/org/codequistify/master/domain/lab/service/KubernetesResourceManager.java @@ -6,13 +6,12 @@ import io.fabric8.kubernetes.api.model.StatusDetails; import io.fabric8.kubernetes.client.KubernetesClient; import lombok.RequiredArgsConstructor; +import org.codequistify.master.domain.lab.config.LabInfrastructureDefaults; import org.codequistify.master.domain.lab.factory.PodFactory; import org.codequistify.master.domain.lab.factory.ServiceFactory; -import org.codequistify.master.domain.lab.utils.KubernetesResourceNaming; -import org.codequistify.master.domain.stage.domain.Stage; +import org.codequistify.master.domain.lab.vo.LabResourceId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Bean; import java.util.List; @@ -26,11 +25,11 @@ public class KubernetesResourceManager { private final KubernetesClient kubernetesClient; - public Service createServiceOnKubernetes(Stage stage, String uid) { - Service service = serviceFactory.create(stage, 8080, uid); + public Service createServiceOnKubernetes(LabResourceId resourceId) { + Service service = serviceFactory.create(resourceId, LabInfrastructureDefaults.LAB_SERVICE_PORT); service = kubernetesClient.services() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .resource(service) .create(); @@ -38,11 +37,11 @@ public Service createServiceOnKubernetes(Stage stage, String uid) { return service; } - public Pod createPodOnKubernetes(Stage stage, String uid) { - Pod pod = podFactory.create(stage, 8080, uid); + public Pod createPodOnKubernetes(LabResourceId resourceId) { + Pod pod = podFactory.create(resourceId, LabInfrastructureDefaults.LAB_SERVICE_PORT); pod = kubernetesClient.pods() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .resource(pod) .create(); @@ -50,11 +49,11 @@ public Pod createPodOnKubernetes(Stage stage, String uid) { return pod; } - public void deleteAsyncService(Stage stage, String uid) { - String svcName = KubernetesResourceNaming.getServiceName(stage.getStageImage().name(), uid); + public void deleteAsyncService(LabResourceId resourceId) { + String svcName = resourceId.resourceName().serviceName(); List result = kubernetesClient.services() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(svcName) .delete(); @@ -63,18 +62,18 @@ public void deleteAsyncService(Stage stage, String uid) { public void deleteAsyncService(String svcName) { List result = kubernetesClient.services() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(svcName) .delete(); LOGGER.debug("[deleteAsyncService] result {}", result); } - public void deleteAsyncPod(Stage stage, String uid) { - String podName = KubernetesResourceNaming.getPodName(stage.getStageImage().name(), uid); + public void deleteAsyncPod(LabResourceId resourceId) { + String podName = resourceId.resourceName().podName(); List result = kubernetesClient.pods() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(podName) .delete(); @@ -83,18 +82,18 @@ public void deleteAsyncPod(Stage stage, String uid) { public void deleteAsyncPod(String podName) { List result = kubernetesClient.pods() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(podName) .delete(); LOGGER.debug("[deleteAsyncPod] result {}", result); } - public Service getService(Stage stage, String uid) { - String svcName = KubernetesResourceNaming.getServiceName(stage.getStageImage().name(), uid); + public Service getService(LabResourceId resourceId) { + String svcName = resourceId.resourceName().serviceName(); Service service = kubernetesClient.services() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(svcName) .get(); @@ -102,11 +101,11 @@ public Service getService(Stage stage, String uid) { return service; } - public Pod getPod(Stage stage, String uid) { - String podName = KubernetesResourceNaming.getPodName(stage.getStageImage().name(), uid); + public Pod getPod(LabResourceId resourceId) { + String podName = resourceId.resourceName().podName(); Pod pod = kubernetesClient.pods() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(podName) .get(); @@ -114,11 +113,11 @@ public Pod getPod(Stage stage, String uid) { return pod; } - public boolean existsService(Stage stage, String uid) { - String svcName = KubernetesResourceNaming.getServiceName(stage.getStageImage().name(), uid); + public boolean existsService(LabResourceId resourceId) { + String svcName = resourceId.resourceName().serviceName(); boolean exists = kubernetesClient.services() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(svcName) .get() != null; @@ -126,11 +125,11 @@ public boolean existsService(Stage stage, String uid) { return exists; } - public boolean existsPod(Stage stage, String uid) { - String podName = KubernetesResourceNaming.getPodName(stage.getStageImage().name(), uid); + public boolean existsPod(LabResourceId resourceId) { + String podName = resourceId.resourceName().podName(); boolean exists = kubernetesClient.pods() - .inNamespace("default") + .inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE) .withName(podName) .get() != null; @@ -139,7 +138,7 @@ public boolean existsPod(Stage stage, String uid) { } public List getTimeOutPods() { - PodList podList = kubernetesClient.pods().inNamespace("default").list(); + PodList podList = kubernetesClient.pods().inNamespace(LabInfrastructureDefaults.LAB_NAMESPACE).list(); return podList.getItems().stream() .filter(this::isPhaseFailed) diff --git a/src/main/java/org/codequistify/master/domain/lab/service/LabAssignmentService.java b/src/main/java/org/codequistify/master/domain/lab/service/LabAssignmentService.java index be1f9d97..e70582c5 100644 --- a/src/main/java/org/codequistify/master/domain/lab/service/LabAssignmentService.java +++ b/src/main/java/org/codequistify/master/domain/lab/service/LabAssignmentService.java @@ -1,9 +1,13 @@ package org.codequistify.master.domain.lab.service; import lombok.RequiredArgsConstructor; -import org.codequistify.master.domain.lab.utils.KubernetesResourceNaming; +import org.codequistify.master.domain.lab.config.LabExternalEndpoints; +import org.codequistify.master.domain.lab.vo.KubernetesResourceName; +import org.codequistify.master.domain.lab.vo.LabUserUid; +import org.codequistify.master.domain.lab.vo.StageCode; import org.codequistify.master.domain.stage.domain.StageImageType; import org.codequistify.master.domain.stage.dto.StageActionRequest; +import org.codequistify.master.global.data.UrlQuery; import org.codequistify.master.global.aspect.LogExecutionTime; import org.codequistify.master.global.exception.ErrorCode; import org.codequistify.master.global.exception.domain.BusinessException; @@ -29,15 +33,16 @@ public class LabAssignmentService { public void testA() { String stageCode = StageImageType.S1015.name(); String uid = "pol-bdbeej-gj5antzprz"; - //String qUrl = KubernetesResourceNaming.getQuery(stageCode, uid); - System.out.println("https://lab.pol.or.kr/grade"+KubernetesResourceNaming.getQuery(stageCode, uid)); - System.out.println("https://lab.pol.or.kr/compose"+KubernetesResourceNaming.getQuery(stageCode, uid)); + KubernetesResourceName resourceName = KubernetesResourceName.of(StageCode.from(stageCode), LabUserUid.from(uid)); + UrlQuery query = resourceName.query(); + System.out.println(LabExternalEndpoints.gradeUrl(query)); + System.out.println(LabExternalEndpoints.composeUrl(query)); } @LogExecutionTime public ResponseEntity sendGradingRequest(String stageCode, String uid, StageActionRequest request) { - //String svcName = KubernetesResourceNaming.getServiceName(stageCode, uid); - String url = "https://lab.pol.or.kr/grade" + KubernetesResourceNaming.getQuery(stageCode, uid); + KubernetesResourceName resourceName = KubernetesResourceName.of(StageCode.from(stageCode), LabUserUid.from(uid)); + String url = LabExternalEndpoints.gradeUrl(resourceName.query()); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -70,7 +75,8 @@ public ResponseEntity sendGradingRequest(String stageCode, Stri @LogExecutionTime public ResponseEntity sendComposeRequest(String stageCode, String uid, StageActionRequest request) { - String url = "https://lab.pol.or.kr/compose"+KubernetesResourceNaming.getQuery(stageCode, uid); + KubernetesResourceName resourceName = KubernetesResourceName.of(StageCode.from(stageCode), LabUserUid.from(uid)); + String url = LabExternalEndpoints.composeUrl(resourceName.query()); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); diff --git a/src/main/java/org/codequistify/master/domain/lab/service/LabService.java b/src/main/java/org/codequistify/master/domain/lab/service/LabService.java index 0761b077..5cd37f0c 100644 --- a/src/main/java/org/codequistify/master/domain/lab/service/LabService.java +++ b/src/main/java/org/codequistify/master/domain/lab/service/LabService.java @@ -3,8 +3,11 @@ import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodCondition; import lombok.RequiredArgsConstructor; -import org.codequistify.master.domain.player.domain.Player; +import org.codequistify.master.domain.lab.dto.PShellCreateResponse; +import org.codequistify.master.domain.lab.dto.PShellExistsResponse; +import org.codequistify.master.domain.lab.vo.LabResourceId; import org.codequistify.master.domain.stage.domain.Stage; +import org.codequistify.master.domain.stage.service.StageSearchService; import org.codequistify.master.global.aspect.LogExecutionTime; import org.codequistify.master.global.exception.ErrorCode; import org.codequistify.master.global.exception.domain.BusinessException; @@ -16,46 +19,75 @@ @RequiredArgsConstructor public class LabService { private final KubernetesResourceManager kubernetesResourceManager; + private final StageSearchService stageSearchService; private final Logger LOGGER = LoggerFactory.getLogger(LabService.class); private final static int THRESHOLD = 20; private final static int SLEEP_PERIOD = 5000; @LogExecutionTime - public void createStageOnKubernetes(Player player, Stage stage){ - String uid = player.getUid().toLowerCase(); + public PShellCreateResponse recreateStageOnKubernetes(String labHost, Long stageId, String playerUid) { + LabResourceId labResourceId = resolveResourceId(stageId, playerUid); - kubernetesResourceManager.createServiceOnKubernetes(stage, uid); - kubernetesResourceManager.createPodOnKubernetes(stage, uid); + deleteSyncStageOnKubernetes(labResourceId); + createStageOnKubernetes(labResourceId); - LOGGER.info("[createStageOnKubernetes] stage: {}", stage.getId()); + LOGGER.info("[createStageOnKubernetes] stage: {}", labResourceId.stage().getId()); + waitForPodReadiness(labResourceId); + return PShellCreateResponse.of(labHost, labResourceId.uid(), labResourceId.stageCode()); } @LogExecutionTime - public void deleteAsyncStageOnKubernetes(Player player, Stage stage) { - String uid = player.getUid().toLowerCase(); - - kubernetesResourceManager.deleteAsyncPod(stage, uid); - kubernetesResourceManager.deleteAsyncService(stage, uid); + public PShellCreateResponse getPShellAccessUrl(String labHost, Long stageId, String playerUid) { + LabResourceId labResourceId = resolveResourceId(stageId, playerUid); + return PShellCreateResponse.of(labHost, labResourceId.uid(), labResourceId.stageCode()); } @LogExecutionTime - public void deleteSyncStageOnKubernetes(Player player, Stage stage) { - String uid = player.getUid().toLowerCase(); + public PShellExistsResponse checkPShellExistence(Long stageId, String playerUid) { + LabResourceId labResourceId = resolveResourceId(stageId, playerUid); + + boolean podExists = kubernetesResourceManager.existsPod(labResourceId); + boolean serviceExists = kubernetesResourceManager.existsService(labResourceId); + + LOGGER.info("[existsStageOnKubernetes] pod: {}, svc: {}", podExists, serviceExists); + + return new PShellExistsResponse( + labResourceId.uid().value(), + stageId, + labResourceId.stage().getStageImage().name(), + podExists && serviceExists + ); + } + + private LabResourceId resolveResourceId(Long stageId, String playerUid) { + Stage stage = stageSearchService.getStageById(stageId); + return LabResourceId.from(stage, playerUid); + } + + private void createStageOnKubernetes(LabResourceId labResourceId) { + kubernetesResourceManager.createServiceOnKubernetes(labResourceId); + kubernetesResourceManager.createPodOnKubernetes(labResourceId); + } + + private void deleteAsyncStageOnKubernetes(LabResourceId labResourceId) { + kubernetesResourceManager.deleteAsyncPod(labResourceId); + kubernetesResourceManager.deleteAsyncService(labResourceId); + } - kubernetesResourceManager.deleteAsyncPod(stage, uid); - kubernetesResourceManager.deleteAsyncService(stage, uid); + private void deleteSyncStageOnKubernetes(LabResourceId labResourceId) { + deleteAsyncStageOnKubernetes(labResourceId); boolean podDeleted = false; boolean serviceDeleted = false; int retryCount = 0; while (!podDeleted || !serviceDeleted) { - if (!podDeleted && !kubernetesResourceManager.existsPod(stage, uid)) { + if (!podDeleted && !kubernetesResourceManager.existsPod(labResourceId)) { podDeleted = true; LOGGER.info("[deleteSyncStageOnKubernetes] Pod 삭제 확인 {}번 시도", retryCount); } - if (!serviceDeleted && !kubernetesResourceManager.existsService(stage, uid)) { + if (!serviceDeleted && !kubernetesResourceManager.existsService(labResourceId)) { serviceDeleted = true; LOGGER.info("[deleteSyncStageOnKubernetes] Service 삭제 확인 {}번 시도", retryCount); } @@ -74,21 +106,10 @@ public void deleteSyncStageOnKubernetes(Player player, Stage stage) { } } - @LogExecutionTime - public boolean existsStageOnKubernetes(Player player, Stage stage) { - boolean podExists = kubernetesResourceManager.existsPod(stage, player.getUid()); - boolean serviceExists = kubernetesResourceManager.existsService(stage, player.getUid()); - - LOGGER.info("[existsStageOnKubernetes] pod: {}, svc: {}", podExists, serviceExists); - return podExists && serviceExists; - } - - @LogExecutionTime - public void waitForPodReadiness(Player player, Stage stage) { - String uid = player.getUid().toLowerCase(); + private void waitForPodReadiness(LabResourceId labResourceId) { int retryCount = 0; while (true) { - Pod pod = kubernetesResourceManager.getPod(stage, uid); + Pod pod = kubernetesResourceManager.getPod(labResourceId); if (pod != null && pod.getStatus() != null && pod.getStatus().getConditions() != null) { for (PodCondition condition : pod.getStatus().getConditions()) { if ("Ready".equals(condition.getType()) && "True".equals(condition.getStatus())) { diff --git a/src/main/java/org/codequistify/master/domain/lab/utils/KubernetesResourceNaming.java b/src/main/java/org/codequistify/master/domain/lab/utils/KubernetesResourceNaming.java deleted file mode 100644 index fa97b7c6..00000000 --- a/src/main/java/org/codequistify/master/domain/lab/utils/KubernetesResourceNaming.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.codequistify.master.domain.lab.utils; - -public class KubernetesResourceNaming { - //private static final String POD_NAME_FORMAT = "%s-pod-%s"; - private static final String POD_NAME_FORMAT = "%s-%s"; - //private static final String SERVICE_NAME_FORMAT = "%s-svc-%s"; - private static final String SERVICE_NAME_FORMAT = "%s-%s"; - private static final String SERVICE_DNS_FORMAT = "%s.%s.svc.cluster.local"; - - private static final String QUERY_URL_FORMAT = "?uid=%s&stage=%s"; - - public static String getPodName(String stageCode, String uid) { - return String.format(POD_NAME_FORMAT, stageCode.toLowerCase(), uid.toLowerCase()); - } - - public static String getServiceName(String stageCode, String uid) { - return String.format(SERVICE_NAME_FORMAT, stageCode.toLowerCase(), uid.toLowerCase()); - } - - public static String getServiceDNS(String svcName, String namespace) { - return String.format(SERVICE_DNS_FORMAT, svcName, namespace); - } - - public static String getQuery(String stageCode, String uid) { - return String.format(QUERY_URL_FORMAT, uid, stageCode.toLowerCase()); - } -} diff --git a/src/main/java/org/codequistify/master/domain/lab/vo/KubernetesResourceName.java b/src/main/java/org/codequistify/master/domain/lab/vo/KubernetesResourceName.java new file mode 100644 index 00000000..6243920d --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/lab/vo/KubernetesResourceName.java @@ -0,0 +1,37 @@ +package org.codequistify.master.domain.lab.vo; + +import org.codequistify.master.global.data.Pair; +import org.codequistify.master.global.data.UrlQuery; + +public record KubernetesResourceName( + StageCode stageCode, + LabUserUid uid +) { + private static final String POD_NAME_FORMAT = "%s-%s"; + private static final String SERVICE_NAME_FORMAT = "%s-%s"; + private static final String SERVICE_DNS_FORMAT = "%s.%s.svc.cluster.local"; + + public static KubernetesResourceName of(StageCode stageCode, LabUserUid uid) { + return new KubernetesResourceName(stageCode, uid); + } + + public String podName() { + return POD_NAME_FORMAT.formatted(stageCode.lowercase(), uid.value()); + } + + public String serviceName() { + return SERVICE_NAME_FORMAT.formatted(stageCode.lowercase(), uid.value()); + } + + public String serviceDns(String namespace) { + return SERVICE_DNS_FORMAT.formatted(serviceName(), namespace); + } + + public UrlQuery query() { + return UrlQuery.from( + Pair.of("uid", uid.value()), + Pair.of("stage", stageCode.lowercase()) + ); + } +} + diff --git a/src/main/java/org/codequistify/master/domain/lab/vo/LabResourceId.java b/src/main/java/org/codequistify/master/domain/lab/vo/LabResourceId.java new file mode 100644 index 00000000..07b0ce39 --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/lab/vo/LabResourceId.java @@ -0,0 +1,31 @@ +package org.codequistify.master.domain.lab.vo; + +import org.codequistify.master.domain.player.domain.Player; +import org.codequistify.master.domain.stage.domain.Stage; + +import java.util.Objects; + +public record LabResourceId( + Stage stage, + StageCode stageCode, + LabUserUid uid +) { + public LabResourceId { + Objects.requireNonNull(stage, "stage must not be null"); + Objects.requireNonNull(stageCode, "stageCode must not be null"); + Objects.requireNonNull(uid, "uid must not be null"); + } + + public static LabResourceId from(Stage stage, Player player) { + return new LabResourceId(stage, StageCode.from(stage), LabUserUid.from(player)); + } + + public static LabResourceId from(Stage stage, String playerUid) { + return new LabResourceId(stage, StageCode.from(stage), LabUserUid.from(playerUid)); + } + + public KubernetesResourceName resourceName() { + return KubernetesResourceName.of(stageCode, uid); + } +} + diff --git a/src/main/java/org/codequistify/master/domain/lab/vo/LabResourceLabels.java b/src/main/java/org/codequistify/master/domain/lab/vo/LabResourceLabels.java new file mode 100644 index 00000000..b5253acf --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/lab/vo/LabResourceLabels.java @@ -0,0 +1,17 @@ +package org.codequistify.master.domain.lab.vo; + +import org.codequistify.master.global.data.Label; +import org.codequistify.master.global.data.Labels; + +public class LabResourceLabels { + private LabResourceLabels() {} + + public static Labels standard(LabResourceId resourceId) { + return Labels.of( + Label.of("app", "pol"), + Label.of("tire", "term"), + Label.of("player", resourceId.uid().value()), + Label.of("stage", resourceId.stageCode().lowercase()) + ); + } +} diff --git a/src/main/java/org/codequistify/master/domain/lab/vo/LabUserUid.java b/src/main/java/org/codequistify/master/domain/lab/vo/LabUserUid.java new file mode 100644 index 00000000..ef92a76f --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/lab/vo/LabUserUid.java @@ -0,0 +1,22 @@ +package org.codequistify.master.domain.lab.vo; + +import org.codequistify.master.domain.player.domain.Player; + +import java.util.Locale; +import java.util.Objects; + +public record LabUserUid(String value) { + public LabUserUid { + Objects.requireNonNull(value, "uid must not be null"); + value = value.toLowerCase(Locale.ROOT); + } + + public static LabUserUid from(Player player) { + return new LabUserUid(player.getUid()); + } + + public static LabUserUid from(String value) { + return new LabUserUid(value); + } +} + diff --git a/src/main/java/org/codequistify/master/domain/lab/vo/Label.java b/src/main/java/org/codequistify/master/domain/lab/vo/Label.java deleted file mode 100644 index f6efc434..00000000 --- a/src/main/java/org/codequistify/master/domain/lab/vo/Label.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.codequistify.master.domain.lab.vo; - -public record Label( - String key, - String value -) { -} diff --git a/src/main/java/org/codequistify/master/domain/lab/vo/StageCode.java b/src/main/java/org/codequistify/master/domain/lab/vo/StageCode.java new file mode 100644 index 00000000..c5df010f --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/lab/vo/StageCode.java @@ -0,0 +1,30 @@ +package org.codequistify.master.domain.lab.vo; + +import org.codequistify.master.domain.stage.domain.Stage; +import org.codequistify.master.domain.stage.domain.StageImageType; + +import java.util.Locale; +import java.util.Objects; + +public record StageCode(String value) { + public StageCode { + Objects.requireNonNull(value, "stageCode must not be null"); + } + + public static StageCode from(Stage stage) { + return from(stage.getStageImage()); + } + + public static StageCode from(StageImageType stageImageType) { + return new StageCode(stageImageType.name()); + } + + public static StageCode from(String stageCode) { + return new StageCode(stageCode); + } + + public String lowercase() { + return value.toLowerCase(Locale.ROOT); + } +} + diff --git a/src/main/java/org/codequistify/master/domain/player/domain/Player.java b/src/main/java/org/codequistify/master/domain/player/domain/Player.java index 64e6d82d..c910ebb8 100644 --- a/src/main/java/org/codequistify/master/domain/player/domain/Player.java +++ b/src/main/java/org/codequistify/master/domain/player/domain/Player.java @@ -33,6 +33,9 @@ public class Player extends BaseTimeEntity implements UserDetails { @Column(name = "player_id") private Long id; + @Transient + private PlayerId playerId; + @Column(name = "uid", unique = true) private String uid; // pol 고유 식별 번호 @@ -112,6 +115,13 @@ public void expireAccount() { this.locked = true; } + public PlayerId id() { + if (playerId == null && id != null) { + playerId = PlayerId.of(id); + } + return playerId; + } + public void dataClear() { this.name = null; this.email = null; @@ -174,6 +184,14 @@ protected void onPrePersist() { addRoles(List.of(PlayerRoleType.PLAYER), List.of(PlayerAccessType.BASIC_PROBLEMS_ACCESS)); } + @PostLoad + @PostPersist + private void syncPlayerId() { + if (id != null) { + this.playerId = PlayerId.of(id); + } + } + private String generateUID() { return UidGenerator.generate(this.email); } diff --git a/src/main/java/org/codequistify/master/domain/player/domain/PlayerId.java b/src/main/java/org/codequistify/master/domain/player/domain/PlayerId.java new file mode 100644 index 00000000..221b7089 --- /dev/null +++ b/src/main/java/org/codequistify/master/domain/player/domain/PlayerId.java @@ -0,0 +1,16 @@ +package org.codequistify.master.domain.player.domain; + +import jakarta.persistence.Embeddable; + +import java.util.Objects; + +@Embeddable +public record PlayerId(Long value) { + public PlayerId { + Objects.requireNonNull(value, "player id must not be null"); + } + + public static PlayerId of(Long value) { + return new PlayerId(value); + } +} diff --git a/src/main/java/org/codequistify/master/domain/player/service/PlayerDetailsService.java b/src/main/java/org/codequistify/master/domain/player/service/PlayerDetailsService.java index 6dc8d40f..21399f5d 100644 --- a/src/main/java/org/codequistify/master/domain/player/service/PlayerDetailsService.java +++ b/src/main/java/org/codequistify/master/domain/player/service/PlayerDetailsService.java @@ -54,7 +54,7 @@ public void resetPassword(Player player, UpdatePasswordRequest request) { @LogMonitoring public void updatePassword(Player player, UpdatePasswordRequest request) { // 비밀번호 데이터는 담기지 않으므로 다시 조회해야함 - player = playerRepository.getReferenceById(player.getId()); // jwt 필터에서 null 아닌 player 객체만 들어옴 + player = playerRepository.getReferenceById(player.id().value()); // jwt 필터에서 null 아닌 player 객체만 들어옴 if (!player.decodePassword(request.rawPassword(), passwordEncoder)) { throw new BusinessException(ErrorCode.INVALID_EMAIL_OR_PASSWORD, HttpStatus.BAD_REQUEST); diff --git a/src/main/java/org/codequistify/master/domain/player/service/PlayerProfileService.java b/src/main/java/org/codequistify/master/domain/player/service/PlayerProfileService.java index e58351b5..56721fda 100644 --- a/src/main/java/org/codequistify/master/domain/player/service/PlayerProfileService.java +++ b/src/main/java/org/codequistify/master/domain/player/service/PlayerProfileService.java @@ -34,19 +34,19 @@ public List findAllPlayerProfiles() { @LogExecutionTime @Transactional public PlayerStageProgressResponse getCompletedStagesByPlayerId(Player player) { - return stageSearchService.getCompletedStagesByPlayerId(player.getId()); + return stageSearchService.getCompletedStagesByPlayerId(player.id()); } @LogExecutionTime @Transactional public PlayerStageProgressResponse getInProgressStagesByPlayerId(Player player) { - return stageSearchService.getInProgressStagesByPlayerId(player.getId()); + return stageSearchService.getInProgressStagesByPlayerId(player.id()); } @LogExecutionTime @Transactional public List getHeatMapDataPointsByModifiedDate(Player player) { - return stageSearchService.getHeatMapDataPointsByModifiedDate(player.getId()); + return stageSearchService.getHeatMapDataPointsByModifiedDate(player.id()); } public boolean isAdmin(Player player) { diff --git a/src/main/java/org/codequistify/master/domain/stage/service/StageSearchService.java b/src/main/java/org/codequistify/master/domain/stage/service/StageSearchService.java index 4dbc752d..a7e9354c 100644 --- a/src/main/java/org/codequistify/master/domain/stage/service/StageSearchService.java +++ b/src/main/java/org/codequistify/master/domain/stage/service/StageSearchService.java @@ -1,6 +1,7 @@ package org.codequistify.master.domain.stage.service; import org.codequistify.master.domain.player.domain.Player; +import org.codequistify.master.domain.player.domain.PlayerId; import org.codequistify.master.domain.player.dto.PlayerStageProgressResponse; import org.codequistify.master.domain.stage.domain.Stage; import org.codequistify.master.domain.stage.dto.*; @@ -18,12 +19,12 @@ public interface StageSearchService { QuestionResponse findQuestion(Long stageId, Integer questionIndex); // 완료한 스테이지 목록 조회 - PlayerStageProgressResponse getCompletedStagesByPlayerId(Long playerId); + PlayerStageProgressResponse getCompletedStagesByPlayerId(PlayerId playerId); // 진행중인 스테이지 목록 조회 - PlayerStageProgressResponse getInProgressStagesByPlayerId(Long playerId); + PlayerStageProgressResponse getInProgressStagesByPlayerId(PlayerId playerId); // 완료한 날짜/횟수 기록 조회 - List getHeatMapDataPointsByModifiedDate(Long playerId); + List getHeatMapDataPointsByModifiedDate(PlayerId playerId); //preview 메서드 StageResponse getStageByChoCho(String src); diff --git a/src/main/java/org/codequistify/master/domain/stage/service/impl/StageSearchServiceImpl.java b/src/main/java/org/codequistify/master/domain/stage/service/impl/StageSearchServiceImpl.java index 5422b6d4..c022c8b2 100644 --- a/src/main/java/org/codequistify/master/domain/stage/service/impl/StageSearchServiceImpl.java +++ b/src/main/java/org/codequistify/master/domain/stage/service/impl/StageSearchServiceImpl.java @@ -8,6 +8,7 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.codequistify.master.domain.player.domain.Player; +import org.codequistify.master.domain.player.domain.PlayerId; import org.codequistify.master.domain.player.dto.PlayerStageProgressResponse; import org.codequistify.master.domain.stage.convertoer.QuestionConverter; import org.codequistify.master.domain.stage.convertoer.StageConverter; @@ -116,20 +117,20 @@ public StageResponse getStageBySearchText(String query) { @Override // 완료 스테이지 조회 @Transactional - public PlayerStageProgressResponse getCompletedStagesByPlayerId(Long playerId) { - return new PlayerStageProgressResponse(completedStageRepository.findCompletedStagesByPlayerId(playerId)); + public PlayerStageProgressResponse getCompletedStagesByPlayerId(PlayerId playerId) { + return new PlayerStageProgressResponse(completedStageRepository.findCompletedStagesByPlayerId(playerId.value())); } @Override // 진행중 스테이지 조회 @Transactional - public PlayerStageProgressResponse getInProgressStagesByPlayerId(Long playerId) { - return new PlayerStageProgressResponse(completedStageRepository.findInProgressStagesByPlayerId(playerId)); + public PlayerStageProgressResponse getInProgressStagesByPlayerId(PlayerId playerId) { + return new PlayerStageProgressResponse(completedStageRepository.findInProgressStagesByPlayerId(playerId.value())); } @Override // 수정일 기준 데이터 조회 @Transactional - public List getHeatMapDataPointsByModifiedDate(Long playerId) { - return completedStageRepository.countDataByModifiedDate(playerId); + public List getHeatMapDataPointsByModifiedDate(PlayerId playerId) { + return completedStageRepository.countDataByModifiedDate(playerId.value()); } @Override // 입력 쿼리를 기반으로 일치하는 문제 조건 검색 diff --git a/src/main/java/org/codequistify/master/global/data/Label.java b/src/main/java/org/codequistify/master/global/data/Label.java new file mode 100644 index 00000000..95cebeca --- /dev/null +++ b/src/main/java/org/codequistify/master/global/data/Label.java @@ -0,0 +1,28 @@ +package org.codequistify.master.global.data; + +import java.util.List; +import java.util.Objects; + +public record Label(String key, List values) { + public Label { + Objects.requireNonNull(key, "key must not be null"); + Objects.requireNonNull(values, "values must not be null"); + values = List.copyOf(values); + } + + public static Label of(String key, String... values) { + return new Label(key, List.of(values)); + } + + public static Label of(String key, List values) { + return new Label(key, values); + } + + public String firstValue() { + return values.stream().findFirst().orElse(""); + } + + public Pair firstValuePair() { + return Pair.of(key, firstValue()); + } +} diff --git a/src/main/java/org/codequistify/master/global/data/Labels.java b/src/main/java/org/codequistify/master/global/data/Labels.java new file mode 100644 index 00000000..beca8c56 --- /dev/null +++ b/src/main/java/org/codequistify/master/global/data/Labels.java @@ -0,0 +1,48 @@ +package org.codequistify.master.global.data; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public record Labels(List