Skip to content

Commit 88ee647

Browse files
Demo deletion (#1923)
* Demo deletion ### What's done: * Implemented demo deletion * Added frontend button that would delete demo * Added ttlSecondsAfterFinished to s3-deployment.yaml in order to clean the startup job (minio-create-bucket) (#1922)
1 parent 1b319da commit 88ee647

7 files changed

Lines changed: 216 additions & 43 deletions

File tree

save-backend/backend-api-docs.json

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,74 @@
18661866
]
18671867
}
18681868
},
1869+
"/api/v1/demo/{organizationName}/{projectName}/delete": {
1870+
"post": {
1871+
"description": "Delete demo by saveourtool organization and project names.",
1872+
"operationId": "deleteDemo",
1873+
"parameters": [
1874+
{
1875+
"description": "name of saveourtool organization",
1876+
"in": "path",
1877+
"name": "organizationName",
1878+
"required": true,
1879+
"schema": {
1880+
"type": "string"
1881+
}
1882+
},
1883+
{
1884+
"description": "name of saveourtool project",
1885+
"in": "path",
1886+
"name": "projectName",
1887+
"required": true,
1888+
"schema": {
1889+
"type": "string"
1890+
}
1891+
},
1892+
{
1893+
"example": "basic",
1894+
"in": "header",
1895+
"name": "X-Authorization-Source",
1896+
"required": true
1897+
}
1898+
],
1899+
"responses": {
1900+
"200": {
1901+
"content": {
1902+
"*/*": {
1903+
"schema": {
1904+
"type": "string"
1905+
}
1906+
}
1907+
},
1908+
"description": "Successfully deleted demo."
1909+
},
1910+
"403": {
1911+
"content": {
1912+
"*/*": {
1913+
"schema": {
1914+
"type": "string"
1915+
}
1916+
}
1917+
},
1918+
"description": "Not enough permission for demo deletion."
1919+
},
1920+
"404": {
1921+
"content": {
1922+
"*/*": {
1923+
"schema": {
1924+
"type": "string"
1925+
}
1926+
}
1927+
},
1928+
"description": "Could not find saveourtool project or demo of a project."
1929+
}
1930+
},
1931+
"summary": "Delete demo.",
1932+
"tags": [
1933+
"demo-manager-controller"
1934+
]
1935+
}
1936+
},
18691937
"/api/v1/demo/{organizationName}/{projectName}/delete-file": {
18701938
"delete": {
18711939
"description": "Delete a file.",

save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/DemoManagerController.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,44 @@ class DemoManagerController(
333333
)
334334
)
335335
}
336+
337+
@PostMapping("/{organizationName}/{projectName}/delete")
338+
@RequiresAuthorizationSourceHeader
339+
@PreAuthorize("hasRole('ROLE_SUPER_ADMIN')")
340+
@Parameters(
341+
Parameter(name = "organizationName", `in` = ParameterIn.PATH, description = "name of saveourtool organization", required = true),
342+
Parameter(name = "projectName", `in` = ParameterIn.PATH, description = "name of saveourtool project", required = true),
343+
)
344+
@Operation(
345+
method = "POST",
346+
summary = "Delete demo.",
347+
description = "Delete demo by saveourtool organization and project names.",
348+
)
349+
@ApiResponse(responseCode = "200", description = "Successfully deleted demo.")
350+
@ApiResponse(responseCode = "403", description = "Not enough permission for demo deletion.")
351+
@ApiResponse(responseCode = "404", description = "Could not find saveourtool project or demo of a project.")
352+
fun deleteDemo(
353+
@PathVariable organizationName: String,
354+
@PathVariable projectName: String,
355+
authentication: Authentication,
356+
): Mono<StringResponse> = projectService.projectByCoordinatesOrNotFound(projectName, organizationName) {
357+
"Could not find $organizationName/$projectName."
358+
}
359+
.requireOrSwitchToResponseException({ projectPermissionEvaluator.hasPermission(authentication, this, Permission.DELETE) }, HttpStatus.FORBIDDEN) {
360+
"Not enough permission for accessing $organizationName/$projectName."
361+
}
362+
.flatMap { project ->
363+
webClientDemo.post()
364+
.uri("/demo/internal/manager/${project.toProjectCoordinates()}/delete")
365+
.retrieve()
366+
.onStatus({ it == HttpStatus.NOT_FOUND }) {
367+
Mono.error(
368+
ResponseStatusException(
369+
HttpStatus.NOT_FOUND,
370+
"Could not find demo for $organizationName/$projectName.",
371+
)
372+
)
373+
}
374+
.toEntity()
375+
}
336376
}

save-cloud-charts/save-cloud/templates/s3-deployment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ metadata:
7878
name: minio-create-bucket
7979
spec:
8080
completions: 1
81+
ttlSecondsAfterFinished: 10
8182
template:
8283
metadata:
8384
name: minio-create-bucket

save-demo/src/main/kotlin/com/saveourtool/save/demo/controller/ManagementController.kt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.saveourtool.save.demo.DemoDto
44
import com.saveourtool.save.demo.entity.*
55
import com.saveourtool.save.demo.service.*
66
import com.saveourtool.save.utils.*
7-
import org.springframework.context.annotation.Profile
87

98
import org.springframework.http.HttpStatus
109
import org.springframework.web.bind.annotation.*
@@ -37,12 +36,27 @@ class ManagementController(
3736
}
3837

3938
/**
40-
* @param organizationName
41-
* @param projectName
42-
* @return [Mono] of [Unit]
39+
* @param organizationName saveourtool organization name
40+
* @param projectName saveourtool project name
41+
* @param version version of demo
42+
* @return [Mono] of [StringResponse]
43+
*/
44+
@PostMapping("/{organizationName}/{projectName}/delete")
45+
fun delete(
46+
@PathVariable organizationName: String,
47+
@PathVariable projectName: String,
48+
@RequestParam(required = false, defaultValue = "manual") version: String,
49+
): Mono<StringResponse> = demoService.findBySaveourtoolProjectOrNotFound(organizationName, projectName) {
50+
"Could not find demo for $organizationName/$projectName."
51+
}
52+
.flatMap { demoService.delete(it, version) }
53+
54+
/**
55+
* @param organizationName saveourtool organization name
56+
* @param projectName saveourtool project name
57+
* @return [Mono] of [StringResponse]
4358
*/
4459
@PostMapping("/{organizationName}/{projectName}/start")
45-
@Profile("kubernetes")
4660
fun start(
4761
@PathVariable organizationName: String,
4862
@PathVariable projectName: String,

save-demo/src/main/kotlin/com/saveourtool/save/demo/service/DemoService.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import com.saveourtool.save.demo.DemoStatus
44
import com.saveourtool.save.demo.entity.Demo
55
import com.saveourtool.save.demo.repository.DemoRepository
66
import com.saveourtool.save.demo.runners.RunnerFactory
7+
import com.saveourtool.save.demo.storage.DependencyStorage
78
import com.saveourtool.save.utils.StringResponse
89
import com.saveourtool.save.utils.blockingToMono
910
import com.saveourtool.save.utils.switchIfEmptyToNotFound
1011

1112
import org.springframework.stereotype.Service
1213
import org.springframework.transaction.annotation.Transactional
1314
import reactor.core.publisher.Mono
15+
import reactor.core.scheduler.Schedulers
1416
import reactor.kotlin.core.publisher.toMono
1517

1618
import kotlinx.coroutines.reactor.mono
@@ -22,6 +24,7 @@ import kotlinx.coroutines.reactor.mono
2224
class DemoService(
2325
private val demoRepository: DemoRepository,
2426
private val kubernetesService: KubernetesService?,
27+
private val dependencyStorage: DependencyStorage,
2528
) {
2629
/**
2730
* Get preferred [RunnerFactory.RunnerType] for demo runner.
@@ -104,4 +107,17 @@ class DemoService(
104107
): Mono<Demo> = blockingToMono {
105108
findBySaveourtoolProject(organizationName, projectName)
106109
}.switchIfEmptyToNotFound(lazyMessage)
110+
111+
/**
112+
* @param demo [Demo] entity
113+
* @param version version of demo
114+
* @return [Mono] of [Unit]
115+
*/
116+
fun delete(demo: Demo, version: String): Mono<StringResponse> = stop(demo)
117+
.let { dependencyStorage.list(demo, version) }
118+
.concatMap { dependencyStorage.delete(it) }
119+
.collectList()
120+
.publishOn(Schedulers.boundedElastic())
121+
.map { demoRepository.delete(demo) }
122+
.map { StringResponse.ok("Successfully deleted demo of ${demo.projectCoordinates()}.") }
107123
}

save-demo/src/main/kotlin/com/saveourtool/save/demo/service/KubernetesService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ class KubernetesService(
6767
)
6868
}
6969
}
70-
.flatMap {
70+
.asyncEffect {
7171
mono { configureDemoAgent(it, version) }
7272
}
73+
.map { StringResponse.ok("Created container for demo.") }
7374

7475
/**
7576
* @param demo demo entity

save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/projects/ProjectDemoMenu.kt

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -39,31 +39,45 @@ val projectDemoMenu: FC<ProjectDemoMenuProps> = FC { props ->
3939
val (demoDto, setDemoDto) = useState(
4040
DemoDto.emptyForProject(props.organizationName, props.projectName)
4141
)
42+
4243
val (demoStatus, setDemoStatus) = useState(DemoStatus.NOT_CREATED)
4344
val (githubProjectCoordinates, setGithubProjectCoordinates) = useState(ProjectCoordinates.empty)
4445
val (selectedFileDtos, setSelectedFileDtos) = useState(emptyList<FileDto>())
45-
val sendDemoCreationRequest = useDeferredRequest {
46-
if (githubProjectCoordinates.consideredBlank()) {
47-
demoDto.copy(githubProjectCoordinates = null)
46+
47+
val getDemoInfo = useDeferredRequest {
48+
val infoResponse = get(
49+
"$apiUrl/demo/${props.organizationName}/${props.projectName}",
50+
jsonHeaders,
51+
::loadingHandler,
52+
::noopResponseHandler,
53+
)
54+
if (infoResponse.ok) {
55+
val demoInfo: DemoInfo = infoResponse.decodeFromJsonString()
56+
setDemoStatus(demoInfo.demoStatus)
57+
setDemoDto(demoInfo.demoDto)
58+
setGithubProjectCoordinates(demoInfo.demoDto.githubProjectCoordinates.orEmpty())
59+
} else if (infoResponse.status != 404.toShort()) {
60+
props.updateErrorMessage(infoResponse.statusText, infoResponse.unpackMessage())
61+
setDemoStatus(DemoStatus.ERROR)
4862
} else {
4963
demoDto.copy(githubProjectCoordinates = githubProjectCoordinates)
5064
}
51-
.let { demoRequest ->
52-
post(
53-
"$apiUrl/demo/${props.organizationName}/${props.projectName}/add",
54-
jsonHeaders,
55-
Json.encodeToString(
56-
DemoCreationRequest(demoRequest, selectedFileDtos)
57-
),
58-
::loadingHandler,
59-
::noopResponseHandler,
60-
)
61-
.let {
62-
if (!it.ok) {
63-
props.updateErrorMessage(it.statusText, it.unpackMessage())
64-
}
65-
}
65+
}
66+
67+
val sendDeleteRequest = useDeferredRequest {
68+
post(
69+
"$apiUrl/demo/${props.organizationName}/${props.projectName}/delete",
70+
jsonHeaders,
71+
Unit,
72+
loadingHandler = ::loadingHandler,
73+
).let {
74+
if (it.ok) {
75+
window.alert(it.body as String)
76+
setDemoStatus(DemoStatus.NOT_CREATED)
77+
setDemoDto(DemoDto.emptyForProject(props.organizationName, props.projectName))
78+
setGithubProjectCoordinates(ProjectCoordinates.empty)
6679
}
80+
}
6781
}
6882

6983
val getDemoStatus = useDeferredRequest {
@@ -81,26 +95,35 @@ val projectDemoMenu: FC<ProjectDemoMenuProps> = FC { props ->
8195
}
8296
}
8397

84-
useRequest {
85-
val infoResponse = get(
86-
"$apiUrl/demo/${props.organizationName}/${props.projectName}",
87-
jsonHeaders,
88-
::loadingHandler,
89-
::noopResponseHandler,
90-
)
91-
if (infoResponse.ok) {
92-
val demoInfo: DemoInfo = infoResponse.decodeFromJsonString()
93-
setDemoStatus(demoInfo.demoStatus)
94-
setDemoDto(demoInfo.demoDto)
95-
setGithubProjectCoordinates(demoInfo.demoDto.githubProjectCoordinates.orEmpty())
96-
} else if (infoResponse.status != 404.toShort()) {
97-
props.updateErrorMessage(infoResponse.statusText, infoResponse.unpackMessage())
98-
setDemoStatus(DemoStatus.ERROR)
98+
val sendDemoCreationRequest = useDeferredRequest {
99+
if (githubProjectCoordinates.consideredBlank()) {
100+
demoDto.copy(githubProjectCoordinates = null)
101+
} else {
102+
demoDto.copy(githubProjectCoordinates = githubProjectCoordinates)
99103
}
104+
.let { demoRequest ->
105+
post(
106+
"$apiUrl/demo/${props.organizationName}/${props.projectName}/add",
107+
jsonHeaders,
108+
Json.encodeToString(
109+
DemoCreationRequest(demoRequest, selectedFileDtos)
110+
),
111+
::loadingHandler,
112+
::noopResponseHandler,
113+
)
114+
.let {
115+
if (it.ok) {
116+
window.alert(it.body as String)
117+
} else {
118+
props.updateErrorMessage(it.statusText, it.unpackMessage())
119+
}
120+
}
121+
}
100122
}
101123

102124
useOnce {
103125
window.alert("This is just a preview, nothing on this view works for now.")
126+
getDemoInfo()
104127
}
105128

106129
div {
@@ -119,7 +142,11 @@ val projectDemoMenu: FC<ProjectDemoMenuProps> = FC { props ->
119142
hr { }
120143
renderSdkSelector(demoDto, setDemoDto, demoStatus != DemoStatus.STOPPED && demoStatus != DemoStatus.NOT_CREATED)
121144
hr { }
122-
renderButtons(demoStatus, props.userProjectRole, sendDemoCreationRequest, getDemoStatus)
145+
renderButtons(demoStatus, props.userProjectRole, sendDemoCreationRequest, getDemoStatus) {
146+
if (window.confirm("Delete demo of ${props.organizationName}/${props.projectName}?")) {
147+
sendDeleteRequest()
148+
}
149+
}
123150
}
124151
}
125152
}
@@ -355,6 +382,7 @@ private fun ChildrenBuilder.renderButtons(
355382
userRole: Role,
356383
sendDemoCreationRequest: () -> Unit,
357384
getDemoStatus: () -> Unit,
385+
deleteDemo: () -> Unit,
358386
) {
359387
div {
360388
className = ClassName("flex-wrap d-flex justify-content-around")
@@ -373,8 +401,13 @@ private fun ChildrenBuilder.renderButtons(
373401
}
374402
}
375403

376-
DemoStatus.RUNNING -> buttonBuilder("Stop", style = "danger", isDisabled = userRole.isLowerThan(Role.ADMIN)) {
377-
// stop request here
404+
DemoStatus.RUNNING -> {
405+
buttonBuilder("Delete", style = "danger", isDisabled = userRole.isLowerThan(Role.OWNER)) {
406+
deleteDemo()
407+
}
408+
buttonBuilder("Stop", style = "danger", isDisabled = userRole.isLowerThan(Role.ADMIN)) {
409+
// stop request here
410+
}
378411
}
379412

380413
DemoStatus.ERROR, DemoStatus.STOPPED -> {
@@ -386,7 +419,7 @@ private fun ChildrenBuilder.renderButtons(
386419
// update request here
387420
}
388421
buttonBuilder("Delete", style = "danger", isDisabled = userRole.isLowerThan(Role.OWNER)) {
389-
// delete request here
422+
deleteDemo()
390423
}
391424
}
392425
}

0 commit comments

Comments
 (0)