Skip to content

Commit 4423ecd

Browse files
yonghaoyjmthibault79
authored andcommitted
From original PR #4232
1 parent 8a26286 commit 4423ecd

File tree

8 files changed

+170
-1
lines changed

8 files changed

+170
-1
lines changed

core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/appRoutesModels.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ final case class CreateAppRequest(kubernetesRuntimeConfig: Option[KubernetesRunt
3939
autodeleteEnabled: Option[Boolean]
4040
)
4141

42+
final case class UpdateAppRequest(autodeleteThreshold: Option[Int],
43+
autodeleteEnabled: Option[Boolean])
44+
4245
final case class GetAppResponse(
4346
workspaceId: Option[WorkspaceId],
4447
appName: AppName,

core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/kubernetesModels.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,8 @@ object AppStatus {
536536
val monitoredStatuses: Set[AppStatus] =
537537
Set(Deleting, Provisioning)
538538

539+
val updatableStatuses: Set[AppStatus] = Set(Running, Stopped)
540+
539541
implicit class EnrichedDiskStatus(status: AppStatus) {
540542
def isDeletable: Boolean = deletableStatuses contains status
541543

http/src/main/resources/swagger/api-docs.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,6 +1594,42 @@ paths:
15941594
application/json:
15951595
schema:
15961596
$ref: "#/components/schemas/ErrorReport"
1597+
patch:
1598+
summary: Updates the configuration of an app
1599+
description: In order to update the configuration of an app, it must first be ready
1600+
operationId: updateApp
1601+
tags:
1602+
- apps
1603+
parameters:
1604+
- in: path
1605+
name: googleProject
1606+
description: googleProject
1607+
required: true
1608+
schema:
1609+
type: string
1610+
- in: path
1611+
name: appName
1612+
description: appName
1613+
required: true
1614+
schema:
1615+
type: string
1616+
requestBody:
1617+
$ref: "#/components/requestBodies/UpdateAppRequest"
1618+
responses:
1619+
"202":
1620+
description: App update request accepted
1621+
"400":
1622+
description: Bad Request
1623+
content:
1624+
application/json:
1625+
schema:
1626+
$ref: "#/components/schemas/ErrorReport"
1627+
"500":
1628+
description: Internal Error
1629+
content:
1630+
application/json:
1631+
schema:
1632+
$ref: "#/components/schemas/ErrorReport"
15971633
delete:
15981634
summary: Deletes an existing app in the given project
15991635
description: deletes an App
@@ -2914,6 +2950,14 @@ components:
29142950
machineSize: "Standard_DS1_v2"
29152951
disk: { labels: {}, name: "disk1", size: 50 }
29162952
autopauseThreshold: 15
2953+
UpdateAppRequest:
2954+
content:
2955+
application/json:
2956+
schema:
2957+
$ref: "#/components/schemas/UpdateAppRequest"
2958+
example:
2959+
authdeleteThrehold: 15
2960+
authdeleteThreholdEnabled: true
29172961
UpdateAppsRequest:
29182962
content:
29192963
application/json:
@@ -4110,6 +4154,18 @@ components:
41104154
type: boolean
41114155
description: Whether to turn on autodelete
41124156

4157+
UpdateAppRequest:
4158+
description: a request to update an app
4159+
type: object
4160+
properties:
4161+
autodeleteThreshold:
4162+
type: integer
4163+
description: The number of minutes of idle time to elapse before the app is
4164+
deleted in minute. When autodeleteEnabled is true, a positive integer is required
4165+
autodeleteEnabled:
4166+
type: boolean
4167+
description: Whether to turn on autodelete
4168+
41134169
UpdateAppsRequest:
41144170
description: a request to update a specific set of apps (v1 or v2)
41154171
required:

http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/db/AppComponent.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,11 @@ object appQuery extends TableQuery(new AppTable(_)) {
302302
.map(_.status)
303303
.update(status)
304304

305+
def updateAutodelete(id: AppId, autodeleteEnabled: Boolean, autodeleteThreshold: Option[Int]): DBIO[Int] =
306+
getByIdQuery(id)
307+
.map(x => (x.autodeleteEnabled, x.autodeleteThreshold))
308+
.update(autodeleteEnabled, autodeleteThreshold)
309+
305310
def markAsErrored(id: AppId): DBIO[Int] =
306311
getByIdQuery(id)
307312
.map(x => (x.status, x.diskId))

http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import io.opencensus.scala.akka.http.TracingDirective.traceRequestForService
1414
import org.broadinstitute.dsde.workbench.leonardo.http.api.AppV2Routes.{
1515
createAppDecoder,
1616
getAppResponseEncoder,
17-
listAppResponseEncoder
17+
listAppResponseEncoder,
18+
updateAppRequestDecoder
1819
}
1920
import org.broadinstitute.dsde.workbench.leonardo.http.service.AppService
2021
import org.broadinstitute.dsde.workbench.model.UserInfo
@@ -71,6 +72,13 @@ class AppRoutes(kubernetesService: AppService[IO], userInfoDirectives: UserInfoD
7172
)
7273
)
7374
} ~
75+
patch {
76+
entity(as[UpdateAppRequest]) { req =>
77+
complete(
78+
updateAppHandler(userInfo, googleProject, appName, req)
79+
)
80+
}
81+
} ~
7482
delete {
7583
parameterMap { params =>
7684
complete(
@@ -167,6 +175,15 @@ class AppRoutes(kubernetesService: AppService[IO], userInfoDirectives: UserInfoD
167175
resp <- ctx.span.fold(apiCall)(span => spanResource[IO](span, "listApp").use(_ => apiCall))
168176
} yield StatusCodes.OK -> resp
169177

178+
private[api] def updateAppHandler(userInfo: UserInfo, googleProject: GoogleProject, appName: AppName, req: UpdateAppRequest)(implicit
179+
ev: Ask[IO, AppContext]
180+
): IO[ToResponseMarshallable] =
181+
for {
182+
ctx <- ev.ask[AppContext]
183+
apiCall = kubernetesService.updateApp(userInfo, CloudContext.Gcp(googleProject), appName, req)
184+
_ <- ctx.span.fold(apiCall)(span => spanResource[IO](span, "updateApp").use(_ => apiCall))
185+
} yield StatusCodes.Accepted
186+
170187
private[api] def deleteAppHandler(userInfo: UserInfo,
171188
googleProject: GoogleProject,
172189
appName: AppName,

http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2Routes.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ object AppV2Routes {
212212
)
213213
}
214214

215+
implicit val updateAppRequestDecoder: Decoder[UpdateAppRequest] =
216+
Decoder.instance { x =>
217+
for {
218+
adtm <- x.downField("autodeleteThreshold").as[Option[Int]]
219+
adte <- x.downField("autodeleteEnabled").as[Option[Boolean]]
220+
} yield UpdateAppRequest(
221+
adtm,
222+
adte
223+
)
224+
}
225+
215226
implicit val nameKeyEncoder: KeyEncoder[ServiceName] = KeyEncoder.encodeKeyString.contramap(_.value)
216227
implicit val listAppResponseEncoder: Encoder[ListAppResponse] =
217228
Encoder.forProduct16(

http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppService.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ trait AppService[F[_]] {
2929
params: Map[String, String]
3030
)(implicit as: Ask[F, AppContext]): F[Vector[ListAppResponse]]
3131

32+
def updateApp(
33+
userInfo: UserInfo,
34+
cloudContext: CloudContext.Gcp,
35+
appName: AppName,
36+
req: UpdateAppRequest,
37+
)(implicit as: Ask[F, AppContext]): F[Unit]
38+
3239
def deleteApp(userInfo: UserInfo, cloudContext: CloudContext.Gcp, appName: AppName, deleteDisk: Boolean)(implicit
3340
as: Ask[F, AppContext]
3441
): F[Unit]

http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,61 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig,
669669
.raiseError[Unit](AppNotFoundByWorkspaceIdException(workspaceId, appName, ctx.traceId, "permission denied"))
670670
} yield GetAppResponse.fromDbResult(app, Config.proxyConfig.proxyUrlBase)
671671

672+
override def updateApp(userInfo: UserInfo, cloudContext: CloudContext.Gcp, appName: AppName, req: UpdateAppRequest)(
673+
implicit as: Ask[F, AppContext]
674+
): F[Unit] =
675+
for {
676+
ctx <- as.ask
677+
// throw 403 if no project-level permission
678+
hasProjectPermission <- authProvider.isUserProjectReader(
679+
cloudContext,
680+
userInfo
681+
)
682+
_ <- F.raiseWhen(!hasProjectPermission)(ForbiddenError(userInfo.userEmail, Some(ctx.traceId)))
683+
684+
appOpt <- KubernetesServiceDbQueries
685+
.getActiveFullAppByName(cloudContext, appName)
686+
.transaction
687+
appResult <- F.fromOption(
688+
appOpt,
689+
AppNotFoundException(cloudContext, appName, ctx.traceId, "No active app found in DB")
690+
)
691+
tags = Map("appType" -> appResult.app.appType.toString)
692+
_ <- metrics.incrementCounter("updateApp", 1, tags)
693+
listOfPermissions <- authProvider.getActions(appResult.app.samResourceId, userInfo)
694+
695+
// throw 404 if no GetAppStatus permission
696+
hasPermission = listOfPermissions.toSet.contains(AppAction.UpdateApp)
697+
_ <-
698+
if (hasPermission) F.unit
699+
else
700+
F.raiseError[Unit](
701+
AppNotFoundException(cloudContext, appName, ctx.traceId, "Permission Denied")
702+
)
703+
704+
705+
canUpdate = AppStatus.updatableStatuses.contains(appResult.app.status)
706+
_ <-
707+
if (canUpdate) F.unit
708+
else
709+
F.raiseError[Unit](
710+
AppCannotBeStoppedException(cloudContext, appName, appResult.app.status, ctx.traceId)
711+
)
712+
713+
// auto delete
714+
autodeleteEnabled = req.autodeleteEnabled.getOrElse(false)
715+
autodeleteThreshold = req.autodeleteThreshold.getOrElse(0)
716+
_ <- Either.cond(!(autodeleteEnabled && autodeleteThreshold <= 0),
717+
(),
718+
BadRequestException("autodeleteThreshold should be a positive value", Some(ctx.traceId))
719+
)
720+
_ <-
721+
if (appResult.app.autodeleteEnabled != autodeleteEnabled || appResult.app.autodeleteThreshold != req.autodeleteThreshold)
722+
appQuery.updateAutodelete(appResult.app.id, autodeleteEnabled, req.autodeleteThreshold).transaction.void
723+
else Async[F].unit
724+
725+
} yield ()
726+
672727
override def createAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName, req: CreateAppRequest)(
673728
implicit as: Ask[F, AppContext]
674729
): F[Unit] =
@@ -1649,6 +1704,19 @@ case class AppAlreadyExistsException(cloudContext: CloudContext, appName: AppNam
16491704
traceId = Some(traceId)
16501705
)
16511706

1707+
case class AppCannotBeUpdatedException(cloudContext: CloudContext,
1708+
appName: AppName,
1709+
status: AppStatus,
1710+
traceId: TraceId,
1711+
extraMsg: String = ""
1712+
) extends LeoException(
1713+
s"App ${cloudContext.asStringWithProvider}/${appName.value} cannot be updated in ${status} status." +
1714+
(if (status == AppStatus.Stopped) " Please start the app first." else ""),
1715+
StatusCodes.Conflict,
1716+
traceId = Some(traceId),
1717+
extraMessageInLogging = extraMsg
1718+
)
1719+
16521720
case class AppCannotBeDeletedException(cloudContext: CloudContext,
16531721
appName: AppName,
16541722
status: AppStatus,

0 commit comments

Comments
 (0)