Skip to content

Commit

Permalink
From original PR #4232
Browse files Browse the repository at this point in the history
  • Loading branch information
yonghaoy authored and jmthibault79 committed May 20, 2024
1 parent e77db82 commit 6e36257
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ final case class CreateAppRequest(kubernetesRuntimeConfig: Option[KubernetesRunt
autodeleteEnabled: Option[Boolean]
)

final case class UpdateAppRequest(autodeleteThreshold: Option[Int],
autodeleteEnabled: Option[Boolean])

final case class GetAppResponse(
workspaceId: Option[WorkspaceId],
appName: AppName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ object AppStatus {
val monitoredStatuses: Set[AppStatus] =
Set(Deleting, Provisioning)

val updatableStatuses: Set[AppStatus] = Set(Running, Stopped)

implicit class EnrichedDiskStatus(status: AppStatus) {
def isDeletable: Boolean = deletableStatuses contains status

Expand Down
56 changes: 56 additions & 0 deletions http/src/main/resources/swagger/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,42 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorReport"
patch:
summary: Updates the configuration of an app
description: In order to update the configuration of an app, it must first be ready
operationId: updateApp
tags:
- apps
parameters:
- in: path
name: googleProject
description: googleProject
required: true
schema:
type: string
- in: path
name: appName
description: appName
required: true
schema:
type: string
requestBody:
$ref: "#/components/requestBodies/UpdateAppRequest"
responses:
"202":
description: App update request accepted
"400":
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorReport"
"500":
description: Internal Error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorReport"
delete:
summary: Deletes an existing app in the given project
description: deletes an App
Expand Down Expand Up @@ -2914,6 +2950,14 @@ components:
machineSize: "Standard_DS1_v2"
disk: { labels: {}, name: "disk1", size: 50 }
autopauseThreshold: 15
UpdateAppRequest:
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateAppRequest"
example:
authdeleteThrehold: 15
authdeleteThreholdEnabled: true
UpdateAppsRequest:
content:
application/json:
Expand Down Expand Up @@ -4110,6 +4154,18 @@ components:
type: boolean
description: Whether to turn on autodelete

UpdateAppRequest:
description: a request to update an app
type: object
properties:
autodeleteThreshold:
type: integer
description: The number of minutes of idle time to elapse before the app is
deleted in minute. When autodeleteEnabled is true, a positive integer is required
autodeleteEnabled:
type: boolean
description: Whether to turn on autodelete

UpdateAppsRequest:
description: a request to update a specific set of apps (v1 or v2)
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ object appQuery extends TableQuery(new AppTable(_)) {
.map(_.status)
.update(status)

def updateAutodelete(id: AppId, autodeleteEnabled: Boolean, autodeleteThreshold: Option[Int]): DBIO[Int] =
getByIdQuery(id)
.map(x => (x.autodeleteEnabled, x.autodeleteThreshold))
.update(autodeleteEnabled, autodeleteThreshold)

def markAsErrored(id: AppId): DBIO[Int] =
getByIdQuery(id)
.map(x => (x.status, x.diskId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import io.opencensus.scala.akka.http.TracingDirective.traceRequestForService
import org.broadinstitute.dsde.workbench.leonardo.http.api.AppV2Routes.{
createAppDecoder,
getAppResponseEncoder,
listAppResponseEncoder
listAppResponseEncoder,
updateAppRequestDecoder
}
import org.broadinstitute.dsde.workbench.leonardo.http.service.AppService
import org.broadinstitute.dsde.workbench.model.UserInfo
Expand Down Expand Up @@ -71,6 +72,13 @@ class AppRoutes(kubernetesService: AppService[IO], userInfoDirectives: UserInfoD
)
)
} ~
patch {
entity(as[UpdateAppRequest]) { req =>
complete(
updateAppHandler(userInfo, googleProject, appName, req)
)
}
} ~
delete {
parameterMap { params =>
complete(
Expand Down Expand Up @@ -167,6 +175,15 @@ class AppRoutes(kubernetesService: AppService[IO], userInfoDirectives: UserInfoD
resp <- ctx.span.fold(apiCall)(span => spanResource[IO](span, "listApp").use(_ => apiCall))
} yield StatusCodes.OK -> resp

private[api] def updateAppHandler(userInfo: UserInfo, googleProject: GoogleProject, appName: AppName, req: UpdateAppRequest)(implicit
ev: Ask[IO, AppContext]
): IO[ToResponseMarshallable] =
for {
ctx <- ev.ask[AppContext]
apiCall = kubernetesService.updateApp(userInfo, CloudContext.Gcp(googleProject), appName, req)
_ <- ctx.span.fold(apiCall)(span => spanResource[IO](span, "updateApp").use(_ => apiCall))
} yield StatusCodes.Accepted

private[api] def deleteAppHandler(userInfo: UserInfo,
googleProject: GoogleProject,
appName: AppName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,17 @@ object AppV2Routes {
)
}

implicit val updateAppRequestDecoder: Decoder[UpdateAppRequest] =
Decoder.instance { x =>
for {
adtm <- x.downField("autodeleteThreshold").as[Option[Int]]
adte <- x.downField("autodeleteEnabled").as[Option[Boolean]]
} yield UpdateAppRequest(
adtm,
adte
)
}

implicit val nameKeyEncoder: KeyEncoder[ServiceName] = KeyEncoder.encodeKeyString.contramap(_.value)
implicit val listAppResponseEncoder: Encoder[ListAppResponse] =
Encoder.forProduct16(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ trait AppService[F[_]] {
params: Map[String, String]
)(implicit as: Ask[F, AppContext]): F[Vector[ListAppResponse]]

def updateApp(
userInfo: UserInfo,
cloudContext: CloudContext.Gcp,
appName: AppName,
req: UpdateAppRequest,
)(implicit as: Ask[F, AppContext]): F[Unit]

def deleteApp(userInfo: UserInfo, cloudContext: CloudContext.Gcp, appName: AppName, deleteDisk: Boolean)(implicit
as: Ask[F, AppContext]
): F[Unit]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,61 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig,
.raiseError[Unit](AppNotFoundByWorkspaceIdException(workspaceId, appName, ctx.traceId, "permission denied"))
} yield GetAppResponse.fromDbResult(app, Config.proxyConfig.proxyUrlBase)

override def updateApp(userInfo: UserInfo, cloudContext: CloudContext.Gcp, appName: AppName, req: UpdateAppRequest)(
implicit as: Ask[F, AppContext]
): F[Unit] =
for {
ctx <- as.ask
// throw 403 if no project-level permission
hasProjectPermission <- authProvider.isUserProjectReader(
cloudContext,
userInfo
)
_ <- F.raiseWhen(!hasProjectPermission)(ForbiddenError(userInfo.userEmail, Some(ctx.traceId)))

appOpt <- KubernetesServiceDbQueries
.getActiveFullAppByName(cloudContext, appName)
.transaction
appResult <- F.fromOption(
appOpt,
AppNotFoundException(cloudContext, appName, ctx.traceId, "No active app found in DB")
)
tags = Map("appType" -> appResult.app.appType.toString)
_ <- metrics.incrementCounter("updateApp", 1, tags)
listOfPermissions <- authProvider.getActions(appResult.app.samResourceId, userInfo)

// throw 404 if no GetAppStatus permission
hasPermission = listOfPermissions.toSet.contains(AppAction.UpdateApp)
_ <-
if (hasPermission) F.unit
else
F.raiseError[Unit](
AppNotFoundException(cloudContext, appName, ctx.traceId, "Permission Denied")
)


canUpdate = AppStatus.updatableStatuses.contains(appResult.app.status)
_ <-
if (canUpdate) F.unit
else
F.raiseError[Unit](
AppCannotBeStoppedException(cloudContext, appName, appResult.app.status, ctx.traceId)
)

// auto delete
autodeleteEnabled = req.autodeleteEnabled.getOrElse(false)
autodeleteThreshold = req.autodeleteThreshold.getOrElse(0)
_ <- Either.cond(!(autodeleteEnabled && autodeleteThreshold <= 0),
(),
BadRequestException("autodeleteThreshold should be a positive value", Some(ctx.traceId))
)
_ <-
if (appResult.app.autodeleteEnabled != autodeleteEnabled || appResult.app.autodeleteThreshold != req.autodeleteThreshold)
appQuery.updateAutodelete(appResult.app.id, autodeleteEnabled, req.autodeleteThreshold).transaction.void
else Async[F].unit

} yield ()

override def createAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName, req: CreateAppRequest)(
implicit as: Ask[F, AppContext]
): F[Unit] =
Expand Down Expand Up @@ -1649,6 +1704,19 @@ case class AppAlreadyExistsException(cloudContext: CloudContext, appName: AppNam
traceId = Some(traceId)
)

case class AppCannotBeUpdatedException(cloudContext: CloudContext,
appName: AppName,
status: AppStatus,
traceId: TraceId,
extraMsg: String = ""
) extends LeoException(
s"App ${cloudContext.asStringWithProvider}/${appName.value} cannot be updated in ${status} status." +
(if (status == AppStatus.Stopped) " Please start the app first." else ""),
StatusCodes.Conflict,
traceId = Some(traceId),
extraMessageInLogging = extraMsg
)

case class AppCannotBeDeletedException(cloudContext: CloudContext,
appName: AppName,
status: AppStatus,
Expand Down

0 comments on commit 6e36257

Please sign in to comment.