Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
44de243
[feat] ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ์‹œ ๋งˆ์šดํŠธ๋œ ๋ณผ๋ฅจ์„ ๋ฐ˜์˜ํ•ด์„œ ์ƒ์„ฑํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€
dolong2 Sep 12, 2025
e2e6f20
[feat] ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์ •๋ณด ์„ธ์ด๋ธŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
dolong2 Sep 12, 2025
948400d
[feat] ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์š”์ฒญ dto ์ถ”๊ฐ€
dolong2 Sep 12, 2025
8d8b60e
[feat] ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์œ ์Šค์ผ€์ด์Šค ์ถ”๊ฐ€
dolong2 Sep 12, 2025
f0aecea
[feat] ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์š”์ฒญ ๊ฐ์ฒด ์ถ”๊ฐ€
dolong2 Sep 12, 2025
cddcf30
[feat] ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
dolong2 Sep 12, 2025
9c14412
[feat] ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ์—์„œ ์˜์กด์„ฑ ์ถ”๊ฐ€
dolong2 Sep 12, 2025
9e87857
[test] ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์œ ์Šค์ผ€์ด์Šค ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
dolong2 Sep 12, 2025
5e9f6be
[test] ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
dolong2 Sep 12, 2025
07c2760
[chore] ๋ณผ๋ฅจ ๊ด€๋ จ ํ”Œ๋ž˜๊ทธ ์˜ต์…˜์„ ์ฝ”๋ฃจํ‹ด ์ปจํ…์ŠคํŠธ์—์„œ ์ƒ์„ฑํ•˜๋„๋ก ์ˆ˜์ •
dolong2 Sep 13, 2025
9663644
[chore] ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ ์œ ์Šค์ผ€์ด์Šค ํ…Œ์ŠคํŠธ ์ˆ˜์ •
dolong2 Sep 13, 2025
3b12c5d
[feat] ๋งˆ์šดํŠธ ๋ณผ๋ฅจ ์š”์ฒญ์‹œ ๋งˆ์šดํŠธ ๊ฒฝ๋กœ์— ์ •๊ทœ์‹ ์ ์šฉ
dolong2 Sep 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,34 @@ 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

@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.name}:${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 ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dcd.server.core.domain.volume.dto.request

data class MountVolumeReqDto(
val mountPath: String,
val readOnly: Boolean,
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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.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()

val volumeMount = VolumeMount(
id = UUID.randomUUID(),
application = application,
volume = volume,
mountPath = mountVolumeReqDto.mountPath,
readOnly = mountVolumeReqDto.readOnly
)
commandVolumePort.saveMount(volumeMount)

//๋งˆ์šดํŠธ ์ƒ์„ฑํ›„ ๋Œ€์ƒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์žฌ๋ฐฐํฌ
eventPublisher.publishEvent(DeployApplicationEvent(listOf(application.id)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class SecurityConfig(
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()

//when url not set
it.anyRequest().denyAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class VolumePersistenceAdapter(
volumeRepository.deleteById(volume.id)
}

override fun saveMount(volumeMount: VolumeMount) {
volumeMountRepository.save(volumeMount.toEntity())
}

override fun findById(id: UUID): Volume? =
volumeRepository.findByIdOrNull(id)
?.toDomain()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ 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.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
Expand All @@ -21,6 +23,7 @@ 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")
Expand All @@ -29,7 +32,8 @@ class VolumeWebAdapter(
private val deleteVolumeUseCase: DeleteVolumeUseCase,
private val updateVolumeUseCase: UpdateVolumeUseCase,
private val getAllVolumeUseCase: GetAllVolumeUseCase,
private val getOneVolumeUseCase: GetOneVolumeUseCase
private val getOneVolumeUseCase: GetOneVolumeUseCase,
private val mountVolumeUseCase: MountVolumeUseCase
) {
@PostMapping
@WorkspaceOwnerVerification("#workspaceId")
Expand Down Expand Up @@ -73,4 +77,15 @@ class VolumeWebAdapter(
): ResponseEntity<VolumeDetailResponse> =
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<Void> =
mountVolumeUseCase.execute(volumeId, applicationId, mountVolumeRequest.toDto())
.run { ResponseEntity.ok().build() }
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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 =
Expand All @@ -15,4 +17,10 @@ fun UpdateVolumeRequest.toDto(): UpdateVolumeReqDto =
UpdateVolumeReqDto(
name = this.name,
description = this.description
)

fun MountVolumeRequest.toDto(): MountVolumeReqDto =
MountVolumeReqDto(
mountPath = this.mountPath,
readOnly = this.readOnly
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,26 @@ 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
import util.application.ApplicationGenerator

class CreateContainerServiceImplTest : BehaviorSpec({
val commandPort = mockk<CommandPort>(relaxed = true)
val queryVolumePort = mockk<QueryVolumePort>(relaxed = true)
val checkExitValuePort = mockk<CheckExitValuePort>(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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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}'") }
}
}
Expand Down
Loading