diff --git a/src/main/java/api/goraebab/domain/blueprint/controller/BlueprintController.java b/src/main/java/api/goraebab/domain/blueprint/controller/BlueprintController.java index c264c7a..667432c 100644 --- a/src/main/java/api/goraebab/domain/blueprint/controller/BlueprintController.java +++ b/src/main/java/api/goraebab/domain/blueprint/controller/BlueprintController.java @@ -3,6 +3,7 @@ import api.goraebab.domain.blueprint.dto.BlueprintReqDto; import api.goraebab.domain.blueprint.dto.BlueprintResDto; import api.goraebab.domain.blueprint.dto.BlueprintsResDto; +import api.goraebab.domain.blueprint.dto.SyncResultDto; import api.goraebab.domain.blueprint.service.BlueprintServiceImpl; import api.goraebab.global.exception.ErrorResponse; import io.swagger.v3.oas.annotations.Operation; @@ -171,11 +172,11 @@ public ResponseEntity getBlueprint(@RequestParam(required = fal "{\"status\": \"CONTAINER_CREATION_FAILED\", \"code\": 500, \"message\": \"Failed to create the specified Docker container.\", \"errors\": []}")) ) }) - public ResponseEntity saveBlueprint(@RequestParam(required = false) @Schema(description = "The unique identifier of the storage. If null, the blueprint is considered to be in the local storage.") Long storageId, + public ResponseEntity saveBlueprint(@RequestParam(required = false) @Schema(description = "The unique identifier of the storage. If null, the blueprint is considered to be in the local storage.") Long storageId, @RequestBody @Valid BlueprintReqDto blueprintReqDto) { - blueprintService.saveBlueprint(storageId, blueprintReqDto); + SyncResultDto syncResultDto = blueprintService.saveBlueprint(storageId, blueprintReqDto); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(syncResultDto); } @Operation(summary = "Modify the blueprint", @@ -214,12 +215,13 @@ public ResponseEntity saveBlueprint(@RequestParam(required = false) @Schem "{\"status\": \"MODIFY_FAILED\", \"code\": 500, \"message\": \"Failed to modify blueprint.\", \"errors\": []}")) ) }) - public ResponseEntity modifyBlueprint(@RequestParam(required = false) @Schema(description = "The unique identifier of the storage. If null, the blueprint is considered to be in the local storage.") Long storageId, + public ResponseEntity modifyBlueprint(@RequestParam(required = false) @Schema(description = "The unique identifier of the storage. If null, the blueprint is considered to be in the local storage.") Long storageId, @PathVariable @Schema(description = "The unique identifier of the blueprint.") Long blueprintId, @RequestBody @Valid BlueprintReqDto blueprintReqDto) { - blueprintService.modifyBlueprint(storageId, blueprintId, blueprintReqDto); + SyncResultDto syncResultDto = blueprintService.modifyBlueprint(storageId, blueprintId, + blueprintReqDto); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(syncResultDto); } @Operation(summary = "Delete the blueprint", diff --git a/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomContainer.java b/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomContainer.java index b56887d..8482e25 100644 --- a/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomContainer.java +++ b/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomContainer.java @@ -10,6 +10,8 @@ @NoArgsConstructor public class CustomContainer { + private String containerId; + private String containerName; @JsonProperty("image") @@ -32,9 +34,10 @@ public class CustomContainer { @Builder - public CustomContainer(String containerName, CustomImage customImage, + public CustomContainer(String containerId, String containerName, CustomImage customImage, CustomNetworkSettings customNetworkSettings, List customPorts, List customMounts, List customEnv, List customCmd) { + this.containerId = containerId; this.containerName = containerName; this.customImage = customImage; this.customNetworkSettings = customNetworkSettings; diff --git a/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomHost.java b/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomHost.java index e7dfa5d..1d96713 100644 --- a/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomHost.java +++ b/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomHost.java @@ -10,21 +10,25 @@ @NoArgsConstructor public class CustomHost { + private String id; + @JsonProperty("network") private List customNetwork; @JsonProperty("volume") private List customVolume; - private Boolean isLocal; + private Boolean isRemote; private String ip; @Builder - public CustomHost(List customNetwork, List customVolume, Boolean isLocal, String ip) { + public CustomHost(String id, List customNetwork, List customVolume, + Boolean isRemote, String ip) { + this.id = id; this.customNetwork = customNetwork; this.customVolume = customVolume; - this.isLocal = isLocal; + this.isRemote = isRemote; this.ip = ip; } diff --git a/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomNetwork.java b/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomNetwork.java index 31718a1..6f87792 100644 --- a/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomNetwork.java +++ b/src/main/java/api/goraebab/domain/blueprint/dockerObject/CustomNetwork.java @@ -10,6 +10,8 @@ @NoArgsConstructor public class CustomNetwork { + private String id; + private String name; private String driver; @@ -22,7 +24,9 @@ public class CustomNetwork { @Builder - public CustomNetwork(String name, String driver, CustomIpam customIpam, List customContainers) { + public CustomNetwork(String id, String name, String driver, CustomIpam customIpam, + List customContainers) { + this.id = id; this.name = name; this.driver = driver; this.customIpam = customIpam; diff --git a/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintResDto.java b/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintResDto.java index ed8d404..b0ca522 100644 --- a/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintResDto.java +++ b/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintResDto.java @@ -1,7 +1,7 @@ package api.goraebab.domain.blueprint.dto; +import api.goraebab.domain.blueprint.dockerObject.ProcessedData; import api.goraebab.domain.remote.database.dto.StorageResDto; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -15,7 +15,7 @@ public class BlueprintResDto extends BlueprintsResDto { private StorageResDto storageInfo; - public BlueprintResDto(Long blueprintId, String name, String data, Boolean isRemote, LocalDateTime dateCreated, LocalDateTime dateUpdated, StorageResDto storageInfo) { + public BlueprintResDto(Long blueprintId, String name, ProcessedData data, Boolean isRemote, LocalDateTime dateCreated, LocalDateTime dateUpdated, StorageResDto storageInfo) { super(blueprintId, name, data, isRemote, dateCreated, dateUpdated); this.storageInfo = storageInfo; } diff --git a/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintsResDto.java b/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintsResDto.java index e901732..af80575 100644 --- a/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintsResDto.java +++ b/src/main/java/api/goraebab/domain/blueprint/dto/BlueprintsResDto.java @@ -1,5 +1,7 @@ package api.goraebab.domain.blueprint.dto; +import api.goraebab.domain.blueprint.dockerObject.CustomHost; +import api.goraebab.domain.blueprint.dockerObject.ProcessedData; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,7 +23,7 @@ public class BlueprintsResDto { private String name; @Schema(description = "The raw data associated with the blueprint.") - private String data; + private ProcessedData data; @Schema(description = "The flag indicating whether the blueprint is stored remotely.", example = "true") private Boolean isRemote; diff --git a/src/main/java/api/goraebab/domain/blueprint/dto/SyncResultDto.java b/src/main/java/api/goraebab/domain/blueprint/dto/SyncResultDto.java new file mode 100644 index 0000000..4849c44 --- /dev/null +++ b/src/main/java/api/goraebab/domain/blueprint/dto/SyncResultDto.java @@ -0,0 +1,24 @@ +package api.goraebab.domain.blueprint.dto; + +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class SyncResultDto { + + private List> failedContainers; + + private List> succeededContainers; + + + @Builder + public SyncResultDto(List> failedContainers, + List> succeededContainers) { + this.failedContainers = failedContainers; + this.succeededContainers = succeededContainers; + } +} diff --git a/src/main/java/api/goraebab/domain/blueprint/mapper/BlueprintMapper.java b/src/main/java/api/goraebab/domain/blueprint/mapper/BlueprintMapper.java index 4c29657..7003c65 100644 --- a/src/main/java/api/goraebab/domain/blueprint/mapper/BlueprintMapper.java +++ b/src/main/java/api/goraebab/domain/blueprint/mapper/BlueprintMapper.java @@ -1,13 +1,19 @@ package api.goraebab.domain.blueprint.mapper; +import api.goraebab.domain.blueprint.dockerObject.ProcessedData; import api.goraebab.domain.blueprint.dto.BlueprintResDto; import api.goraebab.domain.blueprint.dto.BlueprintsResDto; import api.goraebab.domain.blueprint.entity.Blueprint; import api.goraebab.domain.remote.database.mapper.StorageMapper; +import api.goraebab.global.exception.CustomException; +import api.goraebab.global.exception.ErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; import java.util.List; import org.mapstruct.IterableMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.mapstruct.factory.Mappers; @Mapper(uses = {StorageMapper.class}) @@ -17,12 +23,24 @@ public interface BlueprintMapper { @Mapping(source = "id", target = "blueprintId") @Mapping(source = "storage", target = "storageInfo") + @Mapping(source = "data", target = "data", qualifiedByName = "stringToHost") BlueprintResDto toBlueprintResDto(Blueprint blueprint); @Mapping(source = "id", target = "blueprintId") + @Mapping(source = "data", target = "data", qualifiedByName = "stringToHost") BlueprintsResDto toBlueprintsResDto(Blueprint blueprint); @IterableMapping(elementTargetType = BlueprintsResDto.class) List toBlueprintsResDtoList(List blueprints); + @Named("stringToHost") + default ProcessedData stringToHost(String data) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(data, ProcessedData.class); + } catch (IOException e) { + throw new CustomException(ErrorCode.CONVERSION_FAILED); + } + } + } diff --git a/src/main/java/api/goraebab/domain/blueprint/service/BlueprintService.java b/src/main/java/api/goraebab/domain/blueprint/service/BlueprintService.java index c4d4c28..0aa76e5 100644 --- a/src/main/java/api/goraebab/domain/blueprint/service/BlueprintService.java +++ b/src/main/java/api/goraebab/domain/blueprint/service/BlueprintService.java @@ -4,6 +4,7 @@ import api.goraebab.domain.blueprint.dto.BlueprintResDto; import api.goraebab.domain.blueprint.dto.BlueprintsResDto; +import api.goraebab.domain.blueprint.dto.SyncResultDto; import java.util.List; public interface BlueprintService { @@ -12,9 +13,9 @@ public interface BlueprintService { BlueprintResDto getBlueprintById(Long storageId, Long blueprintId); - void saveBlueprint(Long storageId, BlueprintReqDto blueprintReqDto); + SyncResultDto saveBlueprint(Long storageId, BlueprintReqDto blueprintReqDto); - void modifyBlueprint(Long storageId, Long blueprintId, BlueprintReqDto blueprintReqDto); + SyncResultDto modifyBlueprint(Long storageId, Long blueprintId, BlueprintReqDto blueprintReqDto); void deleteBlueprint(Long storageId, Long blueprintId); diff --git a/src/main/java/api/goraebab/domain/blueprint/service/BlueprintServiceImpl.java b/src/main/java/api/goraebab/domain/blueprint/service/BlueprintServiceImpl.java index cc2bb66..f7ccb3c 100644 --- a/src/main/java/api/goraebab/domain/blueprint/service/BlueprintServiceImpl.java +++ b/src/main/java/api/goraebab/domain/blueprint/service/BlueprintServiceImpl.java @@ -4,6 +4,7 @@ import api.goraebab.domain.blueprint.dto.BlueprintReqDto; import api.goraebab.domain.blueprint.dto.BlueprintResDto; import api.goraebab.domain.blueprint.dto.BlueprintsResDto; +import api.goraebab.domain.blueprint.dto.SyncResultDto; import api.goraebab.domain.blueprint.entity.Blueprint; import api.goraebab.domain.blueprint.mapper.BlueprintMapper; import api.goraebab.domain.blueprint.repository.BlueprintRepository; @@ -11,7 +12,10 @@ import api.goraebab.domain.remote.database.repository.StorageRepository; import api.goraebab.global.exception.CustomException; import api.goraebab.global.exception.ErrorCode; + import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; @@ -26,6 +30,12 @@ public class BlueprintServiceImpl implements BlueprintService { private final StorageRepository storageRepository; private final DockerSyncServiceImpl dockerSyncService; private final ObjectMapper objectMapper; + private static final String CONTAINER_SUCCESS = "success"; + private static final String CONTAINER_FAILED = "failed"; + private static final String FAIL_TO_CREATE_MESSAGE = "Failed to create some containers"; + private static final String FAIL_TO_UPDATE_MESSAGE = "Failed to update some containers"; + private static final String STATUS_KEY = "status"; + @Override @Transactional(readOnly = true) @@ -64,53 +74,86 @@ public BlueprintResDto getBlueprintById(Long storageId, Long blueprintId) { @Override @Transactional - public void saveBlueprint(Long storageId, BlueprintReqDto blueprintReqDto) { - try { - Storage storage = null; + public SyncResultDto saveBlueprint(Long storageId, BlueprintReqDto blueprintReqDto) { + Storage storage = null; - if (storageId != null) { - storage = storageRepository.findById(storageId) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_VALUE)); - } + if (storageId != null) { + storage = storageRepository.findById(storageId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_VALUE)); + } - String processedData = convertProcessedDataToJson(blueprintReqDto.getProcessedData()); + String processedData = convertProcessedDataToJson(blueprintReqDto.getProcessedData()); + + Blueprint blueprint = Blueprint.builder() + .name(blueprintReqDto.getBlueprintName()) + .data(processedData) + .isRemote(false) + .storage(storage) + .build(); + + blueprintRepository.save(blueprint); + + List> syncResults = dockerSyncService.syncDockerWithBlueprintData(blueprintReqDto.getProcessedData()); + + List> failedContainers = syncResults.stream() + .filter(result -> CONTAINER_FAILED.equals(result.get(STATUS_KEY))) + .collect(Collectors.toList()); + + List> succeededContainers = syncResults.stream() + .filter(result -> CONTAINER_SUCCESS.equals(result.get(STATUS_KEY))) + .collect(Collectors.toList()); + + if (!failedContainers.isEmpty()) { + throw new CustomException(ErrorCode.SAVE_FAILED, FAIL_TO_CREATE_MESSAGE, + failedContainers, succeededContainers); + } else { + return SyncResultDto.builder() + .failedContainers(failedContainers) + .succeededContainers(succeededContainers) + .build(); + } - Blueprint blueprint = Blueprint.builder() - .name(blueprintReqDto.getBlueprintName()) - .data(processedData) - .isRemote(false) - .storage(storage) - .build(); - blueprintRepository.save(blueprint); - dockerSyncService.syncDockerWithBlueprintData(blueprintReqDto.getProcessedData()); - } catch (Exception e) { - throw new CustomException(ErrorCode.SAVE_FAILED); - } } @Override @Transactional - public void modifyBlueprint(Long storageId, Long blueprintId, BlueprintReqDto blueprintReqDto) { - try { - Blueprint blueprint; + public SyncResultDto modifyBlueprint(Long storageId, Long blueprintId, BlueprintReqDto blueprintReqDto) { + Blueprint blueprint; + + if (storageId == null) { + blueprint = blueprintRepository.findById(blueprintId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_VALUE)); + } else { + blueprint = findBlueprintByStorageAndId(storageId, blueprintId); + } + + String processedData = convertProcessedDataToJson(blueprintReqDto.getProcessedData()); + blueprint.modify(blueprintReqDto.getBlueprintName(), processedData); + + List> syncResults = dockerSyncService.syncDockerWithBlueprintData(blueprintReqDto.getProcessedData()); + + List> failedContainers = syncResults.stream() + .filter(result -> CONTAINER_FAILED.equals(result.get(STATUS_KEY))) + .collect(Collectors.toList()); + + List> succeededContainers = syncResults.stream() + .filter(result -> CONTAINER_SUCCESS.equals(result.get(STATUS_KEY))) + .collect(Collectors.toList()); + + if (!failedContainers.isEmpty()) { + throw new CustomException(ErrorCode.MODIFY_FAILED, + FAIL_TO_UPDATE_MESSAGE, failedContainers, succeededContainers); + } else { + return SyncResultDto.builder() + .failedContainers(failedContainers) + .succeededContainers(succeededContainers) + .build(); + } - if (storageId == null) { - blueprint = blueprintRepository.findById(blueprintId) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_VALUE)); - } else { - blueprint = findBlueprintByStorageAndId(storageId, blueprintId); - } - - String processedData = convertProcessedDataToJson(blueprintReqDto.getProcessedData()); - blueprint.modify(blueprintReqDto.getBlueprintName(), processedData); - - dockerSyncService.syncDockerWithBlueprintData(blueprintReqDto.getProcessedData()); - } catch (Exception e) { - throw new CustomException(ErrorCode.MODIFY_FAILED); - } } + @Override @Transactional public void deleteBlueprint(Long storageId, Long blueprintId) { diff --git a/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncService.java b/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncService.java index 3f2ce54..bbf1974 100644 --- a/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncService.java +++ b/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncService.java @@ -2,8 +2,11 @@ import api.goraebab.domain.blueprint.dockerObject.ProcessedData; +import java.util.List; +import java.util.Map; + public interface DockerSyncService { - void syncDockerWithBlueprintData(ProcessedData processedData); + List> syncDockerWithBlueprintData(ProcessedData processedData); } \ No newline at end of file diff --git a/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncServiceImpl.java b/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncServiceImpl.java index 34313b8..17b76b8 100644 --- a/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncServiceImpl.java +++ b/src/main/java/api/goraebab/domain/blueprint/service/DockerSyncServiceImpl.java @@ -33,21 +33,32 @@ public class DockerSyncServiceImpl implements DockerSyncService { private final DockerClientUtil dockerClientFactory; + private static final String CONTAINER_NAME_KEY = "containerName"; + private static final String READ_ONLY_MODE = "ro"; + private static final String CONTAINER_RESULT_STATUS_KEY = "status"; + private static final String CONTAINER_RESULT_MESSAGE_KEY = "message"; + private final static String CONTAINER_STATUS_SUCCESS = "success"; + private static final String CONTAINER_STATUS_FAILED = "failed"; + private static final String CONTAINER_START_SUCCESS_MESSAGE = "Container started successfully."; private static final String LOCAL_HOST_IP = "host.docker.internal"; private static final int DOCKER_DAEMON_PORT = 2375; public static final Set EXCLUDED_CONTAINER_NAME = new HashSet<>( - Arrays.asList("/goraebab_spring", "/goraebab_mysql", "/goraebab_mariadb", - "/goraebab_postgresql", "/goraebab_oracle")); + Arrays.asList("/goraebab_spring", "/goraebab_mysql", "/goraebab_mariadb", + "/goraebab_postgresql", "/goraebab_oracle")); private static final Set EXCLUDED_NETWORK_SET = new HashSet<>( - Arrays.asList("bridge", "host", "none", "goraebab_network")); + Arrays.asList("bridge", "host", "none", "goraebab_network")); private static final String MOUNT_BIND_TYPE = "bind"; private static final String MOUNT_VOLUME_TYPE = "volume"; private static final String CONTAINER_RUNNING_STATE = "running"; + private static final String DEFAULT_BRIDGE_NETWORK_SUBNET = "172.17.0.0/16"; + private static final String DEFAULT_BRIDGE_NETWORK_NAME = "bridge"; @Override - public void syncDockerWithBlueprintData(ProcessedData processedData) { + public List> syncDockerWithBlueprintData(ProcessedData processedData) { + + List> containerResults = new ArrayList<>(); try { // 1. host list 추출 @@ -56,7 +67,7 @@ public void syncDockerWithBlueprintData(ProcessedData processedData) { for (CustomHost customHost : customHosts) { // 2. local, remote 연결 시도(`/_ping`), 확인 - if (customHost.getIsLocal()) { + if (!customHost.getIsRemote()) { testDockerPing(LOCAL_HOST_IP, DOCKER_DAEMON_PORT); dockerClient = dockerClientFactory.createLocalDockerClient(); } else { @@ -82,25 +93,40 @@ public void syncDockerWithBlueprintData(ProcessedData processedData) { // 6. container list 추출 // 필요한 image를 확인하고 만약 image를 가지고 있지 않다면 image pull // 타겟 네트워크에 container 실행 - syncContainers(dockerClient, customHost.getCustomNetwork()); + List> syncResult = syncContainers(dockerClient, customHost.getCustomNetwork()); + containerResults.addAll(syncResult); log.debug(dockerClient.toString()); } } catch (DockerException e) { throw new CustomException(ErrorCode.DOCKER_SYNC_FAILED, e); } catch (InterruptedException e) { - throw new CustomException(ErrorCode.CONTAINER_SYNC_FAILED, e); + throw new CustomException(ErrorCode.CONTAINER_SYNC_FAILED, e); + } + + return containerResults; + } + + private void customNetworkValidationCheck(List customNetworkList) { + for (CustomNetwork customNetwork : customNetworkList) { + for (CustomConfig config : customNetwork.getCustomIpam().getCustomConfig()) { + if (customNetwork.getName().equals(DEFAULT_BRIDGE_NETWORK_NAME) + && !config.getSubnet().equals(DEFAULT_BRIDGE_NETWORK_SUBNET)) { + throw new CustomException(ErrorCode.NETWORK_CREATION_FAILED); + } + } } } private void syncNetworks(DockerClient dockerClient, List customNetworkList) throws DockerException { + customNetworkValidationCheck(customNetworkList); List existingNetworks = dockerClient.listNetworksCmd().exec(); for (CustomNetwork customNetwork : customNetworkList) { String customNetworkName = customNetwork.getName(); boolean networkExists = existingNetworks.stream() - .anyMatch(existingNetwork -> existingNetwork.getName().equals(customNetworkName)); + .anyMatch(existingNetwork -> existingNetwork.getName().equals(customNetworkName)); // default network 가 아닐 시 생성 if (!networkExists) { @@ -113,10 +139,10 @@ private void syncNetworks(DockerClient dockerClient, List customN } dockerClient.createNetworkCmd() - .withName(customNetworkName) - .withDriver(customNetwork.getDriver()) - .withIpam(new Ipam().withConfig(ipamConfigList)) - .exec(); + .withName(customNetworkName) + .withDriver(customNetwork.getDriver()) + .withIpam(new Ipam().withConfig(ipamConfigList)) + .exec(); } } @@ -131,79 +157,107 @@ private void syncVolumes(DockerClient dockerClient, List customVol // 볼륨이 이미 존재하는지 확인 boolean volumeExists = existingVolumes.stream() - .anyMatch(existingVolume -> existingVolume.getName().equals(volumeName)); + .anyMatch(existingVolume -> existingVolume.getName().equals(volumeName)); if (!volumeExists) { dockerClient.createVolumeCmd() - .withName(volumeName) - .withDriver(customVolume.getDriver()) - .exec(); + .withName(volumeName) + .withDriver(customVolume.getDriver()) + .exec(); } } } - private void syncContainers(DockerClient dockerClient, List customNetworkList) - throws DockerException, InterruptedException { + private List> syncContainers(DockerClient dockerClient, List customNetworkList) + throws DockerException, InterruptedException { + + List> containerResults = new ArrayList<>(); + for (CustomNetwork customNetwork : customNetworkList) { for (CustomContainer customContainer : customNetwork.getCustomContainers()) { String containerName = customContainer.getContainerName(); - String imageName = customContainer.getCustomImage().getName(); - String tag = customContainer.getCustomImage().getTag(); + Map containerResult = new HashMap<>(); + containerResult.put(CONTAINER_NAME_KEY, containerName); - // 이미지가 존재하는지 확인 try { - dockerClient.inspectImageCmd(imageName).exec(); - } catch (NotFoundException e) { - // 이미지가 없으면 pull - dockerClient.pullImageCmd(imageName).withTag(tag).start().awaitCompletion(); - } - - // 포트 바인딩 설정 - List portBindings = customContainer.getCustomPorts().stream() - .map(customPort -> PortBinding.parse(customPort.getPublicPort() + ":" + customPort.getPrivatePort())) - .collect(Collectors.toList()); - - // 마운트 설정 - List binds = new ArrayList<>(); - List mounts = new ArrayList<>(); - - customContainer.getCustomMounts().forEach(customMount -> { - if (MOUNT_BIND_TYPE.equals(customMount.getType())) { - binds.add(new Bind(customMount.getSource(), - new Volume(customMount.getDestination()))); - } else if (MOUNT_VOLUME_TYPE.equals(customMount.getType())) { - Mount mount = new Mount() - .withType(MountType.VOLUME) - .withSource(customMount.getName()) - .withTarget(customMount.getDestination()) - .withReadOnly("ro".equals(customMount.getMode())); - - mounts.add(mount); + String imageName = customContainer.getCustomImage().getName(); + String tag = customContainer.getCustomImage().getTag(); + + // 이미지가 존재하는지 확인 + try { + dockerClient.inspectImageCmd(imageName).exec(); + } catch (NotFoundException e) { + // 이미지가 없으면 pull + dockerClient.pullImageCmd(imageName).withTag(tag).start().awaitCompletion(); } - }); - - // 포트 바인딩 및 볼륨 바인딩 설정 - HostConfig hostConfig = HostConfig.newHostConfig() - .withPortBindings(portBindings) - .withBinds(binds) - .withMounts(mounts); - - // 컨테이너 생성 - CreateContainerResponse containerResponse = dockerClient.createContainerCmd(imageName) - .withName(containerName) - .withHostConfig(hostConfig) - .withEnv(customContainer.getCustomEnv()) - .withCmd(customContainer.getCustomCmd()) - .withNetworkMode(customNetwork.getName()) + + // 포트 바인딩 설정 + List portBindings = customContainer.getCustomPorts().stream() + .map(customPort -> PortBinding.parse(customPort.getPublicPort() + ":" + customPort.getPrivatePort())) + .collect(Collectors.toList()); + + // 마운트 설정 + List binds = new ArrayList<>(); + List mounts = new ArrayList<>(); + + customContainer.getCustomMounts().forEach(customMount -> { + if (MOUNT_BIND_TYPE.equals(customMount.getType())) { + binds.add(new Bind(customMount.getSource(), + new Volume(customMount.getDestination()))); + } else if (MOUNT_VOLUME_TYPE.equals(customMount.getType())) { + Mount mount = new Mount() + .withType(MountType.VOLUME) + .withSource(customMount.getName()) + .withTarget(customMount.getDestination()) + .withReadOnly(READ_ONLY_MODE.equals(customMount.getMode())); + + mounts.add(mount); + } + }); + + // 포트 바인딩 및 볼륨 바인딩 설정 + HostConfig hostConfig = HostConfig.newHostConfig() + .withPortBindings(portBindings) + .withBinds(binds) + .withMounts(mounts); + + ContainerNetwork containerNetwork = new ContainerNetwork() + .withIpamConfig(new ContainerNetwork.Ipam()) + .withIpv4Address(customContainer.getCustomNetworkSettings().getIpAddress()); // IP 주소 설정 + + // 컨테이너 생성 + CreateContainerResponse containerResponse = dockerClient.createContainerCmd(imageName) + .withName(containerName) + .withHostConfig(hostConfig) + .withEnv(customContainer.getCustomEnv()) + .withCmd(customContainer.getCustomCmd()) + .exec(); + + + dockerClient.connectToNetworkCmd() + .withContainerId(containerResponse.getId()) + .withNetworkId(customNetwork.getName()) + .withContainerNetwork(containerNetwork) .exec(); - dockerClient.startContainerCmd(containerResponse.getId()).exec(); + dockerClient.startContainerCmd(containerResponse.getId()).exec(); + + containerResult.put(CONTAINER_RESULT_STATUS_KEY, CONTAINER_STATUS_SUCCESS); + containerResult.put(CONTAINER_RESULT_MESSAGE_KEY, CONTAINER_START_SUCCESS_MESSAGE); + + } catch (Exception e) { + containerResult.put(CONTAINER_RESULT_STATUS_KEY, CONTAINER_STATUS_FAILED); + containerResult.put(CONTAINER_RESULT_MESSAGE_KEY, e.getMessage()); + } + + containerResults.add(containerResult); } } + return containerResults; } - + private void removeAllContainers(DockerClient dockerClient) throws DockerException{ List containerList = dockerClient.listContainersCmd().withShowAll(true).exec(); @@ -212,10 +266,10 @@ private void removeAllContainers(DockerClient dockerClient) throws DockerExcepti boolean isContain = false; for (String name : containerNames) { - if (EXCLUDED_CONTAINER_NAME.contains(name)) { - isContain = true; - break; - } + if (EXCLUDED_CONTAINER_NAME.contains(name)) { + isContain = true; + break; + } } if (!isContain) { @@ -244,13 +298,13 @@ private void removeAllVolumes(DockerClient dockerClient) throws DockerException{ // 사용중인 볼륨 목록 가져오기 (컨테이너 -> 볼륨) List runningContainers = dockerClient.listContainersCmd().exec(); Set usedVolumes = runningContainers.stream() - .map(Container::getId) - .flatMap(containerId -> { - InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); - return Objects.requireNonNull(containerInfo.getMounts()).stream() - .map(InspectContainerResponse.Mount::getName); - }) - .collect(Collectors.toSet()); + .map(Container::getId) + .flatMap(containerId -> { + InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); + return Objects.requireNonNull(containerInfo.getMounts()).stream() + .map(InspectContainerResponse.Mount::getName); + }) + .collect(Collectors.toSet()); // 사용중이지 않은 볼륨 삭제 for (InspectVolumeResponse volume : volumeList) { diff --git a/src/main/java/api/goraebab/global/exception/CustomException.java b/src/main/java/api/goraebab/global/exception/CustomException.java index 6c1539d..ac63410 100644 --- a/src/main/java/api/goraebab/global/exception/CustomException.java +++ b/src/main/java/api/goraebab/global/exception/CustomException.java @@ -2,10 +2,15 @@ import lombok.Getter; +import java.util.List; +import java.util.Map; + @Getter public class CustomException extends RuntimeException { private final ErrorCode errorCode; + private List> failedContainers; + private List> succeededContainers; public CustomException(String message, ErrorCode errorCode) { super(message); @@ -22,4 +27,11 @@ public CustomException(ErrorCode errorCode, Throwable cause) { this.errorCode = errorCode; } + public CustomException(ErrorCode errorCode, String message, List> failedContainers, List> succeededContainers) { + super(message); + this.errorCode = errorCode; + this.failedContainers = failedContainers; + this.succeededContainers = succeededContainers; + } + } diff --git a/src/main/java/api/goraebab/global/exception/ErrorCode.java b/src/main/java/api/goraebab/global/exception/ErrorCode.java index 80552ac..4acda1d 100644 --- a/src/main/java/api/goraebab/global/exception/ErrorCode.java +++ b/src/main/java/api/goraebab/global/exception/ErrorCode.java @@ -46,7 +46,8 @@ public enum ErrorCode { CONTAINER_SYNC_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 500, "An error occurred during the container synchronization process."), CONTAINER_REMOVAL_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Failed to stop or remove the specified container."), CONTAINER_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Failed to create the specified Docker container."), - CONVERSION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Failed to convert the processed data to JSON format."); + CONVERSION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Failed to convert the processed data to JSON format."), + NETWORK_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Failed to create the specified Docker network."); private final HttpStatus status; diff --git a/src/main/java/api/goraebab/global/exception/ErrorResponse.java b/src/main/java/api/goraebab/global/exception/ErrorResponse.java index cd45eff..c72481a 100644 --- a/src/main/java/api/goraebab/global/exception/ErrorResponse.java +++ b/src/main/java/api/goraebab/global/exception/ErrorResponse.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -16,12 +17,18 @@ public class ErrorResponse { private int code; private String message; private List errors; + private List> failedContainers; + private List> succeededContainers; - private ErrorResponse(final ErrorCode code, final List errors) { + private ErrorResponse(final ErrorCode code, final List errors, + final List> failedContainers, + final List> succeededContainers) { this.status = code.getStatus(); this.code = code.getCode(); this.message = code.getMessage(); this.errors = errors; + this.failedContainers = failedContainers; + this.succeededContainers = succeededContainers; } private ErrorResponse(final ErrorCode code) { @@ -36,11 +43,17 @@ public static ErrorResponse of(final ErrorCode code) { } public static ErrorResponse of(final ErrorCode code, final String error) { - return new ErrorResponse(code, List.of(error)); + return new ErrorResponse(code, List.of(error), null, null); // succeededContainers에 null 전달 } public static ErrorResponse of(final ErrorCode code, final List errors) { - return new ErrorResponse(code, errors); + return new ErrorResponse(code, errors, null, null); } -} \ No newline at end of file + public static ErrorResponse ofFailedContainers(final ErrorCode code, + final List> failedContainers, + final List> succeededContainers) { + return new ErrorResponse(code, new ArrayList<>(), failedContainers, succeededContainers); + } + +} diff --git a/src/main/java/api/goraebab/global/exception/GlobalExceptionHandler.java b/src/main/java/api/goraebab/global/exception/GlobalExceptionHandler.java index 104ff71..c4c171c 100644 --- a/src/main/java/api/goraebab/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/api/goraebab/global/exception/GlobalExceptionHandler.java @@ -17,7 +17,12 @@ public class GlobalExceptionHandler { @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(CustomException e) { - final ErrorResponse response = ErrorResponse.of(e.getErrorCode()); + final ErrorResponse response; + if (e.getFailedContainers() != null && !e.getFailedContainers().isEmpty()) { + response = ErrorResponse.ofFailedContainers(e.getErrorCode(), e.getFailedContainers(), e.getSucceededContainers()); + } else { + response = ErrorResponse.of(e.getErrorCode()); + } return new ResponseEntity<>(response, e.getErrorCode().getStatus()); } @@ -34,11 +39,11 @@ protected ResponseEntity handleHttpRequestMethodNotSupportedExcep return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED); } - @ExceptionHandler(DataIntegrityViolationException.class) + @ExceptionHandler(DataIntegrityViolationException.class) protected ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException e) { List errors = List.of(e.getMessage()); ErrorResponse response = ErrorResponse.of(ErrorCode.DATA_INTEGRITY_VIOLATION, errors); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } -} \ No newline at end of file +}