diff --git a/src/main/kotlin/com/dcd/server/core/common/error/ErrorCode.kt b/src/main/kotlin/com/dcd/server/core/common/error/ErrorCode.kt index 72f80c8b..209077df 100644 --- a/src/main/kotlin/com/dcd/server/core/common/error/ErrorCode.kt +++ b/src/main/kotlin/com/dcd/server/core/common/error/ErrorCode.kt @@ -22,6 +22,8 @@ enum class ErrorCode( ALREADY_EXISTS_DOMAIN("이미 존재하는 도메인", 400), ALREADY_CONNECTED_DOMAIN("이미 연결된 도메인", 400), NOT_CONNECTED_DOMAIN("도메인이 애플리케이션에 연결되어 있지 않음", 400), + ALREADY_EXISTS_VOLUME("이미 존재하는 볼륨", 400), + ALREADY_EXISTS_VOLUME_MOUNT("볼륨 마운트가 존재합니다.", 400), UNAUTHORIZED("권한이 없음", 401), EXPIRED_TOKEN("토큰이 만료됨", 401), @@ -44,6 +46,8 @@ enum class ErrorCode( WORKSPACE_NOT_FOUND("해당 워크스페이스를 찾을 수 없음", 404), AUTH_CODE_NOT_FOUND("인증 코드를 찾을 수 없습니다. 코드를 다시 전송 해주세요.", 404), DOMAIN_NOT_FOUND("해당 도메인을 찾을 수 없음", 404), + VOLUME_NOT_FOUND("해당 볼륨을 찾을 수 없음", 404), + VOLUME_MOUNT_NOT_FOUND("해당 볼륨 마운트를 찾을 수 없음", 404), CONFLICT("해당 요청은 서버의 상태와 충돌됩니다.", 409), CAN_NOT_DEPLOY_APPLICATION("애플리케이션을 배포할 수 없습니다. 애플리케이션을 정지시킨 후 실행해주세요.", 409), @@ -59,4 +63,7 @@ enum class ErrorCode( IMAGE_NOT_BUILT("해당 애플리케이션을 이미지로 빌드할 수 없음", 500), INTERNAL_ERROR("서버 내부 에러", 500), INVALID_PARSING_OBJECT_FIELD("파싱할 필드가 올바르게 설정되어있지 않는 객체임", 500), + FAILURE_VOLUME_CREATION("컨테이너 볼륨 생성에 실패했습니다.", 500), + FAILURE_VOLUME_DELETE("컨테이너 볼륨 삭제에 실패했습니다.", 500), + FAILURE_VOLUME_COPY("컨테이너 볼륨 복제에 실패했습니다.", 500), } \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/common/file/FileContent.kt b/src/main/kotlin/com/dcd/server/core/common/file/FileContent.kt index 209f7df6..d615ddcc 100644 --- a/src/main/kotlin/com/dcd/server/core/common/file/FileContent.kt +++ b/src/main/kotlin/com/dcd/server/core/common/file/FileContent.kt @@ -47,12 +47,21 @@ object FileContent { ${getEnvString(env)} """.trimIndent() - fun getImageVersionShellScriptContent(imageName: String, minVersion: String): String = + fun getH2DBDockerFileContent(version: String, port: Int, env: Map): String = """ + FROM oscarfonts/h2:${version} + EXPOSE $port + ${getEnvString(env)} + """.trimIndent() + + fun getImageVersionShellScriptContent(imageName: String, minVersion: String): String { + val imagePrefix = if (imageName.contains("/")) "" else "library/" + + return """ #!/bin/bash # 이미지, 페이지 사이즈, 최소 버전(threshold) 설정 - IMAGE_NAME="library/$imageName" + IMAGE_NAME="${imagePrefix}$imageName" PAGE_SIZE=100 MIN_VERSION="$minVersion" @@ -100,6 +109,7 @@ object FileContent { echo "${'$'}sorted_numeric_tags" fi """.trimIndent() + } fun getApplicationHttpConfig(application: Application, domain: String): String = """ @@ -118,7 +128,7 @@ object FileContent { proxy_set_header X-Real-IP ${'$'}remote_addr; proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for; - proxy_pass http://host.docker.internal:${application.externalPort}; + proxy_pass http://${application.containerName}:${application.externalPort}; } } """.trimIndent() diff --git a/src/main/kotlin/com/dcd/server/core/domain/application/model/enums/ApplicationType.kt b/src/main/kotlin/com/dcd/server/core/domain/application/model/enums/ApplicationType.kt index 64f30e55..f5ff9d0f 100644 --- a/src/main/kotlin/com/dcd/server/core/domain/application/model/enums/ApplicationType.kt +++ b/src/main/kotlin/com/dcd/server/core/domain/application/model/enums/ApplicationType.kt @@ -5,5 +5,6 @@ enum class ApplicationType { NEST_JS, MYSQL, MARIA_DB, - REDIS + REDIS, + H2_DB } \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateContainerServiceImpl.kt b/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateContainerServiceImpl.kt index 8d2fa70b..17ce6923 100644 --- a/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateContainerServiceImpl.kt +++ b/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateContainerServiceImpl.kt @@ -5,6 +5,7 @@ import com.dcd.server.core.domain.application.model.Application import com.dcd.server.core.domain.application.service.CreateContainerService import com.dcd.server.core.domain.application.spi.CheckExitValuePort import com.dcd.server.core.domain.application.util.FailureCase +import com.dcd.server.core.domain.volume.spi.QueryVolumePort import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.springframework.stereotype.Service @@ -12,14 +13,26 @@ import org.springframework.stereotype.Service @Service class CreateContainerServiceImpl( private val commandPort: CommandPort, + private val queryVolumePort: QueryVolumePort, private val checkExitValuePort: CheckExitValuePort ) : CreateContainerService { override suspend fun createContainer(application: Application, externalPort: Int) { withContext(Dispatchers.IO) { + val volumeMountBuilder = StringBuilder() + queryVolumePort.findAllMountByApplication(application) + .forEach { + val volume = it.volume + volumeMountBuilder.append("-v ${volume.volumeName}:${it.mountPath}") + if (it.readOnly) + volumeMountBuilder.append(":ro") + volumeMountBuilder.append(" ") + } + val volumeMountFlags = volumeMountBuilder.toString() val cmd = "docker create --network ${application.workspace.networkName} " + - "--name ${application.containerName} " + - "-p ${externalPort}:${application.port} ${application.containerName}:latest" + "--name ${application.containerName} " + + volumeMountFlags + + "-p ${externalPort}:${application.port} ${application.containerName}:latest" commandPort.executeShellCommand(cmd) .also {exitValue -> diff --git a/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateDockerFileServiceImpl.kt b/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateDockerFileServiceImpl.kt index c26938e0..bf07eaf5 100644 --- a/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateDockerFileServiceImpl.kt +++ b/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateDockerFileServiceImpl.kt @@ -80,6 +80,9 @@ class CreateDockerFileServiceImpl( ApplicationType.NEST_JS -> FileContent.getNestJsDockerFileContent(version, application.port, applicationEnv) + + ApplicationType.H2_DB -> + FileContent.getH2DBDockerFileContent(version, application.port, applicationEnv) } try { if (!file.exists()) diff --git a/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/GetApplicationVersionServiceImpl.kt b/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/GetApplicationVersionServiceImpl.kt index fd86627c..60159ee4 100644 --- a/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/GetApplicationVersionServiceImpl.kt +++ b/src/main/kotlin/com/dcd/server/core/domain/application/service/impl/GetApplicationVersionServiceImpl.kt @@ -17,6 +17,7 @@ class GetApplicationVersionServiceImpl( ApplicationType.MARIA_DB -> "mariadb" to "10" ApplicationType.MYSQL -> "mysql" to "8" ApplicationType.REDIS -> "redis" to "6" + ApplicationType.H2_DB -> "oscarfonts/h2" to "0" } val getVersionScript = FileContent.getImageVersionShellScriptContent(baseImageName, minVersion) return commandPort.executeShellCommandWithResult(getVersionScript) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/extension/VolumeDtoExtension.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/extension/VolumeDtoExtension.kt new file mode 100644 index 00000000..1c38de91 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/extension/VolumeDtoExtension.kt @@ -0,0 +1,50 @@ +package com.dcd.server.core.domain.volume.dto.extension + +import com.dcd.server.core.domain.application.dto.extenstion.toProfileDto +import com.dcd.server.core.domain.volume.dto.request.CreateVolumeReqDto +import com.dcd.server.core.domain.volume.dto.request.UpdateVolumeReqDto +import com.dcd.server.core.domain.volume.dto.response.VolumeDetailResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeMountResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeSimpleResDto +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.workspace.model.Workspace +import java.util.UUID + +fun CreateVolumeReqDto.toEntity(workspace: Workspace): Volume = + Volume( + id = UUID.randomUUID(), + name = this.name, + description = this.description, + workspace = workspace, + ) + +fun UpdateVolumeReqDto.toEntity(volume: Volume): Volume = + Volume( + id = volume.id, + name = this.name, + description = this.description, + workspace = volume.workspace, + ) + +fun Volume.toResDto(): VolumeSimpleResDto = + VolumeSimpleResDto( + id = this.id, + name = this.name, + description = this.description + ) + +fun Volume.toDetailResDto(volumeMountList: List): VolumeDetailResDto = + VolumeDetailResDto( + id = this.id, + name = this.name, + description = this.description, + mountList = volumeMountList.map { it.toResDto() } + ) + +fun VolumeMount.toResDto(): VolumeMountResDto = + VolumeMountResDto( + mountPath = this.mountPath, + readOnly = this.readOnly, + applicationInfo = this.application.toProfileDto() + ) \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/CreateVolumeReqDto.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/CreateVolumeReqDto.kt new file mode 100644 index 00000000..18814cae --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/CreateVolumeReqDto.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.dto.request + +data class CreateVolumeReqDto( + val name: String, + val description: String +) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/MountVolumeReqDto.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/MountVolumeReqDto.kt new file mode 100644 index 00000000..2bb4f1a0 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/MountVolumeReqDto.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.dto.request + +data class MountVolumeReqDto( + val mountPath: String, + val readOnly: Boolean, +) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/UpdateVolumeReqDto.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/UpdateVolumeReqDto.kt new file mode 100644 index 00000000..058c06e1 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/UpdateVolumeReqDto.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.dto.request + +data class UpdateVolumeReqDto( + val name: String, + val description: String +) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeDetailResDto.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeDetailResDto.kt new file mode 100644 index 00000000..b3606c99 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeDetailResDto.kt @@ -0,0 +1,10 @@ +package com.dcd.server.core.domain.volume.dto.response + +import java.util.UUID + +data class VolumeDetailResDto( + val id: UUID, + val name: String, + val description: String, + val mountList: List +) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeListResDto.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeListResDto.kt new file mode 100644 index 00000000..981c2c9b --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeListResDto.kt @@ -0,0 +1,5 @@ +package com.dcd.server.core.domain.volume.dto.response + +data class VolumeListResDto( + val list: List +) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeMountResDto.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeMountResDto.kt new file mode 100644 index 00000000..5c3f4df6 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeMountResDto.kt @@ -0,0 +1,9 @@ +package com.dcd.server.core.domain.volume.dto.response + +import com.dcd.server.core.domain.application.dto.response.ApplicationProfileResDto + +data class VolumeMountResDto( + val mountPath: String, + val readOnly: Boolean, + val applicationInfo: ApplicationProfileResDto +) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeSimpleResDto.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeSimpleResDto.kt new file mode 100644 index 00000000..01013ca4 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/dto/response/VolumeSimpleResDto.kt @@ -0,0 +1,9 @@ +package com.dcd.server.core.domain.volume.dto.response + +import java.util.UUID + +data class VolumeSimpleResDto( + val id: UUID, + val name: String, + val description: String +) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/exception/AlreadyExistsVolumeException.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/AlreadyExistsVolumeException.kt new file mode 100644 index 00000000..45d2f29a --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/AlreadyExistsVolumeException.kt @@ -0,0 +1,8 @@ +package com.dcd.server.core.domain.volume.exception + +import com.dcd.server.core.common.error.BasicException +import com.dcd.server.core.common.error.ErrorCode + +class AlreadyExistsVolumeException : BasicException(ErrorCode.ALREADY_EXISTS_VOLUME){ + +} diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/exception/AlreadyExistsVolumeMountException.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/AlreadyExistsVolumeMountException.kt new file mode 100644 index 00000000..02ddcf0f --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/AlreadyExistsVolumeMountException.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.exception + +import com.dcd.server.core.common.error.BasicException +import com.dcd.server.core.common.error.ErrorCode + +class AlreadyExistsVolumeMountException : BasicException(ErrorCode.ALREADY_EXISTS_VOLUME_MOUNT) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeCopyFailureException.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeCopyFailureException.kt new file mode 100644 index 00000000..05f81d50 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeCopyFailureException.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.exception + +import com.dcd.server.core.common.error.BasicException +import com.dcd.server.core.common.error.ErrorCode + +class VolumeCopyFailureException : BasicException(ErrorCode.FAILURE_VOLUME_COPY) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeCreationFailureException.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeCreationFailureException.kt new file mode 100644 index 00000000..3401a0a8 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeCreationFailureException.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.exception + +import com.dcd.server.core.common.error.BasicException +import com.dcd.server.core.common.error.ErrorCode + +class VolumeCreationFailureException : BasicException(ErrorCode.FAILURE_VOLUME_CREATION) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeDeleteFailureException.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeDeleteFailureException.kt new file mode 100644 index 00000000..46bcd7aa --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeDeleteFailureException.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.exception + +import com.dcd.server.core.common.error.BasicException +import com.dcd.server.core.common.error.ErrorCode + +class VolumeDeleteFailureException : BasicException(ErrorCode.FAILURE_VOLUME_DELETE) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeMountNotFoundException.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeMountNotFoundException.kt new file mode 100644 index 00000000..750e00aa --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeMountNotFoundException.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.exception + +import com.dcd.server.core.common.error.BasicException +import com.dcd.server.core.common.error.ErrorCode + +class VolumeMountNotFoundException : BasicException(ErrorCode.VOLUME_MOUNT_NOT_FOUND) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeNotFoundException.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeNotFoundException.kt new file mode 100644 index 00000000..7a02dff4 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/exception/VolumeNotFoundException.kt @@ -0,0 +1,6 @@ +package com.dcd.server.core.domain.volume.exception + +import com.dcd.server.core.common.error.BasicException +import com.dcd.server.core.common.error.ErrorCode + +class VolumeNotFoundException : BasicException(ErrorCode.VOLUME_NOT_FOUND) diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/model/Volume.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/model/Volume.kt new file mode 100644 index 00000000..aee88790 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/model/Volume.kt @@ -0,0 +1,13 @@ +package com.dcd.server.core.domain.volume.model + +import com.dcd.server.core.domain.workspace.model.Workspace +import java.util.UUID + +class Volume( + val id: UUID, + val name: String, + val description: String, + val workspace: Workspace +) { + val volumeName: String = "${name.replace(" ", "_")}-$id" +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/model/VolumeMount.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/model/VolumeMount.kt new file mode 100644 index 00000000..da1dd9b6 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/model/VolumeMount.kt @@ -0,0 +1,11 @@ +package com.dcd.server.core.domain.volume.model + +import com.dcd.server.core.domain.application.model.Application +import java.util.UUID + +class VolumeMount( + val application: Application, + val volume: Volume, + val mountPath: String, + val readOnly: Boolean +) \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/service/CopyVolumeService.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/service/CopyVolumeService.kt new file mode 100644 index 00000000..714e86e3 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/service/CopyVolumeService.kt @@ -0,0 +1,7 @@ +package com.dcd.server.core.domain.volume.service + +import com.dcd.server.core.domain.volume.model.Volume + +interface CopyVolumeService { + fun copyVolumeContent(existingVolume: Volume, newVolume: Volume) +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/service/CreateVolumeService.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/service/CreateVolumeService.kt new file mode 100644 index 00000000..aaecb737 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/service/CreateVolumeService.kt @@ -0,0 +1,7 @@ +package com.dcd.server.core.domain.volume.service + +import com.dcd.server.core.domain.volume.model.Volume + +interface CreateVolumeService { + fun create(volume: Volume) +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/service/DeleteVolumeService.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/service/DeleteVolumeService.kt new file mode 100644 index 00000000..ea905a3e --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/service/DeleteVolumeService.kt @@ -0,0 +1,7 @@ +package com.dcd.server.core.domain.volume.service + +import com.dcd.server.core.domain.volume.model.Volume + +interface DeleteVolumeService { + fun deleteVolume(volume: Volume) +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/CopyDockerVolumeServiceImpl.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/CopyDockerVolumeServiceImpl.kt new file mode 100644 index 00000000..26822c78 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/CopyDockerVolumeServiceImpl.kt @@ -0,0 +1,30 @@ +package com.dcd.server.core.domain.volume.service.impl + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.domain.volume.exception.VolumeCopyFailureException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.service.CopyVolumeService +import com.dcd.server.core.domain.volume.service.DeleteVolumeService +import org.springframework.stereotype.Service + +@Service +class CopyDockerVolumeServiceImpl( + private val commandPort: CommandPort, + private val deleteVolumeService: DeleteVolumeService +) : CopyVolumeService { + override fun copyVolumeContent(existingVolume: Volume, newVolume: Volume) { + val volumeCopyCmd = + """ + docker run --rm -it \ + -v ${existingVolume.volumeName}:/from \ + -v ${newVolume.volumeName}:/to \ + alpine ash -c \"cp -a /from/. /to/\" + """.trimIndent() + + val exitValue = commandPort.executeShellCommand(volumeCopyCmd) + if (exitValue != 0) { + deleteVolumeService.deleteVolume(newVolume) + throw VolumeCopyFailureException() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/CreateDockerVolumeServiceImpl.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/CreateDockerVolumeServiceImpl.kt new file mode 100644 index 00000000..43a6407a --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/CreateDockerVolumeServiceImpl.kt @@ -0,0 +1,19 @@ +package com.dcd.server.core.domain.volume.service.impl + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.domain.volume.exception.VolumeCreationFailureException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.service.CreateVolumeService +import org.springframework.stereotype.Service + +@Service +class CreateDockerVolumeServiceImpl( + private val commandPort: CommandPort +) : CreateVolumeService { + override fun create(volume: Volume) { + val exitValue = commandPort.executeShellCommand("docker volume create ${volume.volumeName}") + + if (exitValue != 0) + throw VolumeCreationFailureException() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/DeleteDockerVolumeServiceImpl.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/DeleteDockerVolumeServiceImpl.kt new file mode 100644 index 00000000..02c884e0 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/DeleteDockerVolumeServiceImpl.kt @@ -0,0 +1,19 @@ +package com.dcd.server.core.domain.volume.service.impl + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.domain.volume.exception.VolumeDeleteFailureException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.service.DeleteVolumeService +import org.springframework.stereotype.Service + +@Service +class DeleteDockerVolumeServiceImpl( + private val commandPort: CommandPort +) : DeleteVolumeService { + override fun deleteVolume(volume: Volume) { + val exitValue = commandPort.executeShellCommand("docker volume rm ${volume.volumeName}") + + if (exitValue != 0) + throw VolumeDeleteFailureException() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/spi/CommandVolumePort.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/spi/CommandVolumePort.kt new file mode 100644 index 00000000..4074f84c --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/spi/CommandVolumePort.kt @@ -0,0 +1,14 @@ +package com.dcd.server.core.domain.volume.spi + +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount + +interface CommandVolumePort { + fun save(volume: Volume) + + fun delete(volume: Volume) + + fun saveMount(volumeMount: VolumeMount) + + fun deleteMount(volumeMount: VolumeMount) +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/spi/QueryVolumePort.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/spi/QueryVolumePort.kt new file mode 100644 index 00000000..20ef16cc --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/spi/QueryVolumePort.kt @@ -0,0 +1,21 @@ +package com.dcd.server.core.domain.volume.spi + +import com.dcd.server.core.domain.application.model.Application +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.workspace.model.Workspace +import java.util.UUID + +interface QueryVolumePort { + fun findById(id: UUID): Volume? + + fun findAllVolumeByWorkspace(workspace: Workspace): List + + fun existsVolumeByNameAndWorkspace(name: String, workspace: Workspace): Boolean + + fun findAllMountByApplication(application: Application): List + + fun findAllMountByVolume(volume: Volume): List + + fun findMountByApplicationAndVolume(application: Application, volume: Volume): VolumeMount? +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/spi/VolumePort.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/spi/VolumePort.kt new file mode 100644 index 00000000..bf4519e0 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/spi/VolumePort.kt @@ -0,0 +1,4 @@ +package com.dcd.server.core.domain.volume.spi + +interface VolumePort : CommandVolumePort, QueryVolumePort{ +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/CreateVolumeUseCase.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/CreateVolumeUseCase.kt new file mode 100644 index 00000000..896ab082 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/CreateVolumeUseCase.kt @@ -0,0 +1,33 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.annotation.UseCase +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.dto.extension.toEntity +import com.dcd.server.core.domain.volume.dto.request.CreateVolumeReqDto +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeException +import com.dcd.server.core.domain.volume.service.CreateVolumeService +import com.dcd.server.core.domain.volume.spi.CommandVolumePort +import com.dcd.server.core.domain.volume.spi.QueryVolumePort +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException + +@UseCase +class CreateVolumeUseCase( + private val workspaceInfo: WorkspaceInfo, + private val queryVolumePort: QueryVolumePort, + private val commandVolumePort: CommandVolumePort, + private val createVolumeService: CreateVolumeService +) { + fun execute(createVolumeReqDto: CreateVolumeReqDto) { + val workspace = (workspaceInfo.workspace + ?: throw WorkspaceNotFoundException()) + + val existsVolume = queryVolumePort.existsVolumeByNameAndWorkspace(createVolumeReqDto.name, workspace) + if (existsVolume) + throw AlreadyExistsVolumeException() + + val volume = createVolumeReqDto.toEntity(workspace) + commandVolumePort.save(volume) + + createVolumeService.create(volume) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/DeleteVolumeUseCase.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/DeleteVolumeUseCase.kt new file mode 100644 index 00000000..caefd76a --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/DeleteVolumeUseCase.kt @@ -0,0 +1,38 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.annotation.UseCase +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeMountException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.service.DeleteVolumeService +import com.dcd.server.core.domain.volume.spi.CommandVolumePort +import com.dcd.server.core.domain.volume.spi.QueryVolumePort +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import java.util.UUID + +@UseCase +class DeleteVolumeUseCase( + private val queryVolumePort: QueryVolumePort, + private val commandVolumePort: CommandVolumePort, + private val deleteVolumeService: DeleteVolumeService, + private val workspaceInfo: WorkspaceInfo +) { + fun execute(volumeId: UUID) { + val workspace = (workspaceInfo.workspace + ?: throw WorkspaceNotFoundException()) + + val volume = (queryVolumePort.findById(volumeId) + ?: throw VolumeNotFoundException()) + + if (workspace != volume.workspace) + throw VolumeNotFoundException() + + val volumeMountList = queryVolumePort.findAllMountByVolume(volume) + if (volumeMountList.isNotEmpty()) + throw AlreadyExistsVolumeMountException() + + deleteVolumeService.deleteVolume(volume) + + commandVolumePort.delete(volume) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/GetAllVolumeUseCase.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/GetAllVolumeUseCase.kt new file mode 100644 index 00000000..adb0b3cf --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/GetAllVolumeUseCase.kt @@ -0,0 +1,24 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.annotation.UseCase +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.dto.extension.toResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeListResDto +import com.dcd.server.core.domain.volume.spi.QueryVolumePort +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException + +@UseCase(readOnly = true) +class GetAllVolumeUseCase( + private val queryVolumePort: QueryVolumePort, + private val workspaceInfo: WorkspaceInfo +) { + fun execute(): VolumeListResDto { + val workspace = (workspaceInfo.workspace + ?: throw WorkspaceNotFoundException()) + + val volumeList = + queryVolumePort.findAllVolumeByWorkspace(workspace) + .map { it.toResDto() } + return VolumeListResDto(volumeList) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/GetOneVolumeUseCase.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/GetOneVolumeUseCase.kt new file mode 100644 index 00000000..d1602a38 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/GetOneVolumeUseCase.kt @@ -0,0 +1,30 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.annotation.UseCase +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.dto.extension.toDetailResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeDetailResDto +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.spi.QueryVolumePort +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import java.util.UUID + +@UseCase(readOnly = true) +class GetOneVolumeUseCase( + private val queryVolumePort: QueryVolumePort, + private val workspaceInfo: WorkspaceInfo +) { + fun execute(volumeId: UUID): VolumeDetailResDto { + val volume = (queryVolumePort.findById(volumeId) + ?: throw VolumeNotFoundException()) + + val workspace = (workspaceInfo.workspace + ?: throw WorkspaceNotFoundException()) + if (volume.workspace != workspace) + throw VolumeNotFoundException() + + val volumeMountList = queryVolumePort.findAllMountByVolume(volume) + + return volume.toDetailResDto(volumeMountList) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCase.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCase.kt new file mode 100644 index 00000000..b3213c04 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCase.kt @@ -0,0 +1,55 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.annotation.UseCase +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.application.event.DeployApplicationEvent +import com.dcd.server.core.domain.application.exception.ApplicationNotFoundException +import com.dcd.server.core.domain.application.spi.QueryApplicationPort +import com.dcd.server.core.domain.volume.dto.request.MountVolumeReqDto +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeMountException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.volume.spi.CommandVolumePort +import com.dcd.server.core.domain.volume.spi.QueryVolumePort +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import org.springframework.context.ApplicationEventPublisher +import java.util.UUID + +@UseCase +class MountVolumeUseCase( + private val queryVolumePort: QueryVolumePort, + private val queryApplicationPort: QueryApplicationPort, + private val commandVolumePort: CommandVolumePort, + private val workspaceInfo: WorkspaceInfo, + private val eventPublisher: ApplicationEventPublisher, +) { + fun execute(volumeId: UUID, applicationId: String, mountVolumeReqDto: MountVolumeReqDto) { + val workspace = (workspaceInfo.workspace + ?: throw WorkspaceNotFoundException()) + + val volume = (queryVolumePort.findById(volumeId) + ?: throw VolumeNotFoundException()) + + if (volume.workspace != workspace) + throw VolumeNotFoundException() + + val application = (queryApplicationPort.findById(applicationId) + ?: throw ApplicationNotFoundException()) + if (application.workspace != workspace) + throw ApplicationNotFoundException() + + if (queryVolumePort.findMountByApplicationAndVolume(application, volume) != null) + throw AlreadyExistsVolumeMountException() + + val volumeMount = VolumeMount( + application = application, + volume = volume, + mountPath = mountVolumeReqDto.mountPath, + readOnly = mountVolumeReqDto.readOnly + ) + commandVolumePort.saveMount(volumeMount) + + //마운트 생성후 대상 애플리케이션 재배포 + eventPublisher.publishEvent(DeployApplicationEvent(listOf(application.id))) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/UnMountVolumeUseCase.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/UnMountVolumeUseCase.kt new file mode 100644 index 00000000..458c7ce3 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/UnMountVolumeUseCase.kt @@ -0,0 +1,44 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.annotation.UseCase +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.application.event.DeployApplicationEvent +import com.dcd.server.core.domain.application.exception.ApplicationNotFoundException +import com.dcd.server.core.domain.application.spi.QueryApplicationPort +import com.dcd.server.core.domain.volume.exception.VolumeMountNotFoundException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.spi.CommandVolumePort +import com.dcd.server.core.domain.volume.spi.QueryVolumePort +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import org.springframework.context.ApplicationEventPublisher +import java.util.UUID + +@UseCase +class UnMountVolumeUseCase( + private val queryVolumePort: QueryVolumePort, + private val queryApplicationPort: QueryApplicationPort, + private val commandVolumePort: CommandVolumePort, + private val workspaceInfo: WorkspaceInfo, + private val eventPublisher: ApplicationEventPublisher +) { + fun execute(volumeId: UUID, applicationId: String) { + val workspace = (workspaceInfo.workspace + ?: throw WorkspaceNotFoundException()) + + val volume = (queryVolumePort.findById(volumeId) + ?: throw VolumeNotFoundException()) + if (volume.workspace != workspace) + throw VolumeNotFoundException() + + val application = (queryApplicationPort.findById(applicationId) + ?: throw ApplicationNotFoundException()) + if (application.workspace != workspace) + throw ApplicationNotFoundException() + + val volumeMount = queryVolumePort.findMountByApplicationAndVolume(application, volume) + ?: throw VolumeMountNotFoundException() + commandVolumePort.deleteMount(volumeMount) + + eventPublisher.publishEvent(DeployApplicationEvent(listOf(application.id))) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/UpdateVolumeUseCase.kt b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/UpdateVolumeUseCase.kt new file mode 100644 index 00000000..a2cfefeb --- /dev/null +++ b/src/main/kotlin/com/dcd/server/core/domain/volume/usecase/UpdateVolumeUseCase.kt @@ -0,0 +1,48 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.annotation.UseCase +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.dto.extension.toEntity +import com.dcd.server.core.domain.volume.dto.request.UpdateVolumeReqDto +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeMountException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.service.CopyVolumeService +import com.dcd.server.core.domain.volume.service.CreateVolumeService +import com.dcd.server.core.domain.volume.service.DeleteVolumeService +import com.dcd.server.core.domain.volume.spi.CommandVolumePort +import com.dcd.server.core.domain.volume.spi.QueryVolumePort +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import java.util.UUID + +@UseCase +class UpdateVolumeUseCase( + private val queryVolumePort: QueryVolumePort, + private val commandVolumePort: CommandVolumePort, + private val createVolumeService: CreateVolumeService, + private val copyVolumeService: CopyVolumeService, + private val deleteVolumeService: DeleteVolumeService, + private val workspaceInfo: WorkspaceInfo, +) { + fun execute(volumeId: UUID, request: UpdateVolumeReqDto) { + val volume = (queryVolumePort.findById(volumeId) + ?: throw VolumeNotFoundException()) + + val workspace = (workspaceInfo.workspace + ?: throw WorkspaceNotFoundException()) + + if (workspace != volume.workspace) + throw VolumeNotFoundException() + + val volumeMountList = queryVolumePort.findAllMountByVolume(volume) + if (volumeMountList.isNotEmpty()) + throw AlreadyExistsVolumeMountException() + + val newVolume = request.toEntity(volume) + commandVolumePort.save(newVolume) + + // 수정된 볼륨을 생성후 내용을 복사하고, 기존 볼륨 삭제 + createVolumeService.create(newVolume) + copyVolumeService.copyVolumeContent(volume, newVolume) + deleteVolumeService.deleteVolume(volume) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/infrastructure/global/config/SecurityConfig.kt b/src/main/kotlin/com/dcd/server/infrastructure/global/config/SecurityConfig.kt index 2bb66b26..63536d35 100644 --- a/src/main/kotlin/com/dcd/server/infrastructure/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/dcd/server/infrastructure/global/config/SecurityConfig.kt @@ -98,6 +98,15 @@ class SecurityConfig( it.requestMatchers(HttpMethod.GET, "/{workspaceId}/env").authenticated() it.requestMatchers(HttpMethod.GET, "/{workspaceId}/env/{envId}").authenticated() + //volume + it.requestMatchers(HttpMethod.POST, "/{workspaceId}/volume").authenticated() + it.requestMatchers(HttpMethod.DELETE, "/{workspaceId}/volume/{volumeId}").authenticated() + it.requestMatchers(HttpMethod.PUT, "/{workspaceId}/volume/{volumeId}").authenticated() + it.requestMatchers(HttpMethod.GET, "/{workspaceId}/volume").authenticated() + it.requestMatchers(HttpMethod.GET, "/{workspaceId}/volume/{volumeId}").authenticated() + it.requestMatchers(HttpMethod.POST, "/{workspaceId}/volume/{volumeId}/mount").authenticated() + it.requestMatchers(HttpMethod.DELETE, "/{workspaceId}/volume/{volumeId}/mount").authenticated() + //when url not set it.anyRequest().denyAll() } diff --git a/src/main/kotlin/com/dcd/server/persistence/volume/VolumePersistenceAdapter.kt b/src/main/kotlin/com/dcd/server/persistence/volume/VolumePersistenceAdapter.kt new file mode 100644 index 00000000..c8e46186 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/persistence/volume/VolumePersistenceAdapter.kt @@ -0,0 +1,64 @@ +package com.dcd.server.persistence.volume + +import com.dcd.server.core.domain.application.model.Application +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.volume.spi.VolumePort +import com.dcd.server.core.domain.workspace.model.Workspace +import com.dcd.server.persistence.application.adapter.toEntity +import com.dcd.server.persistence.volume.adapter.toDomain +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeMountRepository +import com.dcd.server.persistence.volume.repository.VolumeRepository +import com.dcd.server.persistence.workspace.adapter.toEntity +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Component +import java.util.UUID + +@Component +class VolumePersistenceAdapter( + private val volumeRepository: VolumeRepository, + private val volumeMountRepository: VolumeMountRepository +) : VolumePort{ + override fun save(volume: Volume) { + volumeRepository.save(volume.toEntity()) + } + + override fun delete(volume: Volume) { + volumeRepository.deleteById(volume.id) + } + + override fun saveMount(volumeMount: VolumeMount) { + volumeMountRepository.save(volumeMount.toEntity()) + } + + override fun deleteMount(volumeMount: VolumeMount) { + volumeMountRepository.delete(volumeMount.toEntity()) + } + + override fun findById(id: UUID): Volume? = + volumeRepository.findByIdOrNull(id) + ?.toDomain() + + override fun findAllVolumeByWorkspace(workspace: Workspace): List = + volumeRepository.findAllByWorkspace(workspace.toEntity()) + .map { it.toDomain() } + + override fun existsVolumeByNameAndWorkspace( + name: String, + workspace: Workspace, + ): Boolean = + volumeRepository.existsByNameAndWorkspace(name, workspace.toEntity()) + + override fun findAllMountByApplication(application: Application): List = + volumeMountRepository.findAllByApplication(application.toEntity()) + .map { it.toDomain() } + + override fun findAllMountByVolume(volume: Volume): List = + volumeMountRepository.findAllByVolume(volume.toEntity()) + .map { it.toDomain() } + + override fun findMountByApplicationAndVolume(application: Application, volume: Volume): VolumeMount? = + volumeMountRepository.findByVolumeAndApplication(volume.toEntity(), application.toEntity()) + ?.toDomain() +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/persistence/volume/adapter/VolumeAdapter.kt b/src/main/kotlin/com/dcd/server/persistence/volume/adapter/VolumeAdapter.kt new file mode 100644 index 00000000..9ace0a98 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/persistence/volume/adapter/VolumeAdapter.kt @@ -0,0 +1,42 @@ +package com.dcd.server.persistence.volume.adapter + +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.persistence.application.adapter.toDomain +import com.dcd.server.persistence.application.adapter.toEntity +import com.dcd.server.persistence.volume.entity.VolumeJpaEntity +import com.dcd.server.persistence.volume.entity.VolumeMountJpaEntity +import com.dcd.server.persistence.workspace.adapter.toDomain +import com.dcd.server.persistence.workspace.adapter.toEntity + +fun Volume.toEntity(): VolumeJpaEntity = + VolumeJpaEntity( + id = this.id, + name = this.name, + description = this.description, + workspace = this.workspace.toEntity() + ) + +fun VolumeJpaEntity.toDomain(): Volume = + Volume( + id = this.id, + name = this.name, + description = this.description, + workspace = this.workspace.toDomain() + ) + +fun VolumeMount.toEntity(): VolumeMountJpaEntity = + VolumeMountJpaEntity( + application = this.application.toEntity(), + volume = this.volume.toEntity(), + mountPath = this.mountPath, + readOnly = this.readOnly + ) + +fun VolumeMountJpaEntity.toDomain(): VolumeMount = + VolumeMount( + application = this.application.toDomain(), + volume = this.volume.toDomain(), + mountPath = this.mountPath, + readOnly = this.readOnly + ) \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/persistence/volume/entity/VolumeJpaEntity.kt b/src/main/kotlin/com/dcd/server/persistence/volume/entity/VolumeJpaEntity.kt new file mode 100644 index 00000000..fdc9188b --- /dev/null +++ b/src/main/kotlin/com/dcd/server/persistence/volume/entity/VolumeJpaEntity.kt @@ -0,0 +1,24 @@ +package com.dcd.server.persistence.volume.entity + +import com.dcd.server.persistence.workspace.entity.WorkspaceJpaEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import java.util.UUID + +@Entity +@Table(name = "volume_entity") +class VolumeJpaEntity( + @Id + @Column(columnDefinition = "BINARY(16)") + val id: UUID, + val name: String, + val description: String, + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "workspace_id") + val workspace: WorkspaceJpaEntity, +) \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/persistence/volume/entity/VolumeMountJpaEntity.kt b/src/main/kotlin/com/dcd/server/persistence/volume/entity/VolumeMountJpaEntity.kt new file mode 100644 index 00000000..ad9afb8d --- /dev/null +++ b/src/main/kotlin/com/dcd/server/persistence/volume/entity/VolumeMountJpaEntity.kt @@ -0,0 +1,39 @@ +package com.dcd.server.persistence.volume.entity + +import com.dcd.server.persistence.application.entity.ApplicationJpaEntity +import jakarta.persistence.Column +import jakarta.persistence.Embeddable +import jakarta.persistence.EmbeddedId +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.MapsId +import jakarta.persistence.Table +import java.io.Serializable +import java.util.UUID + +@Entity +@Table(name = "volume_mount_entity") +class VolumeMountJpaEntity( + @MapsId("applicationId") + @ManyToOne + @JoinColumn(name = "application_id") + val application: ApplicationJpaEntity, + @MapsId("volumeId") + @ManyToOne + @JoinColumn(name = "volume_id") + val volume: VolumeJpaEntity, + val mountPath: String, + val readOnly: Boolean +) { + @EmbeddedId + val id: VolumeMountId = VolumeMountId(application.id, volume.id) + @Embeddable + data class VolumeMountId( + @Column(nullable = false) + val applicationId: UUID, + @Column(nullable = false) + val volumeId: UUID + ) : Serializable +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/persistence/volume/repository/VolumeMountRepository.kt b/src/main/kotlin/com/dcd/server/persistence/volume/repository/VolumeMountRepository.kt new file mode 100644 index 00000000..48e97dc8 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/persistence/volume/repository/VolumeMountRepository.kt @@ -0,0 +1,13 @@ +package com.dcd.server.persistence.volume.repository + +import com.dcd.server.persistence.application.entity.ApplicationJpaEntity +import com.dcd.server.persistence.volume.entity.VolumeJpaEntity +import com.dcd.server.persistence.volume.entity.VolumeMountJpaEntity +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface VolumeMountRepository : JpaRepository { + fun findAllByVolume(volume: VolumeJpaEntity): List + fun findAllByApplication(application: ApplicationJpaEntity): List + fun findByVolumeAndApplication(volume: VolumeJpaEntity, application: ApplicationJpaEntity): VolumeMountJpaEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/persistence/volume/repository/VolumeRepository.kt b/src/main/kotlin/com/dcd/server/persistence/volume/repository/VolumeRepository.kt new file mode 100644 index 00000000..764866bb --- /dev/null +++ b/src/main/kotlin/com/dcd/server/persistence/volume/repository/VolumeRepository.kt @@ -0,0 +1,11 @@ +package com.dcd.server.persistence.volume.repository + +import com.dcd.server.persistence.volume.entity.VolumeJpaEntity +import com.dcd.server.persistence.workspace.entity.WorkspaceJpaEntity +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface VolumeRepository : JpaRepository { + fun existsByNameAndWorkspace(name: String, workspace: WorkspaceJpaEntity): Boolean + fun findAllByWorkspace(workspace: WorkspaceJpaEntity): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapter.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapter.kt new file mode 100644 index 00000000..58ddc0b1 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapter.kt @@ -0,0 +1,103 @@ +package com.dcd.server.presentation.domain.volume + +import com.dcd.server.core.common.annotation.WorkspaceOwnerVerification +import com.dcd.server.core.domain.volume.usecase.CreateVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.DeleteVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.GetAllVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.GetOneVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.MountVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.UnMountVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.UpdateVolumeUseCase +import com.dcd.server.presentation.common.annotation.WebAdapter +import com.dcd.server.presentation.domain.volume.data.extension.toDto +import com.dcd.server.presentation.domain.volume.data.extension.toResponse +import com.dcd.server.presentation.domain.volume.data.request.CreateVolumeRequest +import com.dcd.server.presentation.domain.volume.data.request.MountVolumeRequest +import com.dcd.server.presentation.domain.volume.data.request.UpdateVolumeRequest +import com.dcd.server.presentation.domain.volume.data.response.VolumeDetailResponse +import com.dcd.server.presentation.domain.volume.data.response.VolumeListResponse +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import java.util.UUID + +@WebAdapter("/{workspaceId}/volume") +class VolumeWebAdapter( + private val createVolumeUseCase: CreateVolumeUseCase, + private val deleteVolumeUseCase: DeleteVolumeUseCase, + private val updateVolumeUseCase: UpdateVolumeUseCase, + private val getAllVolumeUseCase: GetAllVolumeUseCase, + private val getOneVolumeUseCase: GetOneVolumeUseCase, + private val mountVolumeUseCase: MountVolumeUseCase, + private val unMountVolumeUseCase: UnMountVolumeUseCase +) { + @PostMapping + @WorkspaceOwnerVerification("#workspaceId") + fun createVolume( + @PathVariable workspaceId: String, + @Validated @RequestBody createVolumeRequest: CreateVolumeRequest + ): ResponseEntity = + createVolumeUseCase.execute(createVolumeRequest.toDto()) + .run { ResponseEntity.ok().build() } + + @DeleteMapping("/{volumeId}") + @WorkspaceOwnerVerification("#workspaceId") + fun deleteVolume( + @PathVariable workspaceId: String, + @PathVariable volumeId: UUID, + ): ResponseEntity = + deleteVolumeUseCase.execute(volumeId) + .run { ResponseEntity.ok().build() } + + @PutMapping("/{volumeId}") + @WorkspaceOwnerVerification("#workspaceId") + fun updateVolume( + @PathVariable workspaceId: String, + @PathVariable volumeId: UUID, + @Validated @RequestBody updateVolumeRequest: UpdateVolumeRequest + ): ResponseEntity = + updateVolumeUseCase.execute(volumeId, updateVolumeRequest.toDto()) + .run { ResponseEntity.ok().build() } + + @GetMapping + @WorkspaceOwnerVerification("#workspaceId") + fun getAllVolume(@PathVariable workspaceId: String): ResponseEntity = + getAllVolumeUseCase.execute() + .let { ResponseEntity.ok(it.toResponse()) } + + @GetMapping("/{volumeId}") + @WorkspaceOwnerVerification("#workspaceId") + fun getVolume( + @PathVariable workspaceId: String, + @PathVariable volumeId: UUID + ): ResponseEntity = + getOneVolumeUseCase.execute(volumeId) + .let { ResponseEntity.ok(it.toResponse()) } + + @PostMapping("/{volumeId}/mount") + @WorkspaceOwnerVerification("#workspaceId") + fun mountVolume( + @PathVariable workspaceId: String, + @PathVariable volumeId: UUID, + @RequestParam applicationId: String, + @Validated @RequestBody mountVolumeRequest: MountVolumeRequest + ): ResponseEntity = + mountVolumeUseCase.execute(volumeId, applicationId, mountVolumeRequest.toDto()) + .run { ResponseEntity.ok().build() } + + @DeleteMapping("/{volumeId}/mount") + @WorkspaceOwnerVerification("#workspaceId") + fun unMountVolume( + @PathVariable workspaceId: String, + @PathVariable volumeId: UUID, + @RequestParam applicationId: String, + ): ResponseEntity = + unMountVolumeUseCase.execute(volumeId, applicationId) + .run { ResponseEntity.ok().build() } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeRequestExtension.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeRequestExtension.kt new file mode 100644 index 00000000..c70c7778 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeRequestExtension.kt @@ -0,0 +1,26 @@ +package com.dcd.server.presentation.domain.volume.data.extension + +import com.dcd.server.core.domain.volume.dto.request.CreateVolumeReqDto +import com.dcd.server.core.domain.volume.dto.request.MountVolumeReqDto +import com.dcd.server.core.domain.volume.dto.request.UpdateVolumeReqDto +import com.dcd.server.presentation.domain.volume.data.request.CreateVolumeRequest +import com.dcd.server.presentation.domain.volume.data.request.MountVolumeRequest +import com.dcd.server.presentation.domain.volume.data.request.UpdateVolumeRequest + +fun CreateVolumeRequest.toDto(): CreateVolumeReqDto = + CreateVolumeReqDto( + name = this.name, + description = this.description + ) + +fun UpdateVolumeRequest.toDto(): UpdateVolumeReqDto = + UpdateVolumeReqDto( + name = this.name, + description = this.description + ) + +fun MountVolumeRequest.toDto(): MountVolumeReqDto = + MountVolumeReqDto( + mountPath = this.mountPath, + readOnly = this.readOnly + ) \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeResponseExtension.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeResponseExtension.kt new file mode 100644 index 00000000..226f48a7 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeResponseExtension.kt @@ -0,0 +1,38 @@ +package com.dcd.server.presentation.domain.volume.data.extension + +import com.dcd.server.core.domain.volume.dto.response.VolumeDetailResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeListResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeMountResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeSimpleResDto +import com.dcd.server.presentation.domain.application.data.exetension.toResponse +import com.dcd.server.presentation.domain.volume.data.response.VolumeDetailResponse +import com.dcd.server.presentation.domain.volume.data.response.VolumeListResponse +import com.dcd.server.presentation.domain.volume.data.response.VolumeMountResponse +import com.dcd.server.presentation.domain.volume.data.response.VolumeSimpleResponse + +fun VolumeSimpleResDto.toResponse(): VolumeSimpleResponse = + VolumeSimpleResponse( + id = this.id, + name = this.name, + description = this.description + ) + +fun VolumeListResDto.toResponse(): VolumeListResponse = + VolumeListResponse( + list = this.list.map { it.toResponse() } + ) + +fun VolumeMountResDto.toResponse(): VolumeMountResponse = + VolumeMountResponse( + mountPath = this.mountPath, + readOnly = this.readOnly, + applicationInfo = this.applicationInfo.toResponse() + ) + +fun VolumeDetailResDto.toResponse(): VolumeDetailResponse = + VolumeDetailResponse( + id = this.id, + name = this.name, + description = this.description, + mountList = this.mountList.map { it.toResponse() } + ) \ No newline at end of file diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/CreateVolumeRequest.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/CreateVolumeRequest.kt new file mode 100644 index 00000000..5f24786f --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/CreateVolumeRequest.kt @@ -0,0 +1,12 @@ +package com.dcd.server.presentation.domain.volume.data.request + +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern + +data class CreateVolumeRequest( + @field:NotBlank + @field:Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9_.\\s-]{0,62}$") + val name: String, + @field:NotBlank + val description: String +) diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt new file mode 100644 index 00000000..2d6b225d --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt @@ -0,0 +1,11 @@ +package com.dcd.server.presentation.domain.volume.data.request + +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern + +data class MountVolumeRequest( + @field:NotBlank + @field:Pattern(regexp = "^(?!(?:/(?:proc|sys|dev|etc)?$))/(?:(?:[A-Za-z0-9._-]+/)*[A-Za-z0-9._-]+)?$") + val mountPath: String, + val readOnly: Boolean, +) diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/UpdateVolumeRequest.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/UpdateVolumeRequest.kt new file mode 100644 index 00000000..24d517fd --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/UpdateVolumeRequest.kt @@ -0,0 +1,12 @@ +package com.dcd.server.presentation.domain.volume.data.request + +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern + +data class UpdateVolumeRequest( + @field:NotBlank + @field:Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9_.\\s-]{0,62}$") + val name: String, + @field:NotBlank + val description: String +) diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeDetailResponse.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeDetailResponse.kt new file mode 100644 index 00000000..7d395761 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeDetailResponse.kt @@ -0,0 +1,10 @@ +package com.dcd.server.presentation.domain.volume.data.response + +import java.util.UUID + +data class VolumeDetailResponse( + val id: UUID, + val name: String, + val description: String, + val mountList: List +) diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeListResponse.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeListResponse.kt new file mode 100644 index 00000000..e4798113 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeListResponse.kt @@ -0,0 +1,5 @@ +package com.dcd.server.presentation.domain.volume.data.response + +data class VolumeListResponse( + val list: List +) diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeMountResponse.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeMountResponse.kt new file mode 100644 index 00000000..8da45a59 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeMountResponse.kt @@ -0,0 +1,9 @@ +package com.dcd.server.presentation.domain.volume.data.response + +import com.dcd.server.presentation.domain.application.data.response.ApplicationProfileResponse + +data class VolumeMountResponse( + val mountPath: String, + val readOnly: Boolean, + val applicationInfo: ApplicationProfileResponse +) diff --git a/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeSimpleResponse.kt b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeSimpleResponse.kt new file mode 100644 index 00000000..776fbc38 --- /dev/null +++ b/src/main/kotlin/com/dcd/server/presentation/domain/volume/data/response/VolumeSimpleResponse.kt @@ -0,0 +1,9 @@ +package com.dcd.server.presentation.domain.volume.data.response + +import java.util.UUID + +data class VolumeSimpleResponse( + val id: UUID, + val name: String, + val description: String +) diff --git a/src/test/kotlin/com/dcd/server/core/domain/application/service/CreateContainerServiceImplTest.kt b/src/test/kotlin/com/dcd/server/core/domain/application/service/CreateContainerServiceImplTest.kt index cb9ffd3b..2efe52c4 100644 --- a/src/test/kotlin/com/dcd/server/core/domain/application/service/CreateContainerServiceImplTest.kt +++ b/src/test/kotlin/com/dcd/server/core/domain/application/service/CreateContainerServiceImplTest.kt @@ -4,7 +4,9 @@ import com.dcd.server.core.common.command.CommandPort import com.dcd.server.core.domain.application.service.impl.CreateContainerServiceImpl import com.dcd.server.core.domain.application.spi.CheckExitValuePort import com.dcd.server.core.domain.application.util.FailureCase +import com.dcd.server.core.domain.volume.spi.QueryVolumePort import io.kotest.core.spec.style.BehaviorSpec +import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.CoroutineScope @@ -12,14 +14,16 @@ import util.application.ApplicationGenerator class CreateContainerServiceImplTest : BehaviorSpec({ val commandPort = mockk(relaxed = true) + val queryVolumePort = mockk(relaxed = true) val checkExitValuePort = mockk(relaxUnitFun = true) - val createContainerService = CreateContainerServiceImpl(commandPort, checkExitValuePort) + val createContainerService = CreateContainerServiceImpl(commandPort, queryVolumePort, checkExitValuePort) given("애플리케이션이 주어지고") { val application = ApplicationGenerator.generateApplication() `when`("service를 실행할때") { createContainerService.createContainer(application, application.externalPort) + every { queryVolumePort.findAllMountByApplication(application) } returns listOf() then("컨테이너를 실행하는 명령을 실행해야함") { verify { diff --git a/src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt index 16541755..2f65cb06 100644 --- a/src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt +++ b/src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt @@ -4,6 +4,7 @@ import com.dcd.server.core.common.command.CommandPort import com.dcd.server.core.domain.application.exception.ApplicationNotFoundException import com.dcd.server.core.domain.application.exception.CanNotDeployApplicationException import com.dcd.server.core.domain.application.model.enums.ApplicationStatus +import com.dcd.server.core.domain.application.service.CreateContainerService import com.dcd.server.core.domain.application.service.impl.CreateDockerFileServiceImpl import com.dcd.server.core.domain.application.spi.CommandApplicationPort import com.dcd.server.core.domain.application.spi.QueryApplicationPort @@ -18,6 +19,7 @@ import com.ninjasquad.springmockk.MockkBean import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.verify import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ActiveProfiles import org.springframework.transaction.annotation.Transactional @@ -32,6 +34,8 @@ class DeployApplicationUseCaseTest( private val commandPort: CommandPort, @MockkBean(relaxUnitFun = true) private val createDockerFileService: CreateDockerFileServiceImpl, + @MockkBean(relaxUnitFun = true) + private val createContainerService: CreateContainerService, private val commandUserPort: CommandUserPort, private val commandWorkspacePort: CommandWorkspacePort, private val commandApplicationPort: CommandApplicationPort, @@ -64,13 +68,7 @@ class DeployApplicationUseCaseTest( coVerify { commandPort.executeShellCommand("docker rmi ${result.containerName}") } coVerify { commandPort.executeShellCommand("git clone ${result.githubUrl} '${result.name}'") } coVerify { commandPort.executeShellCommand("cd ./'${result.name}' && docker build -t ${result.containerName}:latest .") } - coVerify { - commandPort.executeShellCommand( - "docker create --network ${result.workspace.title.replace(' ', '_')} " + - "--name ${result.containerName} " + - "-p ${result.externalPort}:${result.port} ${result.containerName}:latest" - ) - } + coVerify { createContainerService.createContainer(result, result.externalPort) } coVerify { commandPort.executeShellCommand("rm -rf '${result.name}'") } } } diff --git a/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/CreateVolumeUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/CreateVolumeUseCaseTest.kt new file mode 100644 index 00000000..91e2068c --- /dev/null +++ b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/CreateVolumeUseCaseTest.kt @@ -0,0 +1,109 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.dto.extension.toEntity +import com.dcd.server.core.domain.volume.dto.request.CreateVolumeReqDto +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeException +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import com.dcd.server.core.domain.workspace.spi.QueryWorkspacePort +import com.dcd.server.persistence.user.adapter.toEntity +import com.dcd.server.persistence.user.repository.UserRepository +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeRepository +import com.dcd.server.persistence.workspace.adapter.toEntity +import com.dcd.server.persistence.workspace.repository.WorkspaceRepository +import com.ninjasquad.springmockk.MockkBean +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import util.user.UserGenerator +import util.workspace.WorkspaceGenerator + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class CreateVolumeUseCaseTest( + private val createVolumeUseCase: CreateVolumeUseCase, + @MockkBean(relaxed = true) + private val commandPort: CommandPort, + private val volumeRepository: VolumeRepository, + private val workspaceRepository: WorkspaceRepository, + private val userRepository: UserRepository, + private val queryWorkspacePort: QueryWorkspacePort, + private val workspaceInfo: WorkspaceInfo +) : BehaviorSpec({ + + given("목표 워크스페이스와 볼륨 생성 요청이 주어지고") { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + val createVolumeReqDto = CreateVolumeReqDto(name = "testVolume", description = "test") + + `when`("목표 워크스페이스에 중복되는 이름의 볼륨이 없을때") { + createVolumeUseCase.execute(createVolumeReqDto) + + then("요청 정보를 가진 볼륨이 정상적으로 생성되어야함") { + val volumeList = volumeRepository.findAll() + volumeList.size shouldBe 1 + + val targetVolume = volumeList.first() + targetVolume.name shouldBe createVolumeReqDto.name + targetVolume.description shouldBe createVolumeReqDto.description + targetVolume.workspace.id.toString() shouldBe workspaceInfo.workspace!!.id + } + } + + volumeRepository.deleteAll() + + `when`("다른 워크스페이스에 해당 이름의 볼륨이 존재할때") { + val user = UserGenerator.generateUser() + userRepository.save(user.toEntity()) + val workspace = WorkspaceGenerator.generateWorkspace(user = user) + workspaceRepository.save(workspace.toEntity()) + val volumeModel = createVolumeReqDto.toEntity(workspace) + volumeRepository.save(volumeModel.toEntity()) + + createVolumeUseCase.execute(createVolumeReqDto) + + then("볼륨은 정상적으로 생성되어야함") { + val volumeList = volumeRepository.findAll().filter { it.workspace.id.toString() == workspaceInfo.workspace!!.id } + volumeList.size shouldBe 1 + + val targetVolume = volumeList.first() + targetVolume.name shouldBe createVolumeReqDto.name + targetVolume.description shouldBe createVolumeReqDto.description + targetVolume.workspace.id.toString() shouldBe workspaceInfo.workspace!!.id + } + } + + volumeRepository.deleteAll() + + `when`("이미 해당 이름의 볼륨이 존재할때") { + val volumeModel = createVolumeReqDto.toEntity(workspaceInfo.workspace!!) + volumeRepository.save(volumeModel.toEntity()) + + then("볼륨 생성에 실패해야함") { + shouldThrow { + createVolumeUseCase.execute(createVolumeReqDto) + } + } + } + } + + given("워크스페이스 정보가 초기화되지 않고") { + workspaceInfo.workspace = null + val createVolumeReqDto = CreateVolumeReqDto(name = "testVolume", description = "test") + + `when`("유스케이스를 실행할때") { + + then("에러가 발생해야함") { + shouldThrow { + createVolumeUseCase.execute(createVolumeReqDto) + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/DeleteVolumeUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/DeleteVolumeUseCaseTest.kt new file mode 100644 index 00000000..b7fcbf9a --- /dev/null +++ b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/DeleteVolumeUseCaseTest.kt @@ -0,0 +1,133 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.application.spi.QueryApplicationPort +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeMountException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.workspace.spi.QueryWorkspacePort +import com.dcd.server.persistence.user.repository.UserRepository +import com.dcd.server.persistence.volume.adapter.toDomain +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeMountRepository +import com.dcd.server.persistence.volume.repository.VolumeRepository +import com.dcd.server.persistence.workspace.adapter.toEntity +import com.dcd.server.persistence.workspace.repository.WorkspaceRepository +import com.ninjasquad.springmockk.MockkBean +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import util.workspace.WorkspaceGenerator +import java.util.UUID + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class DeleteVolumeUseCaseTest( + private val deleteVolumeUseCase: DeleteVolumeUseCase, + @MockkBean(relaxed = true) + private val commandPort: CommandPort, + private val volumeRepository: VolumeRepository, + private val volumeMountRepository: VolumeMountRepository, + private val workspaceRepository: WorkspaceRepository, + private val userRepository: UserRepository, + private val queryWorkspacePort: QueryWorkspacePort, + private val queryApplicationPort: QueryApplicationPort, + private val workspaceInfo: WorkspaceInfo +) : BehaviorSpec({ + val targetVolumeId = UUID.randomUUID() + + beforeSpec { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val volume = Volume( + id = targetVolumeId, + name = "testVolume", + description = "testDescription", + workspace = targetWorkspace + ) + volumeRepository.save(volume.toEntity()) + } + + given("타켓 볼륨 아이디가 주어지고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + `when`("유스케이스를 실행할때") { + deleteVolumeUseCase.execute(targetVolumeId) + + then("해당 볼륨이 삭제되어야함") { + volumeRepository.findByIdOrNull(targetVolumeId) shouldBe null + } + } + } + + given("볼륨이 존재하지 않고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + volumeRepository.deleteAll() + + `when`("유스케이스를 실행하면") { + + then("에러가 발생해야함") { + shouldThrow { + deleteVolumeUseCase.execute(targetVolumeId) + } + } + } + } + + given("마운트된 볼륨이 존재하고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + val application = queryApplicationPort.findById("2fb0f315-8272-422f-8e9f-c4f765c022b2")!! + val volume = volumeRepository.findByIdOrNull(targetVolumeId)!!.toDomain() + val volumeMount = VolumeMount( + application = application, + volume = volume, + mountPath = "/test/volume", + readOnly = false + ) + volumeMountRepository.save(volumeMount.toEntity()) + + `when`("유스케이스를 실행하면") { + + then("에러가 발생해야함") { + shouldThrow { + deleteVolumeUseCase.execute(targetVolumeId) + } + } + } + } + + given("볼륨이 속한 워크스페이스가 아니고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val otherWorkspace = WorkspaceGenerator.generateWorkspace(user = targetWorkspace.owner) + workspaceRepository.save(otherWorkspace.toEntity()) + workspaceInfo.workspace = otherWorkspace + } + + `when`("유스케이스를 실행하면") { + + then("에러가 발생해야함") { + shouldThrow { + deleteVolumeUseCase.execute(targetVolumeId) + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/GetAllVolumeUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/GetAllVolumeUseCaseTest.kt new file mode 100644 index 00000000..13caf626 --- /dev/null +++ b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/GetAllVolumeUseCaseTest.kt @@ -0,0 +1,78 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.dto.extension.toResDto +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import com.dcd.server.core.domain.workspace.spi.QueryWorkspacePort +import com.dcd.server.persistence.volume.adapter.toDomain +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeRepository +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainOnly +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class GetAllVolumeUseCaseTest( + private val getAllVolumeUseCase: GetAllVolumeUseCase, + private val queryWorkspacePort: QueryWorkspacePort, + private val volumeRepository: VolumeRepository, + private val workspaceInfo: WorkspaceInfo +) : BehaviorSpec({ + val targetVolume1Id = UUID.randomUUID() + val targetVolume2Id = UUID.randomUUID() + + beforeSpec { + volumeRepository.deleteAll() + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val volume1 = Volume( + id = targetVolume1Id, + name = "test1Volume", + description = "testDescription", + workspace = targetWorkspace + ).toEntity() + val volume2 = Volume( + id = targetVolume2Id, + name = "test2Volume", + description = "testDescription", + workspace = targetWorkspace + ).toEntity() + volumeRepository.saveAll(listOf(volume1, volume2)) + } + + given("알맞는 워크스페이스가 세팅되고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + `when`("유스케이스를 실행할때") { + val result = getAllVolumeUseCase.execute() + + then("타켓 볼륨의 정보가 반환되어야함") { + val targetVolumeList = volumeRepository.findAllById(listOf(targetVolume1Id, targetVolume2Id)) + targetVolumeList.size shouldBe 2 + result.list shouldContainOnly targetVolumeList.map { it.toDomain().toResDto() } + } + } + } + + given("워크스페이스가 세팅되지 않고") { + + `when`("유스케이스를 실행할때") { + + then("에러가 발생해야함") { + shouldThrow { + getAllVolumeUseCase.execute() + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/GetOneVolumeUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/GetOneVolumeUseCaseTest.kt new file mode 100644 index 00000000..c5ea6d98 --- /dev/null +++ b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/GetOneVolumeUseCaseTest.kt @@ -0,0 +1,105 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.volume.dto.extension.toDetailResDto +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import com.dcd.server.core.domain.workspace.spi.QueryWorkspacePort +import com.dcd.server.persistence.volume.adapter.toDomain +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeRepository +import com.dcd.server.persistence.workspace.adapter.toEntity +import com.dcd.server.persistence.workspace.repository.WorkspaceRepository +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import util.workspace.WorkspaceGenerator +import java.util.UUID + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class GetOneVolumeUseCaseTest( + private val getOneVolumeUseCase: GetOneVolumeUseCase, + private val queryWorkspacePort: QueryWorkspacePort, + private val workspaceRepository: WorkspaceRepository, + private val volumeRepository: VolumeRepository, + private val workspaceInfo: WorkspaceInfo +) : BehaviorSpec({ + val targetVolumeId = UUID.randomUUID() + + beforeSpec { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val volume = Volume( + id = targetVolumeId, + name = "test1Volume", + description = "testDescription", + workspace = targetWorkspace + ).toEntity() + volumeRepository.save(volume) + } + + given("알맞는 워크스페이스가 세팅되고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + `when`("유스케이스를 실행할때") { + val result = getOneVolumeUseCase.execute(targetVolumeId) + + then("타켓 볼륨의 정보가 반환되어야함") { + val targetVolume = volumeRepository.findByIdOrNull(targetVolumeId) + targetVolume.shouldNotBeNull() + + result shouldBe targetVolume.toDomain().toDetailResDto(listOf()) + } + } + + `when`("존재하지 않는 볼륨아이디로 실행할때") { + val invalidVolumeId = UUID.randomUUID() + + then("에러가 발생해야함") { + shouldThrow { + getOneVolumeUseCase.execute(invalidVolumeId) + } + } + } + } + + given("워크스페이스가 세팅되지 않고") { + + `when`("유스케이스를 실행할때") { + + then("에러가 발생해야함") { + shouldThrow { + getOneVolumeUseCase.execute(targetVolumeId) + } + } + } + } + + given("올바른 워크스페이스가 세팅되지 않고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val otherWorkspace = WorkspaceGenerator.generateWorkspace(user = targetWorkspace.owner) + workspaceRepository.save(otherWorkspace.toEntity()) + workspaceInfo.workspace = otherWorkspace + } + + `when`("유스케이스를 실행하면") { + + then("에러가 발생해야함") { + shouldThrow { + getOneVolumeUseCase.execute(targetVolumeId) + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCaseTest.kt new file mode 100644 index 00000000..f773c3ea --- /dev/null +++ b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCaseTest.kt @@ -0,0 +1,154 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.application.exception.ApplicationNotFoundException +import com.dcd.server.core.domain.application.spi.QueryApplicationPort +import com.dcd.server.core.domain.volume.dto.request.MountVolumeReqDto +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeMountException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import com.dcd.server.core.domain.workspace.spi.QueryWorkspacePort +import com.dcd.server.persistence.volume.adapter.toDomain +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeMountRepository +import com.dcd.server.persistence.volume.repository.VolumeRepository +import com.dcd.server.persistence.workspace.adapter.toEntity +import com.dcd.server.persistence.workspace.repository.WorkspaceRepository +import com.ninjasquad.springmockk.MockkBean +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import util.workspace.WorkspaceGenerator +import java.util.UUID + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class MountVolumeUseCaseTest( + private val mountVolumeUseCase: MountVolumeUseCase, + @MockkBean(relaxed = true) + private val commandPort: CommandPort, + private val queryWorkspacePort: QueryWorkspacePort, + private val queryApplicationPort: QueryApplicationPort, + private val volumeRepository: VolumeRepository, + private val volumeMountRepository: VolumeMountRepository, + private val workspaceRepository: WorkspaceRepository, + private val workspaceInfo: WorkspaceInfo +) : BehaviorSpec({ + val targetVolumeId = UUID.randomUUID() + val targetApplicationId = "2fb0f315-8272-422f-8e9f-c4f765c022b2" + + beforeSpec { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val volume = Volume( + id = targetVolumeId, + name = "test1Volume", + description = "testDescription", + workspace = targetWorkspace + ).toEntity() + volumeRepository.save(volume) + } + + given("마운트 요청 객체가 주어지고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + val request = MountVolumeReqDto("/test", false) + + `when`("유스케이스를 실행하면") { + mountVolumeUseCase.execute(targetVolumeId, targetApplicationId, request) + + then("볼륨 마운트가 생성되어야함") { + val volumeMountList = volumeMountRepository.findAll() + volumeMountList.size shouldBe 1 + + val first = volumeMountList.first() + first.mountPath shouldBe request.mountPath + first.readOnly shouldBe request.readOnly + first.application.id.toString() shouldBe targetApplicationId + first.volume.id shouldBe targetVolumeId + } + } + + `when`("존재하지 않는 볼륨 아이디로 실행하면") { + val invalidVolumeId = UUID.randomUUID() + + then("에러가 발생해야함") { + shouldThrow { + mountVolumeUseCase.execute(invalidVolumeId, targetApplicationId, request) + } + } + } + + `when`("존재하지 않는 애플리케이션 아이디로 실행하면") { + val invalidApplicationId = UUID.randomUUID().toString() + + then("에러가 발생행해야함") { + shouldThrow { + mountVolumeUseCase.execute(targetVolumeId, invalidApplicationId, request) + } + } + } + + `when`("세팅된 워크스페이스가 볼륨이 속한 워크스페이스가 아닐때") { + beforeTest { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val otherWorkspace = WorkspaceGenerator.generateWorkspace(user = targetWorkspace.owner) + workspaceRepository.save(otherWorkspace.toEntity()) + workspaceInfo.workspace = otherWorkspace + } + + then("에러가 발생해야함") { + shouldThrow { + mountVolumeUseCase.execute(targetVolumeId, targetApplicationId, request) + } + } + } + + `when`("이미 볼륨에 마운트 됐을때") { + beforeContainer { + val application = queryApplicationPort.findById(targetApplicationId)!! + val volume = volumeRepository.findByIdOrNull(targetVolumeId)!!.toDomain() + val volumeMount = VolumeMount( + application = application, + volume = volume, + mountPath = "/test/volume", + readOnly = false + ) + volumeMountRepository.save(volumeMount.toEntity()) + } + + then("에러가 발생해야함") { + shouldThrow { + mountVolumeUseCase.execute(targetVolumeId, targetApplicationId, request) + } + } + } + } + + given("올바르지 않은 워크스페이스가 세팅되고") { + beforeContainer { + workspaceInfo.workspace = null + } + + val request = MountVolumeReqDto("/test", false) + + `when`("유스케이스를 실행할때") { + + then("에러가 발생해야함") { + shouldThrow { + mountVolumeUseCase.execute(targetVolumeId, targetApplicationId, request) + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/UnMountVolumeUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/UnMountVolumeUseCaseTest.kt new file mode 100644 index 00000000..949e3cdb --- /dev/null +++ b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/UnMountVolumeUseCaseTest.kt @@ -0,0 +1,144 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.application.exception.ApplicationNotFoundException +import com.dcd.server.core.domain.application.spi.QueryApplicationPort +import com.dcd.server.core.domain.volume.exception.VolumeMountNotFoundException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.workspace.exception.WorkspaceNotFoundException +import com.dcd.server.core.domain.workspace.spi.QueryWorkspacePort +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeMountRepository +import com.dcd.server.persistence.volume.repository.VolumeRepository +import com.dcd.server.persistence.workspace.adapter.toEntity +import com.dcd.server.persistence.workspace.repository.WorkspaceRepository +import com.ninjasquad.springmockk.MockkBean +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import util.workspace.WorkspaceGenerator +import java.util.UUID + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class UnMountVolumeUseCaseTest( + private val unMountVolumeUseCase: UnMountVolumeUseCase, + @MockkBean(relaxed = true) + private val commandPort: CommandPort, + private val queryWorkspacePort: QueryWorkspacePort, + private val queryApplicationPort: QueryApplicationPort, + private val volumeRepository: VolumeRepository, + private val volumeMountRepository: VolumeMountRepository, + private val workspaceRepository: WorkspaceRepository, + private val workspaceInfo: WorkspaceInfo +) : BehaviorSpec({ + val targetVolumeId = UUID.randomUUID() + val targetApplicationId = "2fb0f315-8272-422f-8e9f-c4f765c022b2" + + beforeSpec { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val volume = Volume( + id = targetVolumeId, + name = "test1Volume", + description = "testDescription", + workspace = targetWorkspace + ) + volumeRepository.save(volume.toEntity()) + + val application = queryApplicationPort.findById(targetApplicationId)!! + val volumeMount = VolumeMount( + application = application, + volume = volume, + mountPath = "/test/volume", + readOnly = false + ) + volumeMountRepository.save(volumeMount.toEntity()) + } + + given("볼륨과 같은 워크스페이스가 세팅되었고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + `when`("유스케이스를 실행할때") { + unMountVolumeUseCase.execute(targetVolumeId, targetApplicationId) + + then("볼륨 마운트 엔티티가 삭제되어야함") { + volumeMountRepository.findAll().isEmpty() shouldBe true + } + } + + `when`("존재하지 않는 볼륨 아이디로 실행할때") { + val notFoundVolumeId = UUID.randomUUID() + + then("에러가 발생해야함") { + shouldThrow { + unMountVolumeUseCase.execute(notFoundVolumeId, targetApplicationId) + } + } + } + + `when`("존재하지 않는 애플리케이션 아이디로 실행할때") { + val notFoundApplicationId = UUID.randomUUID().toString() + + then("에러가 발생해야함") { + shouldThrow { + unMountVolumeUseCase.execute(targetVolumeId, notFoundApplicationId) + } + } + } + + `when`("마운트된 볼륨이 아닐때") { + beforeContainer { + volumeMountRepository.deleteAll() + } + + then("에러가 발생해야함") { + shouldThrow { + unMountVolumeUseCase.execute(targetVolumeId, targetApplicationId) + } + } + } + } + + given("세팅된 워크스페이스가 볼륨이 속한 워크스페이스가 아니고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val otherWorkspace = WorkspaceGenerator.generateWorkspace(user = targetWorkspace.owner) + workspaceRepository.save(otherWorkspace.toEntity()) + workspaceInfo.workspace = otherWorkspace + } + + `when`("유스케이스를 실행할때") { + + then("에러가 발생해야함") { + shouldThrow { + unMountVolumeUseCase.execute(targetVolumeId, targetApplicationId) + } + } + } + } + + given("워크스페이스가 세팅되지 않고") { + beforeContainer { + workspaceInfo.workspace = null + } + + `when`("유스케이스를 실행할때") { + + then("에러가 발생해야함") { + shouldThrow { + unMountVolumeUseCase.execute(targetVolumeId, targetApplicationId) + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/UpdateVolumeUseCaseTest.kt b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/UpdateVolumeUseCaseTest.kt new file mode 100644 index 00000000..04e5ce7e --- /dev/null +++ b/src/test/kotlin/com/dcd/server/core/domain/volume/usecase/UpdateVolumeUseCaseTest.kt @@ -0,0 +1,145 @@ +package com.dcd.server.core.domain.volume.usecase + +import com.dcd.server.core.common.command.CommandPort +import com.dcd.server.core.common.data.WorkspaceInfo +import com.dcd.server.core.domain.application.spi.QueryApplicationPort +import com.dcd.server.core.domain.volume.dto.request.UpdateVolumeReqDto +import com.dcd.server.core.domain.volume.exception.AlreadyExistsVolumeMountException +import com.dcd.server.core.domain.volume.exception.VolumeNotFoundException +import com.dcd.server.core.domain.volume.model.Volume +import com.dcd.server.core.domain.volume.model.VolumeMount +import com.dcd.server.core.domain.workspace.spi.QueryWorkspacePort +import com.dcd.server.persistence.volume.adapter.toDomain +import com.dcd.server.persistence.volume.adapter.toEntity +import com.dcd.server.persistence.volume.repository.VolumeMountRepository +import com.dcd.server.persistence.volume.repository.VolumeRepository +import com.dcd.server.persistence.workspace.adapter.toEntity +import com.dcd.server.persistence.workspace.repository.WorkspaceRepository +import com.ninjasquad.springmockk.MockkBean +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import util.workspace.WorkspaceGenerator +import java.util.UUID + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class UpdateVolumeUseCaseTest( + private val updateVolumeUseCase: UpdateVolumeUseCase, + @MockkBean(relaxed = true) + private val commandPort: CommandPort, + private val workspaceInfo: WorkspaceInfo, + private val volumeRepository: VolumeRepository, + private val volumeMountRepository: VolumeMountRepository, + private val workspaceRepository: WorkspaceRepository, + private val queryWorkspacePort: QueryWorkspacePort, + private val queryApplicationPort: QueryApplicationPort, +) : BehaviorSpec({ + val targetVolumeId = UUID.randomUUID() + + beforeSpec { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + + val volume = Volume( + id = targetVolumeId, + name = "testVolume", + description = "testDescription", + workspace = targetWorkspace + ) + volumeRepository.save(volume.toEntity()) + } + + given("타겟 볼륨 아이디와 수정할 볼륨 요청 dto가 주어지고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + val request = UpdateVolumeReqDto(name = "updateVolume", description = "updateDescription") + + `when`("유스케이스를 실행할때") { + updateVolumeUseCase.execute(targetVolumeId, request) + + then("볼륨 정보가 수정되어야함") { + val targetVolume = volumeRepository.findByIdOrNull(targetVolumeId) + + targetVolume shouldNotBe null + targetVolume!!.name shouldBe request.name + targetVolume.description shouldBe request.description + } + } + } + + given("볼륨이 존재하지 않고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + volumeRepository.deleteAll() + val request = UpdateVolumeReqDto(name = "updateVolume", description = "updateDescription") + + `when`("유스케이스를 실행할때") { + + then("예외가 발생해야함") { + shouldThrow { + updateVolumeUseCase.execute(targetVolumeId, request) + } + } + } + } + + given("볼륨 마운트가 존재하고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + workspaceInfo.workspace = targetWorkspace + } + + val application = queryApplicationPort.findById("2fb0f315-8272-422f-8e9f-c4f765c022b2")!! + val volume = volumeRepository.findByIdOrNull(targetVolumeId)!!.toDomain() + val volumeMount = VolumeMount( + application = application, + volume = volume, + mountPath = "/test/volume", + readOnly = false + ) + volumeMountRepository.save(volumeMount.toEntity()) + + val request = UpdateVolumeReqDto(name = "updateVolume", description = "updateDescription") + + `when`("유스케이스를 실행하면") { + + then("에러가 발생해야함") { + shouldThrow { + updateVolumeUseCase.execute(targetVolumeId, request) + } + } + } + } + + given("볼륨이 속한 워크스페이스가 아니고") { + beforeContainer { + val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!! + val otherWorkspace = WorkspaceGenerator.generateWorkspace(user = targetWorkspace.owner) + workspaceRepository.save(otherWorkspace.toEntity()) + workspaceInfo.workspace = otherWorkspace + } + + val request = UpdateVolumeReqDto(name = "updateVolume", description = "updateDescription") + + `when`("유스케이스를 실행하면") { + + then("에러가 발생해야함") { + shouldThrow { + updateVolumeUseCase.execute(targetVolumeId, request) + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dcd/server/presentation/env/EnvWebAdapterTest.kt b/src/test/kotlin/com/dcd/server/presentation/domain/env/EnvWebAdapterTest.kt similarity index 97% rename from src/test/kotlin/com/dcd/server/presentation/env/EnvWebAdapterTest.kt rename to src/test/kotlin/com/dcd/server/presentation/domain/env/EnvWebAdapterTest.kt index 8c15d8de..98fa38b7 100644 --- a/src/test/kotlin/com/dcd/server/presentation/env/EnvWebAdapterTest.kt +++ b/src/test/kotlin/com/dcd/server/presentation/domain/env/EnvWebAdapterTest.kt @@ -1,4 +1,4 @@ -package com.dcd.server.presentation.env +package com.dcd.server.presentation.domain.env import com.dcd.server.core.domain.env.dto.response.ApplicationEnvDetailResDto import com.dcd.server.core.domain.env.dto.response.ApplicationEnvListResDto @@ -41,7 +41,7 @@ class EnvWebAdapterTest : BehaviorSpec({ } } - `when`("이미 존재하는 환경변수 일때") { + `when`("updateApplicationEnv 메서드를 실행할때") { val targetEnvId = UUID.randomUUID() every { putApplicationEnvUseCase.execute(targetEnvId, any()) } returns Unit val result = applicationEnvWebAdapter.updateApplicationEnv(testId, targetEnvId, request) diff --git a/src/test/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapterTest.kt b/src/test/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapterTest.kt new file mode 100644 index 00000000..45d605b3 --- /dev/null +++ b/src/test/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapterTest.kt @@ -0,0 +1,171 @@ +package com.dcd.server.presentation.domain.volume + +import com.dcd.server.core.domain.volume.dto.request.CreateVolumeReqDto +import com.dcd.server.core.domain.volume.dto.request.MountVolumeReqDto +import com.dcd.server.core.domain.volume.dto.request.UpdateVolumeReqDto +import com.dcd.server.core.domain.volume.dto.response.VolumeDetailResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeListResDto +import com.dcd.server.core.domain.volume.dto.response.VolumeSimpleResDto +import com.dcd.server.core.domain.volume.usecase.CreateVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.DeleteVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.GetAllVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.GetOneVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.MountVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.UnMountVolumeUseCase +import com.dcd.server.core.domain.volume.usecase.UpdateVolumeUseCase +import com.dcd.server.presentation.domain.volume.data.extension.toResponse +import com.dcd.server.presentation.domain.volume.data.request.CreateVolumeRequest +import com.dcd.server.presentation.domain.volume.data.request.MountVolumeRequest +import com.dcd.server.presentation.domain.volume.data.request.UpdateVolumeRequest +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.springframework.http.HttpStatus +import java.util.UUID + +class VolumeWebAdapterTest : BehaviorSpec({ + val createVolumeUseCase = mockk(relaxUnitFun = true) + val deleteVolumeUseCase = mockk(relaxUnitFun = true) + val updateVolumeUseCase = mockk(relaxUnitFun = true) + val getAllVolumeUseCase = mockk(relaxUnitFun = true) + val getOneVolumeUseCase = mockk(relaxUnitFun = true) + val mountVolumeUseCase = mockk(relaxUnitFun = true) + val unMountVolumeUseCase = mockk(relaxUnitFun = true) + + val volumeWebAdapter = VolumeWebAdapter( + createVolumeUseCase, + deleteVolumeUseCase, + updateVolumeUseCase, + getAllVolumeUseCase, + getOneVolumeUseCase, + mountVolumeUseCase, + unMountVolumeUseCase + ) + + given("워크스페이스 아이디와 볼륨 생성 요청이 주어지고") { + val testWorkspaceId = UUID.randomUUID().toString() + val request = CreateVolumeRequest(name = "testVolume", description = "testDescription") + + `when`("볼륨 생성 메서드를 실행하면") { + val result = volumeWebAdapter.createVolume(testWorkspaceId, request) + + then("상태코드가 OK가 응답되어여함") { + result.statusCode shouldBe HttpStatus.OK + } + + then("볼륨 생성 유스케이스를 실행해야함") { + verify { createVolumeUseCase.execute(any() as CreateVolumeReqDto) } + } + } + } + + given("워크스페이스 아이디와 삭제할 볼륨 아이디가 주어지고") { + val testWorkspaceId = UUID.randomUUID().toString() + val testVolumeId = UUID.randomUUID() + + `when`("볼륨 삭제 메서드를 실행하면") { + val result = volumeWebAdapter.deleteVolume(testWorkspaceId, testVolumeId) + + then("상태코드 OK가 응답되어야함") { + result.statusCode shouldBe HttpStatus.OK + } + + then("볼륨 삭제 유스케이스를 실행해야함") { + verify { deleteVolumeUseCase.execute(testVolumeId) } + } + } + } + + given("워크스페이스 아이디, 볼륨 아이디, 볼륨 수정 요청이 주어지고") { + val testWorkspaceId = UUID.randomUUID().toString() + val testVolumeId = UUID.randomUUID() + val request = UpdateVolumeRequest(name = "testVolume", description = "testDescription") + + `when`("볼륨 수정 메서드를 실행하면") { + val result = volumeWebAdapter.updateVolume(testWorkspaceId, testVolumeId, request) + + then("상태코드 OK가 응답되어야함") { + result.statusCode shouldBe HttpStatus.OK + } + + then("볼륨 수정 유스케이스를 실행해야함") { + verify { updateVolumeUseCase.execute(testVolumeId, any() as UpdateVolumeReqDto) } + } + } + } + + given("조회할 볼륨 아이디가 주어지고") { + val testWorkspaceId = UUID.randomUUID().toString() + val testVolumeId = UUID.randomUUID() + + `when`("볼륨 단일 조회 메서드를 실행할때") { + val volumeDetailResDto = VolumeDetailResDto(testVolumeId, "testVolume", "testDescription", listOf()) + every { getOneVolumeUseCase.execute(testVolumeId) } returns volumeDetailResDto + + val result = volumeWebAdapter.getVolume(testWorkspaceId, testVolumeId) + + then("유스케이스의 응답이 레핑되서 반환되어야함") { + result.body shouldBe volumeDetailResDto.toResponse() + } + then("상태코드 OK가 응답되어야함") { + result.statusCode shouldBe HttpStatus.OK + } + } + } + + given("워크스페이스 아이디만 주어지고") { + val testWorkspaceId = UUID.randomUUID().toString() + + `when`("볼륨 목록 조회 메서드를 실행할때") { + val volumeListResDto = + VolumeListResDto(listOf(VolumeSimpleResDto(UUID.randomUUID(), "testVolume", "testDescription"))) + every { getAllVolumeUseCase.execute() } returns volumeListResDto + + val result = volumeWebAdapter.getAllVolume(testWorkspaceId) + + then("유스케이스의 응답이 레핑되서 반환되어야함") { + result.body shouldBe volumeListResDto.toResponse() + } + then("상태코드 OK가 응답되어야함") { + result.statusCode shouldBe HttpStatus.OK + } + } + } + + given("워크스페이스 아이디, 볼륨 아이디, 애플리케이션 아이디, 볼륨 마운트 요청 객체가 주어지고") { + val testWorkspaceId = UUID.randomUUID().toString() + val testVolumeId = UUID.randomUUID() + val testApplicationId = UUID.randomUUID().toString() + val request = MountVolumeRequest("/test", false) + + `when`("볼륨 마운트 메서드를 실행할때") { + val result = volumeWebAdapter.mountVolume(testWorkspaceId, testVolumeId, testApplicationId, request) + + then("볼륨 마운트 유스케이스가 실행되어야함") { + verify { mountVolumeUseCase.execute(testVolumeId, testApplicationId, any() as MountVolumeReqDto) } + } + then("상태코드 OK가 응답되어야함") { + result.statusCode shouldBe HttpStatus.OK + } + } + } + + given("워크스페이스 아이디, 볼륨 아이디, 애플리케이션 아이디가 주어지고") { + val testWorkspaceId = UUID.randomUUID().toString() + val testVolumeId = UUID.randomUUID() + val testApplicationId = UUID.randomUUID().toString() + + `when`("마운트 해제 메서드를 실행할때") { + val result = volumeWebAdapter.unMountVolume(testWorkspaceId, testVolumeId, testApplicationId) + + then("볼륨 마운트 해제 유스케이스가 실행되어야함") { + verify { unMountVolumeUseCase.execute(testVolumeId, testApplicationId) } + } + then("상태코드 OK가 응답되어야함") { + result.statusCode shouldBe HttpStatus.OK + } + } + } +}) \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 11cab0a7..688f83e7 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -11,6 +11,8 @@ drop table if exists application_env_entity cascade; drop table if exists application_env_detail_entity cascade; drop table if exists application_env_matcher_entity cascade; drop table if exists application_env_label_entity cascade; +drop table if exists volume_entity cascade; +drop table if exists volume_mount_entity cascade; create table application_entity (external_port integer not null, port integer not null, application_type varchar(255) check (application_type in ('SPRING_BOOT','NEST_JS','MYSQL','MARIA_DB','REDIS')), description varchar(255), failure_reason varchar(255), github_url varchar(255), id binary(16) not null, name varchar(255), status varchar(255) check (status in ('CREATED','PENDING','RUNNING','STOPPED','FAILURE')), version varchar(255), workspace_id binary(16), primary key (id)); create table application_label_entity (application_id binary(16) not null, label varchar(255)); create table role_entity (roles varchar(255) check (roles in ('ROLE_ADMIN','ROLE_DEVELOPER','ROLE_USER')), user_id binary(16) not null); @@ -21,6 +23,11 @@ create table application_env_entity (id BINARY(16) not null, description varchar create table application_env_detail_entity (id BINARY(16) not null, encryption bit not null, env_key varchar(255), env_value varchar(255), env_id BINARY(16), primary key (id)); create table application_env_matcher_entity (id BINARY(16) not null, application_id BINARY(16), env_id BINARY(16), primary key (id)); create table application_env_label_entity (application_env_id BINARY(16) not null, label varchar(255)); +create table volume_entity (id BINARY(16) not null, description varchar(255), name varchar(255), workspace_id BINARY(16) not null, primary key (id)); +create table volume_mount_entity (application_id binary(16) not null, volume_id binary(16) not null, mount_path varchar(255) not null, read_only bit not null, primary key (application_id, volume_id)); +alter table if exists volume_entity add constraint FKhp1ggnqtveky8po2cwsb45una foreign key (workspace_id) references workspace_entity (id) on delete cascade; +alter table if exists volume_mount_entity add constraint FKr5bb2ev813ioxtylyobgdphel foreign key (application_id) references application_entity (id) on delete cascade; +alter table if exists volume_mount_entity add constraint FKq6dppr5p20mvgjditmpypicey foreign key (volume_id) references volume_entity (id) on delete cascade; alter table if exists application_env_detail_entity add constraint FKtjqi6ag33hu1vxeg5kgc58ga6 foreign key (env_id) references application_env_entity (id) on delete cascade; alter table if exists application_env_matcher_entity add constraint FK4ivou7scuc0dg5af4g7epjdj0 foreign key (application_id) references application_entity (id) on delete cascade; alter table if exists application_env_matcher_entity add constraint FKqfkw6tgy65k4x6syrh6p858ic foreign key (env_id) references application_env_entity (id) on delete cascade;