From aac1f48a92697ad241037c5f9e6a702c4e83b07d Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Fri, 27 Jun 2025 09:59:12 -0400 Subject: [PATCH 01/43] Initial checkin of incomplete azure deletion changeset --- .../dsde/workbench/leonardo/liquibase/changelog.xml | 1 + .../changesets/20250625_delete_azure_records.xml | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml index 1702806537..dbfd57eecc 100644 --- a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml @@ -118,4 +118,5 @@ + diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml new file mode 100644 index 0000000000..17a67bb35f --- /dev/null +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml @@ -0,0 +1,11 @@ + + + + + DELETE FROM CLUSTER where cloudProvider = 'AZURE'; + + DELETE FROM PERSISTENT_DISK where cloudProvider = 'AZURE'; + + + + From 684322cda387690ba8f0172509ea823c5a2f8f6b Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 30 Jun 2025 16:38:57 -0400 Subject: [PATCH 02/43] Liquibase changeset --- .../20250625_delete_azure_records.xml | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml index 17a67bb35f..a6c5b9af2d 100644 --- a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml @@ -1,10 +1,80 @@ - + - DELETE FROM CLUSTER where cloudProvider = 'AZURE'; - DELETE FROM PERSISTENT_DISK where cloudProvider = 'AZURE'; + + + DELETE UPDATE_APP_LOG FROM UPDATE_APP_LOG + JOIN APP ON APP.id = UPDATE_APP_LOG.appId + JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id + JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id + WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; + + DELETE APP_ERROR FROM APP_ERROR + JOIN APP ON APP.id = APP_ERROR.appId + JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id + JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id + WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; + + DELETE SERVICE FROM SERVICE + JOIN APP ON APP.id = SERVICE.appId + JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id + JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id + WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; + + DELETE LABEL FROM LABEL + JOIN APP ON LABEL.resourceType='APP' and LABEL.resourceId = APP.id + JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id + JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id + WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; + + DELETE APP_CONTROLLED_RESOURCE FROM APP_CONTROLLED_RESOURCE + JOIN APP ON APP.id = APP_CONTROLLED_RESOURCE.appId + JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id + JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id + WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; + + DELETE APP FROM APP + JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id + JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id + WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; + + DELETE NODEPOOL FROM NODEPOOL + JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id + WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; + + DELETE FROM KUBERNETES_CLUSTER where cloudProvider = 'AZURE'; + + + + DELETE RUNTIME_CONTROLLED_RESOURCE FROM RUNTIME_CONTROLLED_RESOURCE + JOIN CLUSTER ON CLUSTER.id = RUNTIME_CONTROLLED_RESOURCE.runtimeId + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE LABEL FROM LABEL + JOIN CLUSTER ON LABEL.resourceType='runtime' and LABEL.resourceId = CLUSTER.id + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE CLUSTER_IMAGE FROM CLUSTER_IMAGE + JOIN CLUSTER ON CLUSTER_IMAGE.clusterId = CLUSTER.id + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE CLUSTER_ERROR FROM CLUSTER_ERROR + JOIN CLUSTER ON CLUSTER_ERROR.clusterId = CLUSTER.id + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE FROM CLUSTER WHERE cloudProvider = 'AZURE'; + + DELETE FROM RUNTIME_CONFIG WHERE cloudService = 'AZURE_VM'; + + + + DELETE LABEL FROM LABEL + JOIN PERSISTENT_DISK ON LABEL.resourceType='persistentDisk' and LABEL.resourceId = PERSISTENT_DISK.id + WHERE PERSISTENT_DISK.cloudProvider = 'AZURE'; + + DELETE FROM PERSISTENT_DISK WHERE cloudProvider = 'AZURE'; From 568af507881220114c39c55e795c2eff75934186 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 1 Jul 2025 10:55:04 -0400 Subject: [PATCH 03/43] Cosmetic change for case consistency --- .../liquibase/changesets/20250625_delete_azure_records.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml index a6c5b9af2d..d20c4c120b 100644 --- a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml @@ -24,7 +24,7 @@ WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; DELETE LABEL FROM LABEL - JOIN APP ON LABEL.resourceType='APP' and LABEL.resourceId = APP.id + JOIN APP ON LABEL.resourceType='app' and LABEL.resourceId = APP.id JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id JOIN KUBERNETES_CLUSTER ON NODEPOOL.clusterId = KUBERNETES_CLUSTER.id WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; From 521fb144adeca9bce5823d87b47c42f689ee6839 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Wed, 2 Jul 2025 10:09:40 -0400 Subject: [PATCH 04/43] Split changeset into one per high-level resource --- .../leonardo/liquibase/changelog.xml | 4 ++- ... => 20250702_delete_azure_records_app.xml} | 36 +------------------ ...20250702_delete_azure_records_cloudenv.xml | 26 ++++++++++++++ .../20250702_delete_azure_records_disk.xml | 12 +++++++ 4 files changed, 42 insertions(+), 36 deletions(-) rename http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/{20250625_delete_azure_records.xml => 20250702_delete_azure_records_app.xml} (68%) create mode 100644 http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_cloudenv.xml create mode 100644 http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_disk.xml diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml index dbfd57eecc..ab3f954504 100644 --- a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changelog.xml @@ -118,5 +118,7 @@ - + + + diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_app.xml similarity index 68% rename from http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml rename to http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_app.xml index d20c4c120b..fe1cff1690 100644 --- a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250625_delete_azure_records.xml +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_app.xml @@ -1,10 +1,7 @@ - + - - - DELETE UPDATE_APP_LOG FROM UPDATE_APP_LOG JOIN APP ON APP.id = UPDATE_APP_LOG.appId JOIN NODEPOOL ON APP.nodepoolId = NODEPOOL.id @@ -45,37 +42,6 @@ WHERE KUBERNETES_CLUSTER.cloudProvider = 'AZURE'; DELETE FROM KUBERNETES_CLUSTER where cloudProvider = 'AZURE'; - - - - DELETE RUNTIME_CONTROLLED_RESOURCE FROM RUNTIME_CONTROLLED_RESOURCE - JOIN CLUSTER ON CLUSTER.id = RUNTIME_CONTROLLED_RESOURCE.runtimeId - WHERE CLUSTER.cloudProvider = 'AZURE'; - - DELETE LABEL FROM LABEL - JOIN CLUSTER ON LABEL.resourceType='runtime' and LABEL.resourceId = CLUSTER.id - WHERE CLUSTER.cloudProvider = 'AZURE'; - - DELETE CLUSTER_IMAGE FROM CLUSTER_IMAGE - JOIN CLUSTER ON CLUSTER_IMAGE.clusterId = CLUSTER.id - WHERE CLUSTER.cloudProvider = 'AZURE'; - - DELETE CLUSTER_ERROR FROM CLUSTER_ERROR - JOIN CLUSTER ON CLUSTER_ERROR.clusterId = CLUSTER.id - WHERE CLUSTER.cloudProvider = 'AZURE'; - - DELETE FROM CLUSTER WHERE cloudProvider = 'AZURE'; - - DELETE FROM RUNTIME_CONFIG WHERE cloudService = 'AZURE_VM'; - - - - DELETE LABEL FROM LABEL - JOIN PERSISTENT_DISK ON LABEL.resourceType='persistentDisk' and LABEL.resourceId = PERSISTENT_DISK.id - WHERE PERSISTENT_DISK.cloudProvider = 'AZURE'; - - DELETE FROM PERSISTENT_DISK WHERE cloudProvider = 'AZURE'; - diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_cloudenv.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_cloudenv.xml new file mode 100644 index 0000000000..8f8a2890ef --- /dev/null +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_cloudenv.xml @@ -0,0 +1,26 @@ + + + + + DELETE RUNTIME_CONTROLLED_RESOURCE FROM RUNTIME_CONTROLLED_RESOURCE + JOIN CLUSTER ON CLUSTER.id = RUNTIME_CONTROLLED_RESOURCE.runtimeId + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE LABEL FROM LABEL + JOIN CLUSTER ON LABEL.resourceType='runtime' and LABEL.resourceId = CLUSTER.id + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE CLUSTER_IMAGE FROM CLUSTER_IMAGE + JOIN CLUSTER ON CLUSTER_IMAGE.clusterId = CLUSTER.id + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE CLUSTER_ERROR FROM CLUSTER_ERROR + JOIN CLUSTER ON CLUSTER_ERROR.clusterId = CLUSTER.id + WHERE CLUSTER.cloudProvider = 'AZURE'; + + DELETE FROM CLUSTER WHERE cloudProvider = 'AZURE'; + + DELETE FROM RUNTIME_CONFIG WHERE cloudService = 'AZURE_VM'; + + + diff --git a/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_disk.xml b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_disk.xml new file mode 100644 index 0000000000..977a5a305c --- /dev/null +++ b/http/src/main/resources/org/broadinstitute/dsde/workbench/leonardo/liquibase/changesets/20250702_delete_azure_records_disk.xml @@ -0,0 +1,12 @@ + + + + + DELETE LABEL FROM LABEL + JOIN PERSISTENT_DISK ON LABEL.resourceType='persistentDisk' and LABEL.resourceId = PERSISTENT_DISK.id + WHERE PERSISTENT_DISK.cloudProvider = 'AZURE'; + + DELETE FROM PERSISTENT_DISK WHERE cloudProvider = 'AZURE'; + + + From d42ece38f5ae30ea5c936b474df6a6028ade4989 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 3 Jul 2025 10:24:09 -0400 Subject: [PATCH 05/43] Remove app v2 endpoints and app v2 service methods --- .../leonardo/http/api/AppRoutes.scala | 132 ++- .../leonardo/http/api/AppV2Routes.scala | 281 ----- .../leonardo/http/api/HttpRoutes.scala | 5 +- .../leonardo/http/service/AppService.scala | 30 - .../http/service/LeoAppServiceInterp.scala | 373 +----- .../leonardo/http/api/AppV2RouteSpec.scala | 48 - .../http/service/AppServiceInterpSpec.scala | 1055 +---------------- .../http/service/MockAppService.scala | 22 - 8 files changed, 132 insertions(+), 1814 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2Routes.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2RouteSpec.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala index 27795d75b0..c7d939a640 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala @@ -7,21 +7,19 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server import akka.http.scaladsl.server.Directives._ import cats.effect.IO +import cats.implicits.catsSyntaxEitherId import cats.mtl.Ask import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ -import io.circe.Decoder +import io.circe.{Decoder, DecodingFailure, Encoder, KeyEncoder} import io.opencensus.scala.akka.http.TracingDirective.traceRequestForService -import org.broadinstitute.dsde.workbench.leonardo.JsonCodec.autodeleteThresholdDecoder -import org.broadinstitute.dsde.workbench.leonardo.http.api.AppRoutes.updateAppRequestDecoder -import org.broadinstitute.dsde.workbench.leonardo.http.api.AppV2Routes.{ - createAppDecoder, - getAppResponseEncoder, - listAppResponseEncoder -} +import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName +import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ +import org.broadinstitute.dsde.workbench.leonardo.http.api.AppRoutes._ import org.broadinstitute.dsde.workbench.leonardo.http.service.AppService import org.broadinstitute.dsde.workbench.model.UserInfo -import org.broadinstitute.dsde.workbench.model.google.GoogleProject +import org.broadinstitute.dsde.workbench.model.google.{GcsBucketName, GoogleProject} import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics +import org.http4s.Uri class AppRoutes(kubernetesService: AppService[IO], userInfoDirectives: UserInfoDirectives)(implicit metrics: OpenTelemetryMetrics[IO] @@ -234,4 +232,120 @@ object AppRoutes { threshold <- x.downField("autodeleteThreshold").as[Option[AutodeleteThreshold]] } yield UpdateAppRequest(enabled, threshold) } + + implicit val createAppDecoder: Decoder[CreateAppRequest] = + Decoder.instance { x => + for { + c <- x.downField("kubernetesRuntimeConfig").as[Option[KubernetesRuntimeConfig]] + s <- x.downField("accessScope").as[Option[AppAccessScope]] + d <- x.downField("diskConfig").as[Option[PersistentDiskRequest]] + l <- x.downField("labels").as[Option[LabelMap]] + cv <- x.downField("customEnvironmentVariables").as[Option[LabelMap]] + dp <- x.downField("descriptorPath").as[Option[Uri]] + ea <- x.downField("extraArgs").as[Option[List[String]]] + wsi <- x.downField("workspaceId").as[Option[WorkspaceId]] + swi <- x.downField("sourceWorkspaceId").as[Option[WorkspaceId]] + adte <- x.downField("autodeleteEnabled").as[Option[Boolean]] + adtm <- x.downField("autodeleteThreshold").as[Option[AutodeleteThreshold]] + autopilot <- x.downField("autopilot").as[Option[Autopilot]] + bucketNameToMount <- x.downField("bucketNameToMount").as[Option[GcsBucketName]] + + optStr <- x.downField("appType").as[Option[String]] + cn <- x.downField("allowedChartName").as[Option[AllowedChartName]] + // TODO: once AOU has migrated to use the new app type, we can use much simpler version instead of this workaround for backwards compatibility + (appType, allowedChartName) <- optStr match { + case Some(value) => + AppType.stringToObject + .get(value) match { + case Some(v) => (v, cn).asRight[DecodingFailure] + case None => + if (value == "RSTUDIO") + (AppType.Allowed, Some(AllowedChartName.RStudio)).asRight[DecodingFailure] + else + DecodingFailure(s"Invalid app type ${value}", List.empty).asLeft[(AppType, Option[AllowedChartName])] + } + case None => (AppType.Galaxy, cn).asRight[DecodingFailure] + } + } yield CreateAppRequest( + c, + appType, + allowedChartName, + s, + d, + l.getOrElse(Map.empty), + cv.getOrElse(Map.empty), + dp, + ea.getOrElse(List.empty), + wsi, + swi, + adte, + adtm, + autopilot, + bucketNameToMount + ) + } + + implicit val nameKeyEncoder: KeyEncoder[ServiceName] = KeyEncoder.encodeKeyString.contramap(_.value) + + implicit val listAppResponseEncoder: Encoder[ListAppResponse] = + Encoder.forProduct17( + "workspaceId", + "cloudContext", + "region", + "kubernetesRuntimeConfig", + "autopilot", + "errors", + "status", + "proxyUrls", + "appName", + "appType", + "chartName", + "diskName", + "auditInfo", + "accessScope", + "labels", + "autodeleteEnabled", + "autodeleteThreshold" + )(x => + (x.workspaceId, + x.cloudContext, + x.region, + x.kubernetesRuntimeConfig, + x.autopilot, + x.errors, + x.status, + x.proxyUrls, + x.appName, + x.appType, + x.chartName, + x.diskName, + x.auditInfo, + x.accessScope, + x.labels, + x.autodeleteEnabled, + x.autodeleteThreshold + ) + ) + + implicit val getAppResponseEncoder: Encoder[GetAppResponse] = + Encoder.forProduct18( + "workspaceId", + "appName", + "cloudContext", + "region", + "kubernetesRuntimeConfig", + "autopilot", + "errors", + "status", + "proxyUrls", + "diskName", + "customEnvironmentVariables", + "auditInfo", + "appType", + "chartName", + "accessScope", + "labels", + "autodeleteEnabled", + "autodeleteThreshold" + )(x => GetAppResponse.unapply(x).get) } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2Routes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2Routes.scala deleted file mode 100644 index 4c983659a1..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2Routes.scala +++ /dev/null @@ -1,281 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package api - -import cats.syntax.all._ -import akka.http.scaladsl.marshalling.ToResponseMarshallable -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server -import akka.http.scaladsl.server.Directives.{pathEndOrSingleSlash, _} -import cats.effect.IO -import cats.mtl.Ask -import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ -import io.circe.{Decoder, DecodingFailure, Encoder, KeyEncoder} -import io.opencensus.scala.akka.http.TracingDirective.traceRequestForService -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName -import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ -import org.broadinstitute.dsde.workbench.leonardo.http.api.AppV2Routes._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.AppService -import org.broadinstitute.dsde.workbench.model.UserInfo -import org.broadinstitute.dsde.workbench.model.google.GcsBucketName -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s.Uri - -class AppV2Routes(kubernetesService: AppService[IO], userInfoDirectives: UserInfoDirectives)(implicit - metrics: OpenTelemetryMetrics[IO] -) { - - val routes: server.Route = traceRequestForService(serviceData) { span => - extractAppContext(Some(span)) { implicit ctx => - userInfoDirectives.requireUserInfo { userInfo => - CookieSupport.setTokenCookie(userInfo) { - pathPrefix("apps" / "v2" / workspaceIdSegment) { workspaceId => - pathEndOrSingleSlash { - parameterMap { params => - get { - complete( - listAppV2Handler(userInfo, workspaceId, params) - ) - } - } - } ~ pathPrefix(Segment) { appNameString => - RouteValidation.validateNameDirective(appNameString, AppName.apply) { appName => - post { - entity(as[CreateAppRequest]) { req => - complete( - createAppV2Handler(userInfo, workspaceId, appName, req) - ) - } - } ~ - get { - complete( - getAppV2Handler(userInfo, workspaceId, appName) - ) - } ~ - delete { - parameterMap { params => - complete( - deleteAppV2Handler(userInfo, workspaceId, appName, params) - ) - } - } - } - } ~ pathPrefix("deleteAll") { - post { - parameterMap { params => - complete( - deleteAllAppsForWorkspaceHandler(userInfo, workspaceId, params) - ) - } - } - } - } - } - } - } - } - - private[api] def createAppV2Handler(userInfo: UserInfo, - workspaceId: WorkspaceId, - appName: AppName, - req: CreateAppRequest - )(implicit ev: Ask[IO, AppContext]): IO[ToResponseMarshallable] = { - val apiCallName = "createAppV2" - for { - _ <- metrics.incrementCounter(apiCallName) - _ <- withSpanResource(apiCallName, kubernetesService.createAppV2(userInfo, workspaceId, appName, req)) - } yield StatusCodes.Accepted - } - - private[api] def getAppV2Handler(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName)(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = { - val apiCallName = "getAppV2" - for { - _ <- metrics.incrementCounter(apiCallName) - resp <- withSpanResource(apiCallName, - kubernetesService.getAppV2( - userInfo, - workspaceId, - appName - ) - ) - } yield StatusCodes.OK -> resp - } - - private[api] def listAppV2Handler(userInfo: UserInfo, workspaceId: WorkspaceId, params: Map[String, String])(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = { - val apiCallName = "listAppV2" - for { - _ <- metrics.incrementCounter(apiCallName) - resp <- withSpanResource(apiCallName, - kubernetesService.listAppV2( - userInfo, - workspaceId, - params - ) - ) - } yield StatusCodes.OK -> resp - } - - private[api] def deleteAppV2Handler(userInfo: UserInfo, - workspaceId: WorkspaceId, - appName: AppName, - params: Map[String, String] - )(implicit ev: Ask[IO, AppContext]): IO[ToResponseMarshallable] = { - val apiCallName = "deleteAppV2" - val deleteDisk = params.get("deleteDisk").exists(_ == "true") - val tags = Map("deleteDisk" -> deleteDisk.toString) - for { - _ <- metrics.incrementCounter(apiCallName, 1, tags) - _ <- withSpanResource(apiCallName, - kubernetesService.deleteAppV2( - userInfo, - workspaceId, - appName, - deleteDisk - ) - ) - } yield StatusCodes.Accepted - } - - private[api] def deleteAllAppsForWorkspaceHandler(userInfo: UserInfo, - workspaceId: WorkspaceId, - params: Map[String, String] - )(implicit ev: Ask[IO, AppContext]): IO[ToResponseMarshallable] = { - val apiCallName = "deleteAllAppV2" - val deleteDisk = params.get("deleteDisk").exists(_ == "true") - val tags = Map("deleteDisk" -> deleteDisk.toString) - for { - _ <- metrics.incrementCounter(apiCallName, 1, tags) - _ <- withSpanResource(apiCallName, - kubernetesService.deleteAllAppsV2( - userInfo, - workspaceId, - deleteDisk - ) - ) - } yield StatusCodes.Accepted - } - -} - -object AppV2Routes { - - implicit val createAppDecoder: Decoder[CreateAppRequest] = - Decoder.instance { x => - for { - c <- x.downField("kubernetesRuntimeConfig").as[Option[KubernetesRuntimeConfig]] - s <- x.downField("accessScope").as[Option[AppAccessScope]] - d <- x.downField("diskConfig").as[Option[PersistentDiskRequest]] - l <- x.downField("labels").as[Option[LabelMap]] - cv <- x.downField("customEnvironmentVariables").as[Option[LabelMap]] - dp <- x.downField("descriptorPath").as[Option[Uri]] - ea <- x.downField("extraArgs").as[Option[List[String]]] - wsi <- x.downField("workspaceId").as[Option[WorkspaceId]] - swi <- x.downField("sourceWorkspaceId").as[Option[WorkspaceId]] - adte <- x.downField("autodeleteEnabled").as[Option[Boolean]] - adtm <- x.downField("autodeleteThreshold").as[Option[AutodeleteThreshold]] - autopilot <- x.downField("autopilot").as[Option[Autopilot]] - bucketNameToMount <- x.downField("bucketNameToMount").as[Option[GcsBucketName]] - - optStr <- x.downField("appType").as[Option[String]] - cn <- x.downField("allowedChartName").as[Option[AllowedChartName]] - // TODO: once AOU has migrated to use the new app type, we can use much simpler version instead of this workaround for backwards compatibility - (appType, allowedChartName) <- optStr match { - case Some(value) => - AppType.stringToObject - .get(value) match { - case Some(v) => (v, cn).asRight[DecodingFailure] - case None => - if (value == "RSTUDIO") - (AppType.Allowed, Some(AllowedChartName.RStudio)).asRight[DecodingFailure] - else - DecodingFailure(s"Invalid app type ${value}", List.empty).asLeft[(AppType, Option[AllowedChartName])] - } - case None => (AppType.Galaxy, cn).asRight[DecodingFailure] - } - } yield CreateAppRequest( - c, - appType, - allowedChartName, - s, - d, - l.getOrElse(Map.empty), - cv.getOrElse(Map.empty), - dp, - ea.getOrElse(List.empty), - wsi, - swi, - adte, - adtm, - autopilot, - bucketNameToMount - ) - } - - implicit val nameKeyEncoder: KeyEncoder[ServiceName] = KeyEncoder.encodeKeyString.contramap(_.value) - - implicit val listAppResponseEncoder: Encoder[ListAppResponse] = - Encoder.forProduct17( - "workspaceId", - "cloudContext", - "region", - "kubernetesRuntimeConfig", - "autopilot", - "errors", - "status", - "proxyUrls", - "appName", - "appType", - "chartName", - "diskName", - "auditInfo", - "accessScope", - "labels", - "autodeleteEnabled", - "autodeleteThreshold" - )(x => - (x.workspaceId, - x.cloudContext, - x.region, - x.kubernetesRuntimeConfig, - x.autopilot, - x.errors, - x.status, - x.proxyUrls, - x.appName, - x.appType, - x.chartName, - x.diskName, - x.auditInfo, - x.accessScope, - x.labels, - x.autodeleteEnabled, - x.autodeleteThreshold - ) - ) - - implicit val getAppResponseEncoder: Encoder[GetAppResponse] = - Encoder.forProduct18( - "workspaceId", - "appName", - "cloudContext", - "region", - "kubernetesRuntimeConfig", - "autopilot", - "errors", - "status", - "proxyUrls", - "diskName", - "customEnvironmentVariables", - "auditInfo", - "appType", - "chartName", - "accessScope", - "labels", - "autodeleteEnabled", - "autodeleteThreshold" - )(x => GetAppResponse.unapply(x).get) -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala index 36ae7e705c..b421cfbaab 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala @@ -45,7 +45,6 @@ class HttpRoutes( private val corsSupport = new CorsSupport(contentSecurityPolicy, refererConfig) private val kubernetesRoutes = new AppRoutes(kubernetesService, userInfoDirectives) private val appRoutes = createAppRoutesUsingServicesRegistry - private val appV2Routes = new AppV2Routes(kubernetesService, userInfoDirectives) private val runtimeV2Routes = new RuntimeV2Routes(refererConfig, azureService, userInfoDirectives) private val diskV2Routes = new DiskV2Routes(diskV2Service, userInfoDirectives) private val adminRoutes = new AdminRoutes(adminService, userInfoDirectives) @@ -125,7 +124,7 @@ class HttpRoutes( ) ~ oidcConfig.oauth2Routes ~ proxyRoutes.get.route ~ statusRoutes.route ~ pathPrefix("api") { runtimeRoutes.get.routes ~ runtimeV2Routes.routes ~ - diskRoutes.get.routes ~ kubernetesRoutes.routes ~ appV2Routes.routes ~ diskV2Routes.routes ~ adminRoutes.routes ~ + diskRoutes.get.routes ~ kubernetesRoutes.routes ~ diskV2Routes.routes ~ adminRoutes.routes ~ resourcesRoutes.get.routes } ) @@ -136,7 +135,7 @@ class HttpRoutes( pathPrefix("api") { runtimeRoutes.get.routes ~ runtimeV2Routes.routes ~ diskRoutes.get.routes ~ diskV2Routes.routes ~ - appRoutes.get.routes ~ appV2Routes.routes ~ adminRoutes.routes + appRoutes.get.routes ~ adminRoutes.routes } ) } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppService.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppService.scala index 42ec6f4468..f45cf54502 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppService.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppService.scala @@ -59,36 +59,6 @@ trait AppService[F[_]] { def startApp(userInfo: UserInfo, cloudContext: CloudContext.Gcp, appName: AppName)(implicit as: Ask[F, AppContext] ): F[Unit] - - def createAppV2( - userInfo: UserInfo, - workspaceId: WorkspaceId, - appName: AppName, - req: CreateAppRequest - )(implicit as: Ask[F, AppContext]): F[Unit] - - def getAppV2( - userInfo: UserInfo, - workspaceId: WorkspaceId, - appName: AppName - )(implicit as: Ask[F, AppContext]): F[GetAppResponse] - - def listAppV2( - userInfo: UserInfo, - workspaceId: WorkspaceId, - params: Map[String, String] - )(implicit as: Ask[F, AppContext]): F[Vector[ListAppResponse]] - - def deleteAppV2( - userInfo: UserInfo, - workspaceId: WorkspaceId, - appName: AppName, - deleteDisk: Boolean - )(implicit as: Ask[F, AppContext]): F[Unit] - - def deleteAllAppsV2(userInfo: UserInfo, workspaceId: WorkspaceId, deleteDisk: Boolean)(implicit - as: Ask[F, AppContext] - ): F[Unit] } final case class AppServiceConfig(enableCustomAppCheck: Boolean, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala index 8f1a591f70..30a0eef108 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala @@ -3,7 +3,6 @@ package http package service import akka.http.scaladsl.model.StatusCodes -import bio.terra.workspace.model.{IamRole, WorkspaceDescription} import cats.Parallel import cats.data.NonEmptyList import cats.effect.Async @@ -14,16 +13,7 @@ import monocle.macros.syntax.lens._ import org.apache.commons.lang3.RandomStringUtils import org.broadinstitute.dsde.workbench.google2.GKEModels.{KubernetesClusterName, NodepoolName} import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.NamespaceName -import org.broadinstitute.dsde.workbench.google2.{ - DiskName, - GoogleComputeService, - GoogleResourceService, - KubernetesName, - Location, - MachineTypeName, - RegionName, - ZoneName -} +import org.broadinstitute.dsde.workbench.google2.{DiskName, GoogleComputeService, GoogleResourceService, KubernetesName, Location, MachineTypeName, RegionName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo.AppRestore.GalaxyRestore import org.broadinstitute.dsde.workbench.leonardo.AppType._ import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ @@ -32,13 +22,8 @@ import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.dao.WsmApiClientProvider import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService import org.broadinstitute.dsde.workbench.leonardo.db.DBIOInstances.dbioInstance -import org.broadinstitute.dsde.workbench.leonardo.db.KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.LeoAppServiceInterp.{ - checkIfCanBeDeleted, - getAppSamPolicyMap, - isPatchVersionDifference -} +import org.broadinstitute.dsde.workbench.leonardo.http.service.LeoAppServiceInterp.{checkIfCanBeDeleted, getAppSamPolicyMap, isPatchVersionDifference} import org.broadinstitute.dsde.workbench.leonardo.model.SamResourceAction._ import org.broadinstitute.dsde.workbench.leonardo.model._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage._ @@ -646,56 +631,6 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, _ <- publisherQueue.offer(message) } yield () - override def listAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, params: Map[String, String])(implicit - as: Ask[F, AppContext] - ): F[Vector[ListAppResponse]] = - for { - ctx <- as.ask - // Make sure that the user still has access to the resource parent workspace - hasWorkspacePermission <- authProvider.isUserWorkspaceReader( - WorkspaceResourceSamResourceId(workspaceId), - userInfo - ) - _ <- F.raiseUnless(hasWorkspacePermission)(ForbiddenError(userInfo.userEmail)) - - paramMap <- F.fromEither(processListParameters(params)) - creatorOnly <- F.fromEither(processCreatorOnlyParameter(userInfo.userEmail, params, ctx.traceId)) - allClusters <- KubernetesServiceDbQueries - .listFullAppsByWorkspaceId(Some(workspaceId), paramMap._1, paramMap._2, creatorOnly) - .transaction - res <- filterAppsBySamPermission(allClusters, userInfo, paramMap._3, false) - - } yield res - - override def getAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName)(implicit - as: Ask[F, AppContext] - ): F[GetAppResponse] = - for { - ctx <- as.ask - appOpt <- getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName).transaction - app <- F.fromOption( - appOpt, - AppNotFoundByWorkspaceIdException(workspaceId, appName, ctx.traceId, "No active app found in DB") - ) - hasWorkspacePermission <- authProvider.isUserWorkspaceReader( - WorkspaceResourceSamResourceId(workspaceId), - userInfo - ) - _ <- F.raiseUnless(hasWorkspacePermission)(ForbiddenError(userInfo.userEmail)) - - hasResourcePermission <- authProvider.hasPermission[AppSamResourceId, AppAction](app.app.samResourceId, - AppAction.GetAppStatus, - userInfo - ) - _ <- - if (hasResourcePermission) F.unit - else - log.info(ctx.loggingCtx)( - s"User ${userInfo} tried to access app ${appName.value} without proper permissions. Returning 404" - ) >> F - .raiseError[Unit](AppNotFoundByWorkspaceIdException(workspaceId, appName, ctx.traceId, "permission denied")) - } yield GetAppResponse.fromDbResult(app, Config.proxyConfig.proxyUrlBase) - private def getUpdateAppTransaction(appId: AppId, validatedChanges: UpdateAppRequest): F[Unit] = (for { _ <- validatedChanges.autodeleteEnabled.traverse(enabled => appQuery @@ -709,6 +644,7 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, ) } yield ()).transaction + // TODO I think this is Azure-only, any point in leaving it around for GCP someday? override def updateApp(userInfo: UserInfo, cloudContext: CloudContext.Gcp, appName: AppName, req: UpdateAppRequest)( implicit as: Ask[F, AppContext] ): F[Unit] = @@ -738,309 +674,6 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, _ <- getUpdateAppTransaction(appResult.app.id, req) } yield () - override def createAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName, req: CreateAppRequest)( - implicit as: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- as.ask - - // Resolve the user email in Sam from the user token. This translates a pet token to the owner email. - userEmail <- samService.getUserEmail(userInfo.accessToken.token) - - // Check the calling user has permission on the workspace - hasPermission <- authProvider.hasPermission[WorkspaceResourceSamResourceId, WorkspaceAction]( - WorkspaceResourceSamResourceId(workspaceId), - WorkspaceAction.CreateControlledUserResource, - userInfo - ) - _ <- F.raiseUnless(hasPermission)(ForbiddenError(userEmail)) - - // Validate shared access scope apps against an allow-list. No-op for private apps. - _ <- req.accessScope match { - case Some(AppAccessScope.WorkspaceShared) => - F.raiseUnless(ConfigReader.appConfig.azure.allowedSharedApps.contains(req.appType))( - SharedAppNotAllowedException(req.appType, ctx.traceId) - ) - case _ => F.unit - } - - // Resolve the workspace in WSM to get the cloud context - workspaceDescOpt <- wsmClientProvider.getWorkspace(userInfo.accessToken.token, workspaceId) - workspaceDesc <- F.fromOption(workspaceDescOpt, WorkspaceNotFoundException(workspaceId, ctx.traceId)) - cloudContext <- (workspaceDesc.azureContext, workspaceDesc.gcpContext) match { - case (Some(azureContext), _) => F.pure[CloudContext](CloudContext.Azure(azureContext)) - case (_, Some(gcpContext)) => F.pure[CloudContext](CloudContext.Gcp(gcpContext)) - case (None, None) => F.raiseError[CloudContext](CloudContextNotFoundException(workspaceId, ctx.traceId)) - } - - // Check if the app already exists - appOpt <- KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName).transaction - _ <- appOpt.fold(F.unit)(c => - F.raiseError[Unit](AppAlreadyExistsInWorkspaceException(workspaceId, appName, c.app.status, ctx.traceId)) - ) - - // Validate the machine config from the request - // For Azure: we don't support setting a machine type in the request; we use the landing zone configuration instead. - // For GCP: we support setting optionally a machine type in the request; and use a default value otherwise. - machineConfig <- (cloudContext.cloudProvider, req.kubernetesRuntimeConfig) match { - case (CloudProvider.Azure, Some(_)) => - F.raiseError(AppMachineConfigNotSupportedException(ctx.traceId)) - case (CloudProvider.Azure, None) => - F.pure(KubernetesRuntimeConfig(NumNodes(1), MachineTypeName("unset"), false)) - case (CloudProvider.Gcp, Some(mt)) => F.pure(mt) - case (CloudProvider.Gcp, None) => - F.pure( - KubernetesRuntimeConfig( - config.leoKubernetesConfig.nodepoolConfig.galaxyNodepoolConfig.numNodes, - config.leoKubernetesConfig.nodepoolConfig.galaxyNodepoolConfig.machineType, - config.leoKubernetesConfig.nodepoolConfig.galaxyNodepoolConfig.autoscalingEnabled - ) - ) - } - - samResourceId <- F.delay(AppSamResourceId(UUID.randomUUID().toString, req.accessScope)) - - // Create kubernetes-app Sam resource with a creator policy and the workspace as the parent - leoToken <- authProvider.getLeoAuthToken - leoEmail <- samService.getUserEmail(leoToken) - _ <- samService.createResource(userInfo.accessToken.token, - samResourceId, - None, - Some(workspaceId), - getAppSamPolicyMap(userEmail, leoEmail, req.accessScope) - ) - - // Save or retrieve a KubernetesCluster record for the app - saveCluster <- F.fromEither( - getSavableCluster(userEmail, cloudContext, false, ctx.now) - ) - saveClusterResult <- KubernetesServiceDbQueries - .saveOrGetClusterForApp(saveCluster, ctx.traceId) - .transaction(isolationLevel = TransactionIsolation.Serializable) - _ <- - if (saveClusterResult.minimalCluster.status == KubernetesClusterStatus.Error) - F.raiseError[Unit]( - KubernetesAppCreationException( - s"You cannot create an app while a cluster ${saveClusterResult.minimalCluster.clusterName.value} is in status ${saveClusterResult.minimalCluster.status}", - Some(ctx.traceId) - ) - ) - else F.unit - - // Use the default nodepool of the cluster. - // The v1 createApp endpoint has logic for managing nodepools per user in Leonardo. - // In the v2 endpoint, nodepools are created upstream of Leonardo. - nodepool = saveClusterResult.defaultNodepool.toNodepool() - - // Retrieve a pet identity from Sam - petSA <- samService.getPetServiceAccountOrManagedIdentity(userInfo.accessToken.token, cloudContext) - _ <- ctx.span.traverse(s => F.delay(s.addAnnotation("Done Sam call for getPetServiceAccount"))) - - // Process persistent disk in the request, check if the disk was previously attached to any other app - diskResultOpt <- req.diskConfig.traverse(diskReq => - RuntimeServiceInterp.processPersistentDiskRequestForWorkspace( - diskReq, - config.leoKubernetesConfig.diskConfig.defaultZone, // this need to be updated if we support non-default zone for k8s apps - cloudContext, - workspaceId, - userInfo, - userEmail, - petSA, - appTypeToFormattedByType(req.appType), - samService, - config.leoKubernetesConfig.diskConfig - ) - ) - lastUsedApp <- getLastUsedAppForDisk(req, diskResultOpt) - - // Save a new App record in the database - saveApp <- F.fromEither( - getSavableApp( - cloudContext, - appName, - userEmail, - samResourceId, - req, - diskResultOpt.map(_.disk), - lastUsedApp, - petSA, - nodepool.id, - Some(workspaceId), - None, - ctx - ) - ) - app <- appQuery.save(saveApp, Some(ctx.traceId)).transaction - - // Publish a CreateApp message for Back Leo - createAppV2Message = CreateAppV2Message( - app.id, - app.appName, - workspaceId, - cloudContext, - BillingProfileId(workspaceDesc.spendProfile), - Some(ctx.traceId) - ) - _ <- publisherQueue.offer(createAppV2Message) - } yield () - - override def deleteAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName, deleteDisk: Boolean)(implicit - as: Ask[F, AppContext] - ): F[Unit] = for { - hasWorkspacePermission <- authProvider.isUserWorkspaceReader(WorkspaceResourceSamResourceId(workspaceId), userInfo) - _ <- F.raiseUnless(hasWorkspacePermission)(ForbiddenError(userInfo.userEmail)) - - ctx <- as.ask - appOpt <- KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName).transaction - appResult <- F.fromOption( - appOpt, - AppNotFoundByWorkspaceIdException(workspaceId, appName, ctx.traceId, "No active app found in DB") - ) - - workspaceApi <- wsmClientProvider.getWorkspaceApi(userInfo.accessToken.token) - attempt <- F.delay(workspaceApi.getWorkspace(workspaceId.value, IamRole.READER)).attempt - - _ <- attempt match { - // if the workspace is found, delete the app normally - case Right(workspaceDesc) => - deleteAppV2Base(appResult.app, appResult.cluster.cloudContext, userInfo, workspaceId, deleteDisk, workspaceDesc) - // if the workspace can't be found, delete the workspace records - case Left(error) => - if (error.getMessage.contains("not found")) { - deleteAppRecords(userInfo, appResult.cluster.cloudContext, appResult.app.appName) - } - // raise error if the user doesn't have permission - else { - F.raiseError(WorkspaceNotFoundException(workspaceId, ctx.traceId)) - } - } - - } yield () - - override def deleteAllAppsV2(userInfo: UserInfo, workspaceId: WorkspaceId, deleteDisk: Boolean)(implicit - as: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- as.ask - hasWorkspacePermission <- authProvider.isUserWorkspaceReader(WorkspaceResourceSamResourceId(workspaceId), userInfo) - _ <- F.raiseUnless(hasWorkspacePermission)(ForbiddenError(userInfo.userEmail)) - - allClusters <- KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId), Map.empty).transaction - - // need the cloudContext to delete the resources, get it from the KubernetesCluster here - // apps = List(App, CloudContext) - apps = allClusters.flatMap { cluster => - val cloudContext = cluster.cloudContext - cluster.nodepools.flatMap { nodepool => - nodepool.apps.map { app => - (app, cloudContext) - } - } - } - - nonDeletableApps = apps.filterNot(app => AppStatus.deletableStatuses.contains(app._1.status)).map(_._1) - - _ <- F - .raiseError(DeleteAllAppsCannotBePerformed(workspaceId, nonDeletableApps, ctx.traceId)) - .whenA(!nonDeletableApps.isEmpty) - - workspaceApi <- wsmClientProvider.getWorkspaceApi(userInfo.accessToken.token) - attempt <- F.delay(workspaceApi.getWorkspace(workspaceId.value, IamRole.READER)).attempt - - _ <- attempt match { - // if the workspace is found, delete the app normally - case Right(workspaceDesc) => - apps - .traverse { app => - deleteAppV2Base(app._1, app._2, userInfo, workspaceId, deleteDisk, workspaceDesc) - } - case Left(error) => - // if the workspace can't be found, delete the workspace records - if (error.getMessage.contains("not found")) { - apps.traverse { app => - deleteAppRecords(userInfo, app._2, app._1.appName) - } - } - // raise error if the user doesn't have permission - else { - F.raiseError(WorkspaceNotFoundException(workspaceId, ctx.traceId)) - } - } - - } yield () - - private def deleteAppV2Base(app: App, - cloudContext: CloudContext, - userInfo: UserInfo, - workspaceId: WorkspaceId, - deleteDisk: Boolean, - workspaceDesc: WorkspaceDescription - )(implicit - as: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- as.ask - listOfPermissions <- authProvider.getActions(app.samResourceId, userInfo) - - // throw 404 if no GetAppStatus permission - hasReadPermission = listOfPermissions.toSet.contains(AppAction.GetAppStatus) - _ <- - if (hasReadPermission) F.unit - else - F.raiseError[Unit]( - AppNotFoundByWorkspaceIdException(workspaceId, app.appName, ctx.traceId, "no read permission") - ) - - // throw 403 if no DeleteApp permission - hasDeletePermission = listOfPermissions.toSet.contains(AppAction.DeleteApp) - _ <- - if (hasDeletePermission) F.unit - else F.raiseError[Unit](ForbiddenError(userInfo.userEmail)) - - // check if app can be deleted (Leo manages apps, so checking the leo status) - _ <- F.raiseUnless(app.status.isDeletable)( - AppCannotBeDeletedException(cloudContext, app.appName, app.status, ctx.traceId) - ) - - // check if databases, namespaces and managed identities associated with the app can be deleted - // (but only for Azure apps) - _ <- - if (cloudContext.cloudProvider == CloudProvider.Azure) { - for { - _ <- checkIfSubresourcesDeletable(app.id, WsmResourceType.AzureDatabase, userInfo, workspaceId) - _ <- checkIfSubresourcesDeletable(app.id, WsmResourceType.AzureManagedIdentity, userInfo, workspaceId) - _ <- checkIfSubresourcesDeletable(app.id, WsmResourceType.AzureKubernetesNamespace, userInfo, workspaceId) - } yield () - } else F.unit - - // Get the disk and check if its deletable (if disk is being deleted) - diskIdOpt = if (deleteDisk) app.appResources.disk.map(_.id) else None - _ = (deleteDisk, diskIdOpt, cloudContext.cloudProvider) match { - // only check WSM state for Azure apps (Azure apps don't have disks currently, but they are coming...) - case (true, Some(diskId), CloudProvider.Azure) => - for { - _ <- checkIfSubresourcesDeletable(app.id, WsmResourceType.AzureDisk, userInfo, workspaceId) - _ <- persistentDiskQuery.markPendingDeletion(diskId, ctx.now).transaction - } yield () - case (true, None, _) => - log.info(s"No disk found to delete for app ${app.id}, ${app.appName}. No-op for deleteDisk") - case _ => F.unit // Do nothing if deleteDisk is false - } - - _ <- - for { - _ <- KubernetesServiceDbQueries.markPreDeleting(app.id).transaction - deleteMessage = DeleteAppV2Message( - app.id, - app.appName, - workspaceId, - cloudContext, - diskIdOpt, - BillingProfileId(workspaceDesc.getSpendProfile), - Some(ctx.traceId) - ) - _ <- publisherQueue.offer(deleteMessage) - } yield () - } yield () - private def checkIfSubresourcesDeletable(appId: AppId, resourceType: WsmResourceType, userInfo: UserInfo, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2RouteSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2RouteSpec.scala deleted file mode 100644 index 7969367a12..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppV2RouteSpec.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.http.api - -import io.circe.parser.decode -import org.broadinstitute.dsde.workbench.google2.DiskName -import org.broadinstitute.dsde.workbench.leonardo.http.api.AppV2Routes.createAppDecoder -import org.broadinstitute.dsde.workbench.leonardo.http.{CreateAppRequest, PersistentDiskRequest} -import org.broadinstitute.dsde.workbench.leonardo.{AllowedChartName, AppType, LeonardoTestSuite, WorkspaceId} -import org.scalatest.flatspec.AnyFlatSpec - -import java.util.UUID - -class AppV2RouteSpec extends AnyFlatSpec with LeonardoTestSuite { - it should "decode createApp request properly" in { - val workspaceId = WorkspaceId(UUID.randomUUID()) - val jsonString = - s""" - |{ - | "diskConfig": { - | "name": "disk1" - | }, - | "appType": "ALLOWED", - | "allowedChartName": "rstudio", - | "workspaceId": "${workspaceId.value.toString}" - |} - |""".stripMargin - - val res = decode[CreateAppRequest](jsonString) - - val expected = CreateAppRequest( - None, - AppType.Allowed, - Some(AllowedChartName.RStudio), - None, - Some(PersistentDiskRequest(DiskName("disk1"), None, None, Map.empty)), - Map.empty, - Map.empty, - None, - List.empty, - Some(workspaceId), - None, - None, - None, - None, - None - ) - res shouldBe (Right(expected)) - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala index 07e1d20f07..2f7135c6b9 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala @@ -15,24 +15,15 @@ import org.broadinstitute.dsde.workbench.leonardo.AppRestore.{GalaxyRestore, Oth import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData._ import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.auth.{AllowlistAuthProvider, SamAuthProvider} +import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config.Config.leoKubernetesConfig import org.broadinstitute.dsde.workbench.leonardo.config.{Config, CustomAppConfig, CustomApplicationAllowListConfig} import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.model._ -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAppMessage, - CreateAppV2Message, - DeleteAppMessage, - DeleteAppV2Message -} -import org.broadinstitute.dsde.workbench.leonardo.monitor.{ - ClusterNodepoolAction, - LeoPubsubMessage, - LeoPubsubMessageType -} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, DeleteAppMessage} +import org.broadinstitute.dsde.workbench.leonardo.monitor.{ClusterNodepoolAction, LeoPubsubMessage, LeoPubsubMessageType} import org.broadinstitute.dsde.workbench.leonardo.util.{AzureTestUtils, QueueFactory} import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} @@ -44,9 +35,8 @@ import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.{verify, when} import org.scalatest.Assertion import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.prop.TableDrivenPropertyChecks.{forAll, _} +import org.scalatest.prop.TableDrivenPropertyChecks._ import org.scalatestplus.mockito.MockitoSugar -import org.typelevel.log4cats.StructuredLogger import java.time.Instant import scala.concurrent.ExecutionContext.Implicits.global @@ -1820,188 +1810,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le } - it should "Azure - only delete App records in deleteAllAppsV2 if a workspace has been deleted" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue) - - val appName = AppName("app1") - - val appReq = - createAppRequest.copy( - kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - allowedChartName = Some(AllowedChartName.Sas), - diskConfig = None, - workspaceId = Some(workspaceId2) - ) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId2, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId2, appName) - } - // Set the cluster, app, and nodepool to running and start tracking the usage - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - dbFutureValue( - kubernetesClusterQuery.updateStatus(appResultPreStatusUpdate.get.cluster.id, KubernetesClusterStatus.Running) - ) - - kubeServiceInterp - .deleteAllAppsV2(userInfo, workspaceId2, true) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val clusterPostDelete = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId2), Map.empty, true) - } - - clusterPostDelete.length shouldEqual 1 - clusterPostDelete.map(_.status) shouldEqual List(KubernetesClusterStatus.Deleted) - val nodepools = clusterPostDelete.flatMap(_.nodepools) - val apps = clusterPostDelete.flatMap(_.nodepools).flatMap(_.apps) - nodepools.map(_.status) shouldEqual List(NodepoolStatus.Deleted) - apps.map(_.status) shouldEqual List(AppStatus.Deleted) - - // throw away create messages - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val messages = publisherQueue.tryTakeN(Some(1)).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - messages.map(_.messageType) shouldBe List.empty - - } - - it should "Azure - only delete App records in deleteAppV2 if a workspace has been deleted" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue) - - val appName = AppName("app1") - - val appReq = - createAppRequest.copy( - kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - allowedChartName = Some(AllowedChartName.Sas), - diskConfig = None, - workspaceId = Some(workspaceId2) - ) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId2, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId2, appName) - } - // Set the cluster, app, and nodepool to running and start tracking the usage - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - dbFutureValue( - kubernetesClusterQuery.updateStatus(appResultPreStatusUpdate.get.cluster.id, KubernetesClusterStatus.Running) - ) - - kubeServiceInterp - .deleteAppV2(userInfo, workspaceId2, appName, true) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val clusterPostDelete = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId2), Map.empty, true) - } - - clusterPostDelete.length shouldEqual 1 - clusterPostDelete.map(_.status) shouldEqual List(KubernetesClusterStatus.Deleted) - val nodepools = clusterPostDelete.flatMap(_.nodepools) - val apps = clusterPostDelete.flatMap(_.nodepools).flatMap(_.apps) - nodepools.map(_.status) shouldEqual List(NodepoolStatus.Deleted) - apps.map(_.status) shouldEqual List(AppStatus.Deleted) - - // throw away create messages - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val messages = publisherQueue.tryTakeN(Some(1)).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - messages.map(_.messageType) shouldBe List.empty - - } - - it should "error on deleteAllApps if the user doesn't have permission to access the workspace" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = wsmDao) - when { - workspaceApi.getWorkspace(ArgumentMatchers.eq(workspaceId2.value), any()) - } thenAnswer (_ => throw new Exception("User does not have access to this workspace")) - - val appName = AppName("app1") - - val appReq = - createAppRequest.copy( - kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - allowedChartName = Some(AllowedChartName.Sas), - diskConfig = None, - workspaceId = Some(workspaceId2) - ) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId2, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId2, appName) - } - // Set the cluster, app, and nodepool to running and start tracking the usage - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - dbFutureValue( - kubernetesClusterQuery.updateStatus(appResultPreStatusUpdate.get.cluster.id, KubernetesClusterStatus.Running) - ) - - an[WorkspaceNotFoundException] should be thrownBy { - kubeServiceInterp - .deleteAllAppsV2(userInfo, workspaceId2, false) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "error on deleteAppV2 if the user doesn't have permission to access the workspace" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = wsmDao) - when { - workspaceApi.getWorkspace(ArgumentMatchers.eq(workspaceId2.value), any()) - } thenAnswer (_ => throw new Exception("User does not have access to this workspace")) - - val appName = AppName("app1") - - val appReq = - createAppRequest.copy( - kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - allowedChartName = Some(AllowedChartName.Sas), - diskConfig = None, - workspaceId = Some(workspaceId2) - ) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId2, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId2, appName) - } - // Set the cluster, app, and nodepool to running and start tracking the usage - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - dbFutureValue( - kubernetesClusterQuery.updateStatus(appResultPreStatusUpdate.get.cluster.id, KubernetesClusterStatus.Running) - ) - - an[WorkspaceNotFoundException] should be thrownBy { - kubeServiceInterp - .deleteAppV2(userInfo, workspaceId2, appName, false) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - it should "V1 GCP - deleteAppRecords error on delete if app creator is removed from app's project" in isolatedDbTest { val appName = AppName("app1") @@ -2335,861 +2143,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le messages shouldBe List.empty } - // ----- App V2 tests ----- - - it should "V2 GCP - create an app V2 and a new disk" in isolatedDbTest { - val appName = AppName("app1") - val createDiskConfig = PersistentDiskRequest(diskName, None, None, Map.empty) - val customEnvVars = Map("WORKSPACE_NAME" -> "testWorkspace") - val appReq = createAppRequest.copy(diskConfig = Some(createDiskConfig), customEnvironmentVariables = customEnvVars) - - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val clusters = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId)) - } - clusters.length shouldEqual 1 - clusters.flatMap(_.nodepools).length shouldEqual 1 - val cluster = clusters.head - cluster.auditInfo.creator shouldEqual userInfo.userEmail - - clusters.flatMap(_.nodepools).size shouldBe 1 - val nodepool = clusters.flatMap(_.nodepools).head - nodepool.isDefault shouldBe true - - clusters.flatMap(_.nodepools).flatMap(_.apps).length shouldEqual 1 - val app = clusters.flatMap(_.nodepools).flatMap(_.apps).head - app.appName shouldEqual appName - app.chart shouldEqual galaxyChart - app.auditInfo.creator shouldEqual userInfo.userEmail - app.customEnvironmentVariables shouldEqual customEnvVars - app.workspaceId shouldEqual Some(workspaceId) - - val savedDisk = dbFutureValue { - persistentDiskQuery.getById(app.appResources.disk.get.id) - } - savedDisk.map(_.name) shouldEqual Some(diskName) - } - - it should "V2 Azure - create an app V2" in isolatedDbTest { - val appName = AppName("app1") - val customEnvVars = Map("WORKSPACE_NAME" -> "testWorkspace", - "RELAY_HYBRID_CONNECTION_NAME" -> s"${appName.value}-${workspaceId.value}" - ) - val appReq = createAppRequest.copy(kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - diskConfig = None, - customEnvironmentVariables = customEnvVars - ) - - appServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val clusters = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId)) - } - clusters.length shouldEqual 1 - clusters.flatMap(_.nodepools).length shouldEqual 1 - val cluster = clusters.head - cluster.auditInfo.creator shouldEqual userInfo.userEmail - cluster.status shouldEqual KubernetesClusterStatus.Running - - clusters.flatMap(_.nodepools).size shouldBe 1 - val nodepool = clusters.flatMap(_.nodepools).head - nodepool.isDefault shouldBe true - - clusters.flatMap(_.nodepools).flatMap(_.apps).length shouldEqual 1 - val app = clusters.flatMap(_.nodepools).flatMap(_.apps).head - app.appName shouldEqual appName - app.chart shouldEqual coaChart - app.auditInfo.creator shouldEqual userInfo.userEmail - app.customEnvironmentVariables shouldEqual customEnvVars - app.status shouldEqual AppStatus.Precreating - app.appResources.disk shouldEqual None - app.workspaceId shouldEqual Some(workspaceId) - } - - it should "V2 Azure - throw an error when providing a machine config" in isolatedDbTest { - val appName = AppName("app1") - val appReq = createAppRequest.copy( - appType = AppType.Cromwell, - diskConfig = None - ) - - a[AppMachineConfigNotSupportedException] should be thrownBy - appServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "V2 Azure - throw an error when providing a disk config" in isolatedDbTest { - val appName = AppName("app1") - val appReq = createAppRequest.copy( - kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - diskConfig = Some(PersistentDiskRequest(diskName, None, None, Map.empty)) - ) - - a[AppDiskNotSupportedException] should be thrownBy - appServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "V2 GCP - queue the proper v2 message when creating an app and a new disk" in isolatedDbTest { - val appName = AppName("app1") - val createDiskConfig = PersistentDiskRequest(diskName, None, None, Map.empty) - val customEnvVars = Map("WORKSPACE_NAME" -> "testWorkspace") - val appReq = createAppRequest.copy(diskConfig = Some(createDiskConfig), customEnvironmentVariables = customEnvVars) - - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao, wsmClientProvider = googleWsmClientProvider) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val getApp = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - }.get - - val getMinimalCluster = dbFutureValue { - kubernetesClusterQuery.getMinimalClusterById(getApp.cluster.id) - }.get - - val defaultNodepools = getMinimalCluster.nodepools.filter(_.isDefault) - defaultNodepools.length shouldBe 1 - - val message = publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - message.messageType shouldBe LeoPubsubMessageType.CreateAppV2 - val createAppMessage = message.asInstanceOf[CreateAppV2Message] - createAppMessage.appId shouldBe getApp.app.id - createAppMessage.appName shouldBe getApp.app.appName - createAppMessage.workspaceId shouldBe workspaceId - } - - it should "V2 Azure - queue the proper v2 message when creating an app" in isolatedDbTest { - val appName = AppName("app1") - val customEnvVars = Map("WORKSPACE_NAME" -> "testWorkspace") - val appReq = createAppRequest.copy(kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - diskConfig = None, - customEnvironmentVariables = customEnvVars - ) - - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val getApp = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - }.get - - val getMinimalCluster = dbFutureValue { - kubernetesClusterQuery.getMinimalClusterById(getApp.cluster.id) - }.get - - val defaultNodepools = getMinimalCluster.nodepools.filter(_.isDefault) - defaultNodepools.length shouldBe 1 - - val message = publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - message.messageType shouldBe LeoPubsubMessageType.CreateAppV2 - val createAppMessage = message.asInstanceOf[CreateAppV2Message] - createAppMessage.appId shouldBe getApp.app.id - createAppMessage.appName shouldBe getApp.app.appName - createAppMessage.workspaceId shouldBe workspaceId - } - - it should "V2 GCP - delete a gcp app V2, update status appropriately, and queue a message" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao, wsmClientProvider = googleWsmClientProvider) - val appName = AppName("app1") - val createDiskConfig = PersistentDiskRequest(diskName, None, None, Map.empty) - val appReq = createAppRequest.copy(diskConfig = Some(createDiskConfig)) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - - // we can't delete while its creating, so set it to Running - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - - val appResultPreDelete = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - appResultPreDelete.get.app.status shouldEqual AppStatus.Running - appResultPreDelete.get.app.auditInfo.destroyedDate shouldBe None - - kubeServiceInterp - .deleteAppV2(userInfo, workspaceId, appName, false) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val clusterPostDelete = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId), includeDeleted = true) - } - - clusterPostDelete.length shouldEqual 1 - val nodepool = clusterPostDelete.head.nodepools.head - nodepool.status shouldEqual NodepoolStatus.Running - val app = nodepool.apps.head - app.status shouldEqual AppStatus.Predeleting - - // throw away create message - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val message = publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - message.messageType shouldBe LeoPubsubMessageType.DeleteAppV2 - val deleteAppMessage = message.asInstanceOf[DeleteAppV2Message] - deleteAppMessage.appId shouldBe app.id - deleteAppMessage.workspaceId shouldBe workspaceId - deleteAppMessage.diskId shouldBe None - } - - it should "V2 GCP - fail to delete an app when creator has lost workspace permission" in isolatedDbTest { - val appName = AppName("app1") - val createDiskConfig = PersistentDiskRequest(diskName, None, None, Map.empty) - val appReq = createAppRequest.copy(diskConfig = Some(createDiskConfig)) - - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val appResult = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - dbFutureValue(kubernetesClusterQuery.updateStatus(appResult.get.cluster.id, KubernetesClusterStatus.Running)) - val azureService2 = makeInterp(QueueFactory.makePublisherQueue(), allowListAuthProvider2, gcpWsmDao) - a[ForbiddenError] should be thrownBy azureService2 - .deleteAppV2(userInfo, workspaceId, appName, true) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "V2 Azure - delete an Azure app V2, update status appropriately, and queue a message" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue) - val appName = AppName("app1") - val appReq = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - - // we can't delete while its creating, so set it to Running - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - - val appResultPreDelete = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - appResultPreDelete.get.app.status shouldEqual AppStatus.Running - appResultPreDelete.get.app.auditInfo.destroyedDate shouldBe None - - kubeServiceInterp - .deleteAppV2(userInfo, workspaceId, appName, false) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val clusterPostDelete = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId), includeDeleted = true) - } - - clusterPostDelete.length shouldEqual 1 - val nodepool = clusterPostDelete.head.nodepools.head - nodepool.status shouldEqual NodepoolStatus.Running - val app = nodepool.apps.head - app.status shouldEqual AppStatus.Predeleting - - // throw away create message - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val message = publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - message.messageType shouldBe LeoPubsubMessageType.DeleteAppV2 - val deleteAppMessage = message.asInstanceOf[DeleteAppV2Message] - deleteAppMessage.appId shouldBe app.id - deleteAppMessage.workspaceId shouldBe workspaceId - deleteAppMessage.diskId shouldBe None - } - - it should "V2 Azure - fail to delete an app when creator has lost workspace permission" in isolatedDbTest { - val appName = AppName("app1") - val appReq = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - - appServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val appResult = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - dbFutureValue(kubernetesClusterQuery.updateStatus(appResult.get.cluster.id, KubernetesClusterStatus.Running)) - val azureService2 = makeInterp(QueueFactory.makePublisherQueue(), allowListAuthProvider2) - a[ForbiddenError] should be thrownBy azureService2 - .deleteAppV2(userInfo, workspaceId, appName, true) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "V2 GCP - list apps V2 should return apps for workspace" in isolatedDbTest { - val appName1 = AppName("app1") - val diskName1 = DiskName("newDiskName1") - val createDiskConfig1 = PersistentDiskRequest(diskName1, None, None, Map.empty) - val appReq1 = createAppRequest.copy(labels = Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3"), - diskConfig = Some(createDiskConfig1) - ) - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId, appName1, appReq1) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResult = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName1) - } - dbFutureValue(kubernetesClusterQuery.updateStatus(appResult.get.cluster.id, KubernetesClusterStatus.Running)) - - val appName2 = AppName("app2") - val diskName2 = DiskName("newDiskName2") - val createDiskConfig2 = PersistentDiskRequest(diskName2, None, None, Map.empty) - val appReq2 = createAppRequest.copy(diskConfig = Some(createDiskConfig2)) - - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId, appName2, appReq2) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appName3 = AppName("app3") - val diskName3 = DiskName("newDiskName3") - val createDiskConfig3 = PersistentDiskRequest(diskName3, None, None, Map.empty) - val appReq3 = createAppRequest.copy(diskConfig = Some(createDiskConfig3)) - - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId3, appName3, appReq3) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val listProject1Apps = - gcpWorkspaceAppServiceInterp - .listAppV2(userInfo, workspaceId, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - listProject1Apps.length shouldBe 2 - listProject1Apps.map(_.appName) should contain(appName1) - listProject1Apps.map(_.appName) should contain(appName2) - - val listProject3Apps = - gcpWorkspaceAppServiceInterp - .listAppV2(userInfo, workspaceId3, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - listProject3Apps.length shouldBe 1 - } - - it should "V2 GCP - list apps V2 should fail to return apps for workspace if creator lost workspace access" in isolatedDbTest { - val appName1 = AppName("app1") - val diskName1 = DiskName("newDiskName1") - val createDiskConfig1 = PersistentDiskRequest(diskName1, None, None, Map.empty) - val appReq1 = createAppRequest.copy(labels = Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3"), - diskConfig = Some(createDiskConfig1) - ) - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId, appName1, appReq1) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResult = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName1) - } - dbFutureValue(kubernetesClusterQuery.updateStatus(appResult.get.cluster.id, KubernetesClusterStatus.Running)) - - val listProject1Apps = - gcpWorkspaceAppServiceInterp - .listAppV2(userInfo, workspaceId, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - listProject1Apps.length shouldBe 1 - listProject1Apps.map(_.appName) should contain(appName1) - - val azureService2 = makeInterp(QueueFactory.makePublisherQueue(), allowListAuthProvider2, gcpWsmDao) - a[ForbiddenError] should be thrownBy azureService2 - .listAppV2(userInfo, workspaceId, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "V2 Azure - list apps V2 should return apps for workspace" in isolatedDbTest { - val appName1 = AppName("app1") - val appReq1 = createAppRequest.copy(labels = Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3"), - kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - diskConfig = None - ) - appServiceInterp - .createAppV2(userInfo, workspaceId, appName1, appReq1) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResult = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName1) - } - dbFutureValue(kubernetesClusterQuery.updateStatus(appResult.get.cluster.id, KubernetesClusterStatus.Running)) - - val appName2 = AppName("app2") - val appReq2 = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - - appServiceInterp - .createAppV2(userInfo, workspaceId, appName2, appReq2) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appName3 = AppName("app3") - val appReq3 = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - - appServiceInterp - .createAppV2(userInfo, workspaceId3, appName3, appReq3) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val listProject1Apps = - appServiceInterp - .listAppV2(userInfo, workspaceId, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - listProject1Apps.map(_.appName) should contain(appName1) - listProject1Apps.map(_.appName) should contain(appName2) - listProject1Apps.length shouldBe 2 - - val listProject3Apps = - appServiceInterp - .listAppV2(userInfo, workspaceId3, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - listProject3Apps.length shouldBe 1 - } - - it should "V2 Azure - list apps V2 should fail to return apps for workspace if creator lost workspace access" in isolatedDbTest { - val appName1 = AppName("app1") - val appReq1 = createAppRequest.copy(labels = Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3"), - kubernetesRuntimeConfig = None, - appType = AppType.Cromwell, - diskConfig = None - ) - appServiceInterp - .createAppV2(userInfo, workspaceId, appName1, appReq1) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResult = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName1) - } - dbFutureValue(kubernetesClusterQuery.updateStatus(appResult.get.cluster.id, KubernetesClusterStatus.Running)) - - val listProject1Apps = - appServiceInterp - .listAppV2(userInfo, workspaceId, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - listProject1Apps.length shouldBe 1 - listProject1Apps.map(_.appName) should contain(appName1) - - val azureService2 = makeInterp(QueueFactory.makePublisherQueue(), allowListAuthProvider2) - a[ForbiddenError] should be thrownBy azureService2 - .listAppV2(userInfo, workspaceId, Map()) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "V2 GCP - get app" in isolatedDbTest { - val appName1 = AppName("app1") - val diskName1 = DiskName("newDiskName1") - val createDiskConfig1 = PersistentDiskRequest(diskName1, None, None, Map.empty) - val appReq1 = createAppRequest.copy(labels = Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3"), - diskConfig = Some(createDiskConfig1) - ) - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId, appName1, appReq1) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResult = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName1) - } - dbFutureValue(kubernetesClusterQuery.updateStatus(appResult.get.cluster.id, KubernetesClusterStatus.Running)) - - val appName2 = AppName("app2") - val diskName2 = DiskName("newDiskName2") - val createDiskConfig2 = PersistentDiskRequest(diskName2, None, None, Map.empty) - val appReq2 = createAppRequest.copy(diskConfig = Some(createDiskConfig2)) - - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId, appName2, appReq2) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appName3 = AppName("app3") - val diskName3 = DiskName("newDiskName3") - val createDiskConfig3 = PersistentDiskRequest(diskName3, None, None, Map.empty) - val appReq3 = createAppRequest.copy(diskConfig = Some(createDiskConfig3)) - - gcpWorkspaceAppServiceInterp - .createAppV2(userInfo, workspaceId3, appName3, appReq3) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - // New service where the userInfo is not part of the allowlist and therefore not a workspace reader - val azureService2 = makeInterp(QueueFactory.makePublisherQueue(), allowListAuthProvider2, gcpWsmDao) - - val getApp1 = - appServiceInterp.getAppV2(userInfo, workspaceId, appName1).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - getApp1.diskName shouldBe Some(diskName1) - a[ForbiddenError] should be thrownBy azureService2 - .getAppV2(userInfo, workspaceId, appName1) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val getApp2 = - appServiceInterp.getAppV2(userInfo, workspaceId, appName2).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - getApp2.diskName shouldBe Some(diskName2) - a[ForbiddenError] should be thrownBy azureService2 - .getAppV2(userInfo, workspaceId, appName2) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val getApp3 = - appServiceInterp.getAppV2(userInfo, workspaceId3, appName3).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - getApp3.diskName shouldBe Some(diskName3) - a[ForbiddenError] should be thrownBy azureService2 - .getAppV2(userInfo, workspaceId3, appName3) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "V2 GCP - error creating Galaxy app with an existing disk that was formatted by Cromwell" in isolatedDbTest { - val cluster = makeKubeCluster(0).save() - val nodepool = makeNodepool(1, cluster.id).save() - val cromwellApp = makeApp(1, nodepool.id).save() - val disk = - makePersistentDisk(None, formattedBy = Some(FormattedBy.Cromwell), appRestore = Some(Other(cromwellApp.id))) - .copy(cloudContext = CloudContext.Gcp(GoogleProject(workspaceId.toString))) - .save() - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val galaxyAppName = AppName("galaxy-app1") - val createDiskConfig = PersistentDiskRequest(disk.name, None, None, Map.empty) - val galaxyAppReq = createAppRequest.copy(diskConfig = Some(createDiskConfig)) - - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao, wsmClientProvider = googleWsmClientProvider) - val res = kubeServiceInterp - .createAppV2(userInfo, workspaceId, galaxyAppName, galaxyAppReq) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - res.swap.toOption.get.getMessage shouldBe s"Persistent disk Gcp/${workspaceId}/disk is already formatted by CROMWELL" - } - - it should "V2 Azure - deleteAllApp, update all status appropriately, and queue multiple messages" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue) - val appName = AppName("app1") - val appReq = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - val appName2 = AppName("app2") - val appReq2 = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName2, appReq2) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - val appResultPreStatusUpdate2 = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName2) - } - - // we can't delete while its creating, so set it to Running - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate2.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate2.get.nodepool.id, NodepoolStatus.Running)) - - val appResultPreDelete = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - appResultPreDelete.get.app.status shouldEqual AppStatus.Running - appResultPreDelete.get.app.auditInfo.destroyedDate shouldBe None - - val appResultPreDelete2 = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName2) - } - appResultPreDelete2.get.app.status shouldEqual AppStatus.Running - appResultPreDelete2.get.app.auditInfo.destroyedDate shouldBe None - - kubeServiceInterp - .deleteAllAppsV2(userInfo, workspaceId, false) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val clusterPostDelete = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId), includeDeleted = true) - } - - clusterPostDelete.length shouldEqual 1 - val nodepools = clusterPostDelete.flatMap(_.nodepools) - val apps = clusterPostDelete.flatMap(_.nodepools).flatMap(_.apps) - nodepools.map(_.status) shouldEqual List(NodepoolStatus.Running) - apps.map(_.status) shouldEqual List(AppStatus.Predeleting, AppStatus.Predeleting) - - // throw away create messages - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val messages = publisherQueue.tryTakeN(Some(2)).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - messages.map(_.messageType) shouldBe List(LeoPubsubMessageType.DeleteAppV2, LeoPubsubMessageType.DeleteAppV2) - val deleteAppMessages = messages.map(_.asInstanceOf[DeleteAppV2Message]) - deleteAppMessages.map(_.appId) should contain theSameElementsAs apps.map(_.id) - deleteAppMessages.map(_.workspaceId) shouldBe List(workspaceId, workspaceId) - deleteAppMessages.map(_.diskId) shouldBe List(None, None) - } - - it should "V2 GCP - deleteAllApp, update all status appropriately, and queue multiple messages" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao, wsmClientProvider = googleWsmClientProvider) - - val appName = AppName("app1") - val appName2 = AppName("app2") - val diskConfig1 = PersistentDiskRequest(DiskName("disk1"), None, None, Map.empty) - val diskConfig2 = PersistentDiskRequest(DiskName("disk2"), None, None, Map.empty) - - val appReq = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Galaxy, diskConfig = Some(diskConfig1)) - val appReq2 = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Galaxy, diskConfig = Some(diskConfig2)) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - // we can't delete/create another while its creating, so set it to Running - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - dbFutureValue( - kubernetesClusterQuery.updateStatus(appResultPreStatusUpdate.get.cluster.id, KubernetesClusterStatus.Running) - ) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName2, appReq2) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate2 = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName2) - } - - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate2.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate2.get.nodepool.id, NodepoolStatus.Running)) - dbFutureValue( - kubernetesClusterQuery.updateStatus(appResultPreStatusUpdate2.get.cluster.id, KubernetesClusterStatus.Running) - ) - - val appResultPreDelete = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - appResultPreDelete.get.app.status shouldEqual AppStatus.Running - appResultPreDelete.get.app.auditInfo.destroyedDate shouldBe None - - val appResultPreDelete2 = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName2) - } - appResultPreDelete2.get.app.status shouldEqual AppStatus.Running - appResultPreDelete2.get.app.auditInfo.destroyedDate shouldBe None - - kubeServiceInterp - .deleteAllAppsV2(userInfo, workspaceId, false) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val clusterPostDelete = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId), includeDeleted = true) - } - - clusterPostDelete.length shouldEqual 1 - val nodepools = clusterPostDelete.flatMap(_.nodepools) - val apps = clusterPostDelete.flatMap(_.nodepools).flatMap(_.apps) - nodepools.map(_.status) shouldEqual List(NodepoolStatus.Running) - apps.map(_.status) shouldEqual List(AppStatus.Predeleting, AppStatus.Predeleting) - - // throw away create messages - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val messages = publisherQueue.tryTakeN(Some(2)).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - messages.map(_.messageType) shouldBe List(LeoPubsubMessageType.DeleteAppV2, LeoPubsubMessageType.DeleteAppV2) - val deleteAppMessages = messages.map(_.asInstanceOf[DeleteAppV2Message]) - deleteAppMessages.map(_.appId) shouldBe apps.map(_.id) - deleteAppMessages.map(_.workspaceId) shouldBe List(workspaceId, workspaceId) - deleteAppMessages.map(_.diskId) shouldBe List(None, None) - } - - it should "V2 - deleteAllApp, should fail and not update anything if there is a non-deletable app status appropriately" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue) - val appName = AppName("app1") - val appReq = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - val appName2 = AppName("app2") - val appReq2 = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - kubeServiceInterp - .createAppV2(userInfo, workspaceId, appName2, appReq2) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - val appResultPreStatusUpdate2 = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName2) - } - - // set this one to a non-deletable status - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Provisioning)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - - // set this one to a deletable status - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate2.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate2.get.nodepool.id, NodepoolStatus.Running)) - - val thrown = the[DeleteAllAppsCannotBePerformed] thrownBy { - kubeServiceInterp - .deleteAllAppsV2(userInfo, workspaceId, false) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - val clusterPostDelete = dbFutureValue { - KubernetesServiceDbQueries.listFullAppsByWorkspaceId(Some(workspaceId), includeDeleted = true) - } - - clusterPostDelete.length shouldEqual 1 - val nodepools = clusterPostDelete.flatMap(_.nodepools) - val apps = clusterPostDelete.flatMap(_.nodepools).flatMap(_.apps) - nodepools.map(_.status) shouldEqual List(NodepoolStatus.Running) - apps.map(_.status).toSet shouldEqual Set(AppStatus.Running, AppStatus.Provisioning) - - // throw away create messages - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - publisherQueue.take.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val messages = publisherQueue.tryTakeN(Some(2)).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - messages shouldBe List.empty - } - - /** TODO: Once disks are supported on Azure Apps - it should "error on delete if disk is in a status that cannot be deleted" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val wsmClientProvider = new MockWsmClientProvider() { - override def getDiskState(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[IO, AppContext] - ): IO[WsmState] = - IO.pure(WsmState(Some("CREATING"))) - } - val appServiceInterp = makeInterp(publisherQueue, wsmClientProvider = wsmClientProvider) - - val appName = AppName("app1") - val diskConfig = PersistentDiskRequest(DiskName("disk1"), None, None, Map.empty) - val appReq = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = Some(diskConfig)) - - appServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - - // we can't delete while its creating, so set it to Running - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - - val appResultPreDelete = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - appResultPreDelete.get.app.status shouldEqual AppStatus.Running - appResultPreDelete.get.app.auditInfo.destroyedDate shouldBe None - - an[DiskCannotBeDeletedWsmException] should be thrownBy { - appServiceInterp - .deleteAppV2(userInfo, workspaceId, appName, true) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - */ - - it should "error on delete if app subresource is in a status that cannot be deleted" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - IO.pure(WsmState(Some("CREATING"))) - } - val appServiceInterp = makeInterp(publisherQueue, wsmClientProvider = wsmClientProvider) - - val appName = AppName("app1") - val appReq = - createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.Cromwell, diskConfig = None) - - appServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val appResultPreStatusUpdate = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - - // we can't delete while its creating, so set it to Running - dbFutureValue(appQuery.updateStatus(appResultPreStatusUpdate.get.app.id, AppStatus.Running)) - dbFutureValue(nodepoolQuery.updateStatus(appResultPreStatusUpdate.get.nodepool.id, NodepoolStatus.Running)) - - // add database record - dbFutureValue( - appControlledResourceQuery - .insert( - appResultPreStatusUpdate.get.app.id.id, - wsmResourceId, - WsmResourceType.AzureDatabase, - AppControlledResourceStatus.Creating - ) - ) - - val appResultPreDelete = dbFutureValue { - KubernetesServiceDbQueries.getActiveFullAppByWorkspaceIdAndAppName(workspaceId, appName) - } - appResultPreDelete.get.app.status shouldEqual AppStatus.Running - appResultPreDelete.get.app.auditInfo.destroyedDate shouldBe None - - an[AppResourceCannotBeDeletedException] should be thrownBy { - appServiceInterp - .deleteAppV2(userInfo, workspaceId, appName, true) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail to create a V2 app if it is disabled" in { - val appName = AppName("app1") - val appReq = createAppRequest.copy(kubernetesRuntimeConfig = None, appType = AppType.HailBatch, diskConfig = None) - - val thrown = the[AppTypeNotEnabledException] thrownBy { - appServiceInterp - .createAppV2(userInfo, workspaceId, appName, appReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - thrown.appType shouldBe AppType.HailBatch - } - "checkIfAppCreationIsAllowed" should "enable IntraNodeVisibility if customApp check is disabled" in { val interp = makeInterp(QueueFactory.makePublisherQueue(), enableCustomAppCheckFlag = false) val res = diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockAppService.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockAppService.scala index d027799ea7..d293e929dd 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockAppService.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockAppService.scala @@ -58,28 +58,6 @@ class MockAppService extends AppService[IO] { as: Ask[IO, AppContext] ): IO[Unit] = IO.unit - override def getAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName)(implicit - as: Ask[IO, AppContext] - ): IO[GetAppResponse] = IO.pure(getAppResponse) - - override def listAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, params: Map[String, String])(implicit - as: Ask[IO, AppContext] - ): IO[Vector[ListAppResponse]] = IO.pure(listAppResponse) - - override def createAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName, req: CreateAppRequest)( - implicit as: Ask[IO, AppContext] - ): IO[Unit] = IO.unit - - override def deleteAppV2(userInfo: UserInfo, workspaceId: WorkspaceId, appName: AppName, deleteDisk: Boolean)(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = - IO.unit - - override def deleteAllAppsV2(userInfo: UserInfo, workspaceId: WorkspaceId, deleteDisk: Boolean)(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = - IO.unit - override def updateApp(userInfo: UserInfo, cloudContext: CloudContext.Gcp, appName: AppName, req: UpdateAppRequest)( implicit as: Ask[IO, AppContext] ): IO[Unit] = IO.unit From 95c74e2af3a21adb2540af7dea58b0189e888254 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 3 Jul 2025 10:55:42 -0400 Subject: [PATCH 06/43] Remove runtime v2 routes and runtime v2 service --- .../http/AppDependenciesBuilder.scala | 8 - .../http/BaselineDependenciesBuilder.scala | 10 +- .../dsde/workbench/leonardo/http/Boot.scala | 1 - .../http/CloudDependenciesBuilder.scala | 1 - .../leonardo/http/api/HttpRoutes.scala | 6 +- .../leonardo/http/api/RuntimeV2Routes.scala | 320 --- .../http/service/RuntimeServiceInterp.scala | 3 +- .../http/service/RuntimeV2Service.scala | 88 - .../http/service/RuntimeV2ServiceInterp.scala | 635 ------ .../leonardo/monitor/MonitorAtBoot.scala | 17 +- .../util/AzurePubsubHandlerAlgebra.scala | 45 +- .../leonardo/http/ConfigReaderSpec.scala | 17 +- .../http/service/MockRuntimeV2Interp.scala | 93 - .../service/RuntimeV2ServiceInterpSpec.scala | 1896 ----------------- 14 files changed, 50 insertions(+), 3090 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 450009afe6..46562af924 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -96,13 +96,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu baselineDependencies.samService ) - val azureService = new RuntimeV2ServiceInterp[IO]( - baselineDependencies.runtimeServicesConfig, - baselineDependencies.publisherQueue, - baselineDependencies.dateAccessedUpdaterQueue, - baselineDependencies.wsmClientProvider, - baselineDependencies.samService - ) val adminService = new AdminServiceInterp[IO](baselineDependencies.authProvider, baselineDependencies.publisherQueue) @@ -119,7 +112,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu dependenciesRegistry, diskV2Service, leoKubernetesService, - azureService, adminService, StandardUserInfoDirectives, contentSecurityPolicy, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index fdf4886a34..c1fa1a9fdb 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -37,7 +37,6 @@ import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{HttpSamApiClientProvi import org.broadinstitute.dsde.workbench.leonardo.db.DbReference import org.broadinstitute.dsde.workbench.leonardo.dns._ import org.broadinstitute.dsde.workbench.leonardo.http.service.{ - AzureServiceConfig, RuntimeServiceConfig, SamResourceCacheKey } @@ -284,14 +283,7 @@ class BaselineDependenciesBuilder { imageConfig, autoFreezeConfig, dataprocConfig, - gceConfig, - AzureServiceConfig( - // For now azure disks share same defaults as normal disks - ConfigReader.appConfig.persistentDisk, - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.image, - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.listenerImage, - ConfigReader.appConfig.azure.pubsubHandler.welderImage - ) + gceConfig ) } yield BaselineDependencies[F]( sslContext, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala index 12c947d7df..65f98d064b 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala @@ -64,7 +64,6 @@ object Boot extends IOApp { servicesDependencies.cloudSpecificDependenciesRegistry, servicesDependencies.diskV2Service, servicesDependencies.kubernetesService, - servicesDependencies.azureService, servicesDependencies.adminService, StandardUserInfoDirectives, contentSecurityPolicy, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala index 6edd2110ff..ec4fe27504 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala @@ -74,7 +74,6 @@ final case class ServicesDependencies( cloudSpecificDependenciesRegistry: ServicesRegistry, diskV2Service: DiskV2Service[IO], kubernetesService: AppService[IO], - azureService: RuntimeV2Service[IO], adminService: AdminService[IO], userInfoDirectives: UserInfoDirectives, contentSecurityPolicy: ContentSecurityPolicyConfig, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala index b421cfbaab..c3061c7853 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala @@ -33,7 +33,6 @@ class HttpRoutes( gcpOnlyServicesRegistry: ServicesRegistry, diskV2Service: DiskV2Service[IO], kubernetesService: AppService[IO], - azureService: RuntimeV2Service[IO], adminService: AdminService[IO], userInfoDirectives: UserInfoDirectives, contentSecurityPolicy: ContentSecurityPolicyConfig, @@ -45,7 +44,6 @@ class HttpRoutes( private val corsSupport = new CorsSupport(contentSecurityPolicy, refererConfig) private val kubernetesRoutes = new AppRoutes(kubernetesService, userInfoDirectives) private val appRoutes = createAppRoutesUsingServicesRegistry - private val runtimeV2Routes = new RuntimeV2Routes(refererConfig, azureService, userInfoDirectives) private val diskV2Routes = new DiskV2Routes(diskV2Service, userInfoDirectives) private val adminRoutes = new AdminRoutes(adminService, userInfoDirectives) private val diskRoutes = createDiskRoutesUsingServicesRegistry @@ -123,7 +121,7 @@ class HttpRoutes( "swagger/api-docs.yaml" ) ~ oidcConfig.oauth2Routes ~ proxyRoutes.get.route ~ statusRoutes.route ~ pathPrefix("api") { - runtimeRoutes.get.routes ~ runtimeV2Routes.routes ~ + runtimeRoutes.get.routes ~ diskRoutes.get.routes ~ kubernetesRoutes.routes ~ diskV2Routes.routes ~ adminRoutes.routes ~ resourcesRoutes.get.routes } @@ -133,7 +131,7 @@ class HttpRoutes( oidcConfig .swaggerRoutes("swagger/api-docs.yaml") ~ oidcConfig.oauth2Routes ~ statusRoutes.route ~ pathPrefix("api") { - runtimeRoutes.get.routes ~ runtimeV2Routes.routes ~ + runtimeRoutes.get.routes ~ diskRoutes.get.routes ~ diskV2Routes.routes ~ appRoutes.get.routes ~ adminRoutes.routes } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala deleted file mode 100644 index 3b5f9910c4..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala +++ /dev/null @@ -1,320 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package api - -import akka.http.scaladsl.marshalling.ToResponseMarshallable -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server -import akka.http.scaladsl.server.Directives._ -import cats.effect.IO -import cats.mtl.Ask -import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ -import io.circe.Decoder -import io.opencensus.scala.akka.http.TracingDirective.traceRequestForService -import org.broadinstitute.dsde.workbench.leonardo.config.RefererConfig -import org.broadinstitute.dsde.workbench.leonardo.http.service.RuntimeV2Service -import org.broadinstitute.dsde.workbench.model.UserInfo -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import JsonCodec._ -import RuntimeRoutesCodec._ -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes - -class RuntimeV2Routes(saturnIframeExtensionHostConfig: RefererConfig, - runtimeV2Service: RuntimeV2Service[IO], - userInfoDirectives: UserInfoDirectives -)(implicit - metrics: OpenTelemetryMetrics[IO] -) { - // See https://github.com/DataBiosphere/terra-ui/blob/ef88f396a61383ee08beb65a37af7cae9476cc20/src/libs/ajax.js#L1358 - private val allValidSaturnIframeExtensions = - saturnIframeExtensionHostConfig.validHosts.map(s => s"https://${s}/jupyter-iframe-extension.js") - - val routes: server.Route = traceRequestForService(serviceData) { span => - extractAppContext(Some(span)) { implicit ctx => - userInfoDirectives.requireUserInfo { userInfo => - CookieSupport.setTokenCookie(userInfo) { - pathPrefix("v2" / "runtimes") { - pathEndOrSingleSlash { - parameterMap { params => - get { - complete( - listRuntimesHandler( - userInfo, - None, - None, - params - ) - ) - } - } - } ~ - pathPrefix(workspaceIdSegment) { workspaceId => - pathEndOrSingleSlash { - parameterMap { params => - get { - complete( - listRuntimesHandler( - userInfo, - Some(workspaceId), - None, - params - ) - ) - } - } - } ~ pathPrefix("deleteAll") { - post { - parameterMap { params => - complete( - deleteAllRuntimesForWorkspaceHandler(userInfo, workspaceId, params) - ) - } - } - } ~ pathPrefix(runtimeNameSegmentWithValidation) { runtimeName => - path("stop") { - post { - complete( - stopRuntimeHandler( - userInfo, - workspaceId, - runtimeName - ) - ) - } - } ~ - path("start") { - post { - complete( - startRuntimeHandler( - userInfo, - workspaceId, - runtimeName - ) - ) - } - } ~ - path("updateDateAccessed") { - patch { - complete( - updateDateAccessedHandler( - userInfo, - workspaceId, - runtimeName - ) - ) - } - } - } ~ - pathPrefix("azure") { - pathEndOrSingleSlash { - parameterMap { params => - get { - complete( - listRuntimesHandler( - userInfo, - Some(workspaceId), - Some(CloudProvider.Azure), - params - ) - ) - } - } - } ~ - pathPrefix(runtimeNameSegmentWithValidation) { runtimeName => - pathEndOrSingleSlash { - post { - parameterMap { params => - entity(as[CreateAzureRuntimeRequest]) { req => - complete( - createAzureRuntimeHandler(userInfo, workspaceId, runtimeName, req, params) - ) - } - } - } ~ get { - complete( - getAzureRuntimeHandler(userInfo, workspaceId, runtimeName) - ) - } ~ patch { - entity(as[UpdateAzureRuntimeRequest]) { req => - complete( - updateAzureRuntimeHandler(userInfo, workspaceId, runtimeName, req) - ) - } - } ~ delete { - parameterMap { params => - complete( - deleteAzureRuntimeHandler(userInfo, workspaceId, runtimeName, params) - ) - } - } - } - } - } - } - } - } - } - } - } - - private[api] def createAzureRuntimeHandler(userInfo: UserInfo, - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - req: CreateAzureRuntimeRequest, - params: Map[String, String] - )(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - // if not specified, create new disk - useExistingDisk = params.get("useExistingDisk").exists(_ == "true") - apiCall = runtimeV2Service.createRuntime(userInfo, runtimeName, workspaceId, useExistingDisk, req) - _ <- metrics.incrementCounter("createRuntimeV2") - resp <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "createRuntimeV2") - .use(_ => apiCall) - ) - } yield StatusCodes.Accepted -> resp: ToResponseMarshallable - - private[api] def getAzureRuntimeHandler(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)( - implicit ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = runtimeV2Service.getRuntime(userInfo, runtimeName, workspaceId) - _ <- metrics.incrementCounter("getRuntimeV2") - resp <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "getRuntimeV2") - .use(_ => apiCall) - ) - } yield StatusCodes.OK -> resp: ToResponseMarshallable - - private[api] def startRuntimeHandler(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = runtimeV2Service.startRuntime(userInfo, runtimeName, workspaceId) - _ <- metrics.incrementCounter("startRuntimeV2") - resp <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "startRuntimeV2") - .use(_ => apiCall) - ) - } yield StatusCodes.Accepted -> resp: ToResponseMarshallable - - private[api] def stopRuntimeHandler(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = runtimeV2Service.stopRuntime(userInfo, runtimeName, workspaceId) - _ <- metrics.incrementCounter("stopRuntimeV2") - resp <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "stopRuntimeV2") - .use(_ => apiCall) - ) - } yield StatusCodes.Accepted -> resp: ToResponseMarshallable - - def updateAzureRuntimeHandler(userInfo: UserInfo, - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - req: UpdateAzureRuntimeRequest - )(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = runtimeV2Service.updateRuntime(userInfo, runtimeName, workspaceId, req) - _ <- metrics.incrementCounter("updateRuntimeV2") - _ <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "updateRuntimeV2") - .use(_ => apiCall) - ) - } yield StatusCodes.Accepted: ToResponseMarshallable - - def deleteAzureRuntimeHandler(userInfo: UserInfo, - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - params: Map[String, String] - )(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - // default to true, if `deleteDisk` is explicitly set to false, then we don't delete disk - deleteDisk = !params.get("deleteDisk").exists(_ == "false") - apiCall = runtimeV2Service.deleteRuntime(userInfo, runtimeName, workspaceId, deleteDisk) - tags = Map("deleteDisk" -> deleteDisk.toString) - _ <- metrics.incrementCounter("deleteRuntimeV2", 1, tags) - _ <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "deleteRuntimeV2") - .use(_ => apiCall) - ) - } yield StatusCodes.Accepted: ToResponseMarshallable - - def deleteAllRuntimesForWorkspaceHandler(userInfo: UserInfo, workspaceId: WorkspaceId, params: Map[String, String])( - implicit ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - // default to true, if `deleteDisk` is explicitly set to false, then we don't delete disk - deleteDisk = !params.get("deleteDisk").exists(_ == "false") - apiCall = runtimeV2Service.deleteAllRuntimes(userInfo, workspaceId, deleteDisk) - tags = Map("deleteDisk" -> deleteDisk.toString) - _ <- metrics.incrementCounter("deleteAllRuntimesV2", 1, tags) - _ <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "deleteAllRuntimesV2") - .use(_ => apiCall) - ) - } yield StatusCodes.Accepted: ToResponseMarshallable - - private[api] def listRuntimesHandler(userInfo: UserInfo, - workspaceId: Option[WorkspaceId], - cloudProvider: Option[CloudProvider], - params: Map[String, String] - )(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = runtimeV2Service.listRuntimes(userInfo, workspaceId, cloudProvider, params) - _ <- metrics.incrementCounter("listRuntimeV2") - resp <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "listRuntimeV2") - .use(_ => apiCall) - ) - } yield StatusCodes.OK -> resp: ToResponseMarshallable - - private[api] def updateDateAccessedHandler(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)( - implicit ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = runtimeV2Service.updateDateAccessed(userInfo, workspaceId, runtimeName) - _ <- metrics.incrementCounter("updateDateAccessed") - _ <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "updateDateAccessed") - .use(_ => apiCall) - ) - } yield StatusCodes.Accepted: ToResponseMarshallable - - implicit val createAzureDiskReqDecoder: Decoder[CreateAzureDiskRequest] = - Decoder.forProduct4("labels", "name", "size", "diskType")(CreateAzureDiskRequest.apply) - - implicit val createAzureRuntimeRequestDecoder: Decoder[CreateAzureRuntimeRequest] = Decoder.instance { c => - for { - labels <- c.downField("labels").as[LabelMap] - machineSize <- c.downField("machineSize").as[VirtualMachineSizeTypes] - customEnvVars <- c - .downField("customEnvironmentVariables") - .as[Option[Map[String, String]]] - azureDiskReq <- c.downField("disk").as[CreateAzureDiskRequest] - apt <- c.downField("autopauseThreshold").as[Option[Int]] - } yield CreateAzureRuntimeRequest(labels, machineSize, customEnvVars.getOrElse(Map.empty), azureDiskReq, apt) - } - - implicit val updateAzureRuntimeRequestDecoder: Decoder[UpdateAzureRuntimeRequest] = - Decoder.forProduct1("machineSize")(UpdateAzureRuntimeRequest.apply) - -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterp.scala index 6b7730b226..619c3b76c7 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterp.scala @@ -1164,8 +1164,7 @@ final case class RuntimeServiceConfig( imageConfig: ImageConfig, autoFreezeConfig: AutoFreezeConfig, dataprocConfig: DataprocConfig, - gceConfig: GceConfig, - azureConfig: AzureServiceConfig + gceConfig: GceConfig ) final case class WrongCloudServiceException( diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala deleted file mode 100644 index 3ff80c1801..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala +++ /dev/null @@ -1,88 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.leonardo.config.PersistentDiskConfig -import org.broadinstitute.dsde.workbench.model.UserInfo - -//TODO: all functions but non-workspace-specific list are currently azure-specific -trait RuntimeV2Service[F[_]] { - def createRuntime(userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - useExistingDisk: Boolean, - req: CreateAzureRuntimeRequest - )(implicit - as: Ask[F, AppContext] - ): F[CreateRuntimeResponse] - - def getRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[F, AppContext] - ): F[GetRuntimeResponse] - - def startRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[F, AppContext] - ): F[Unit] - - def stopRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[F, AppContext] - ): F[Unit] - - def updateRuntime(userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - req: UpdateAzureRuntimeRequest - )(implicit - as: Ask[F, AppContext] - ): F[Unit] - - def deleteRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId, deleteDisk: Boolean)( - implicit as: Ask[F, AppContext] - ): F[Unit] - - def deleteAllRuntimes(userInfo: UserInfo, workspaceId: WorkspaceId, deleteDisk: Boolean)(implicit - as: Ask[F, AppContext] - ): F[Unit] - - def listRuntimes(userInfo: UserInfo, - workspaceId: Option[WorkspaceId], - cloudProvider: Option[CloudProvider], - params: Map[String, String] - )(implicit - as: Ask[F, AppContext] - ): F[Vector[ListRuntimeResponse2]] - - def updateDateAccessed(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)(implicit - as: Ask[F, AppContext] - ): F[Unit] -} - -final case class CustomScriptExtensionConfig(name: String, - publisher: String, - `type`: String, - version: String, - minorVersionAutoUpgrade: Boolean, - fileUris: List[String] -) -final case class AzureServiceConfig(diskConfig: PersistentDiskConfig, - image: AzureImage, - listenerImage: String, - welderImage: String -) -final case class VMCredential(username: String, password: String) - -final case class AzureRuntimeDefaults(ipControlledResourceDesc: String, - ipNamePrefix: String, - networkControlledResourceDesc: String, - networkNamePrefix: String, - subnetNamePrefix: String, - addressSpaceCidr: CidrIP, - subnetAddressCidr: CidrIP, - diskControlledResourceDesc: String, - vmControlledResourceDesc: String, - image: AzureImage, - customScriptExtension: CustomScriptExtensionConfig, - listenerImage: String, - vmCredential: VMCredential -) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala deleted file mode 100644 index 76c25be952..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala +++ /dev/null @@ -1,635 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import akka.http.scaladsl.model.StatusCodes -import cats.Parallel -import cats.effect.Async -import cats.effect.std.Queue -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.google2.{DiskName, MachineTypeName, ZoneName} -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.{ - PersistentDiskSamResourceId, - RuntimeSamResourceId, - WorkspaceResourceSamResourceId, - WsmResourceSamResourceId -} -import org.broadinstitute.dsde.workbench.leonardo.config.PersistentDiskConfig -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamException, SamService, SamUtils} -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.DiskServiceInterp.getDiskSamPolicyMap -import org.broadinstitute.dsde.workbench.leonardo.http.service.RuntimeServiceInterp.getRuntimeSamPolicyMap -import org.broadinstitute.dsde.workbench.leonardo.model.SamResource.RuntimeSamResource -import org.broadinstitute.dsde.workbench.leonardo.model._ -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAzureRuntimeMessage, - DeleteAzureRuntimeMessage, - StartRuntimeMessage, - StopRuntimeMessage -} -import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoPubsubMessage, UpdateDateAccessedMessage, UpdateTarget} -import org.broadinstitute.dsde.workbench.model.{TraceId, UserInfo, WorkbenchEmail} -import org.typelevel.log4cats.StructuredLogger - -import java.time.Instant -import java.util.UUID -import scala.concurrent.ExecutionContext - -class RuntimeV2ServiceInterp[F[_]: Parallel]( - config: RuntimeServiceConfig, - publisherQueue: Queue[F, LeoPubsubMessage], - dateAccessUpdaterQueue: Queue[F, UpdateDateAccessedMessage], - wsmClientProvider: WsmApiClientProvider[F], - samService: SamService[F] -)(implicit F: Async[F], dbReference: DbReference[F], ec: ExecutionContext, log: StructuredLogger[F]) - extends RuntimeV2Service[F] { - - override def createRuntime( - userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - useExistingDisk: Boolean, - req: CreateAzureRuntimeRequest - )(implicit as: Ask[F, AppContext]): F[CreateRuntimeResponse] = - for { - ctx <- as.ask - - _ <- samService - .checkAuthorized(userInfo.accessToken.token, - WorkspaceResourceSamResourceId(workspaceId), - WorkspaceAction.Compute - ) - .adaptError { - case e: SamException if e.statusCode == StatusCodes.Forbidden => ForbiddenError(userInfo.userEmail) - } - _ <- ctx.span.traverse(s => F.delay(s.addAnnotation("Done auth call for azure runtime permission"))) - - workspaceDescOpt <- wsmClientProvider.getWorkspace(userInfo.accessToken.token, workspaceId) - workspaceDesc <- F.fromOption(workspaceDescOpt, WorkspaceNotFoundException(workspaceId, ctx.traceId)) - - // TODO: when we fully support google here, do something intelligent instead of defaulting to azure - cloudContext <- (workspaceDesc.azureContext, workspaceDesc.gcpContext) match { - case (Some(azureContext), _) => F.pure[CloudContext](CloudContext.Azure(azureContext)) - case (_, Some(gcpContext)) => F.pure[CloudContext](CloudContext.Gcp(gcpContext)) - case (None, None) => F.raiseError[CloudContext](CloudContextNotFoundException(workspaceId, ctx.traceId)) - } - - // Resolve the user email in Sam from the user token. This translates a pet token to the owner email. - userEmail <- samService.getUserEmail(userInfo.accessToken.token) - - // enforcing one runtime per workspace/user at a time - samResources <- samService.listResources(userInfo.accessToken.token, RuntimeSamResource.resourceType) - runtimes <- RuntimeServiceDbQueries - .listRuntimes( - runtimeIds = samResources.map(RuntimeSamResourceId).toSet, - cloudProvider = Some(cloudContext.cloudProvider), - creatorEmail = Some(userEmail), - excludeStatuses = List(RuntimeStatus.Deleted, RuntimeStatus.Deleting), - workspaceId = Some(workspaceId) - ) - .transaction - - _ <- F - .raiseError( - OnlyOneRuntimePerWorkspacePerCreator( - workspaceId, - userEmail, - runtimes.head.clusterName, - runtimes.head.status - ) - ) - .whenA(runtimes.length != 0) - - runtimeOpt <- RuntimeServiceDbQueries.getStatusByName(cloudContext, runtimeName).transaction - _ <- ctx.span.traverse(s => F.delay(s.addAnnotation("Done DB query for azure runtime"))) - - runtimeImage: RuntimeImage = RuntimeImage( - RuntimeImageType.Azure, - config.azureConfig.image.asString, - None, - ctx.now - ) - listenerImage: RuntimeImage = RuntimeImage( - RuntimeImageType.Listener, - config.azureConfig.listenerImage, - None, - ctx.now - ) - welderImage: RuntimeImage = RuntimeImage( - RuntimeImageType.Welder, - config.azureConfig.welderImage, - None, - ctx.now - ) - - _ <- runtimeOpt match { - case Some(status) => F.raiseError[Unit](RuntimeAlreadyExistsException(cloudContext, runtimeName, status)) - case None => - for { - diskId <- useExistingDisk match { - - // if using existing disk, find disk in users workspace - case true => - for { - disks <- DiskServiceDbQueries - .listDisks( - Map.empty, - Some(userEmail), - Some(cloudContext), - Some(workspaceId) - ) - .transaction - // check if 0 or multiple disks - disk <- disks.length match { - case 1 => F.pure(disks.head) - case 0 => F.raiseError(NoPersistentDiskException(workspaceId)) - case _ => - F.raiseError(MultiplePersistentDisksException(workspaceId, disks.length, disks)) - } - // check disk is ready - _ <- F - .raiseError(PersistentDiskNotReadyException(disk.id, disk.status)) - .whenA(disk.status != DiskStatus.Ready) - isAttached <- persistentDiskQuery.isDiskAttached(disk.id).transaction - _ <- F - .raiseError(DiskAlreadyAttachedException(disk.cloudContext, disk.name, ctx.traceId)) - .whenA(isAttached) - } yield disk.id - - // if not using existing disk, create a new one - case false => - for { - samResource <- F.delay(PersistentDiskSamResourceId(UUID.randomUUID().toString)) - pd = - convertToDisk( - userEmail, - cloudContext, - DiskName(req.azureDiskConfig.name.value), - samResource, - config.azureConfig.diskConfig, - req, - workspaceId, - ctx.now - ) - // Create a persistent-disk Sam resource with a creator policy and the workspace as the parent - _ <- samService.createResource(userInfo.accessToken.token, - samResource, - None, - Some(workspaceId), - getDiskSamPolicyMap(userEmail) - ) - disk <- persistentDiskQuery.save(pd).transaction - } yield disk.id - } - - samResource <- F.delay(RuntimeSamResourceId(UUID.randomUUID().toString)) - runtime = convertToRuntime( - workspaceId, - runtimeName, - cloudContext, - userEmail, - req, - samResource, - Set(runtimeImage, listenerImage, welderImage), - Set.empty, - ctx.now - ) - - runtimeConfig = RuntimeConfig.AzureConfig( - MachineTypeName(req.machineSize.toString), - Some(diskId), - None - ) - runtimeToSave = SaveCluster(cluster = runtime, runtimeConfig = runtimeConfig, now = ctx.now) - - // Create a notebook-cluster Sam resource with a creator policy and the workspace as the parent - _ <- samService.createResource(userInfo.accessToken.token, - samResource, - None, - Some(workspaceId), - getRuntimeSamPolicyMap(userEmail) - ) - - savedRuntime <- clusterQuery.save(runtimeToSave).transaction - _ <- publisherQueue.offer( - CreateAzureRuntimeMessage( - savedRuntime.id, - workspaceId, - useExistingDisk, - Some(ctx.traceId), - workspaceDesc.displayName, - BillingProfileId(workspaceDesc.spendProfile) - ) - ) - } yield () - } - - } yield CreateRuntimeResponse(ctx.traceId) - - override def getRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[F, AppContext] - ): F[GetRuntimeResponse] = - for { - ctx <- as.ask - - runtime <- RuntimeServiceDbQueries.getRuntimeByWorkspaceId(workspaceId, runtimeName).transaction - _ <- SamUtils.checkRuntimeAction(samService, - userInfo, - workspaceId, - runtimeName, - runtime.samResource, - RuntimeAction.GetRuntimeStatus - ) - _ <- ctx.span.traverse(s => F.delay(s.addAnnotation("Done auth call for get azure runtime permission"))) - } yield runtime - - override def updateRuntime( - userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - req: UpdateAzureRuntimeRequest - )(implicit as: Ask[F, AppContext]): F[Unit] = - F.pure(AzureUnimplementedException("patch not implemented yet")) - - override def deleteRuntime( - userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - deleteDisk: Boolean - )(implicit as: Ask[F, AppContext]): F[Unit] = - for { - ctx <- as.ask - - runtime <- getClusterRecordWithRequiredAction(userInfo, workspaceId, runtimeName, RuntimeAction.DeleteRuntime) - - diskIdOpt <- RuntimeConfigQueries.getDiskId(runtime.runtimeConfigId).transaction - diskId <- diskIdOpt match { - case Some(value) => F.pure(value) - case _ => - F.raiseError[DiskId]( - AzureRuntimeHasInvalidRuntimeConfig(runtime.cloudContext, runtime.runtimeName, ctx.traceId) - ) - } - - // check if the VM is deletable in WSM - wsmResourceId = WsmControlledResourceId(UUID.fromString(runtime.internalId)) - wsmState <- wsmClientProvider.getWsmState(userInfo.accessToken.token, - workspaceId, - wsmResourceId, - WsmResourceType.AzureVm - ) - _ <- F - .raiseUnless(wsmState.isDeletable)( - RuntimeCannotBeDeletedWsmException(runtime.cloudContext, runtime.runtimeName, wsmState) - ) - - // pass the disk to delete to publisher and set Leo status (if deleting disk) - diskIdToDeleteOpt <- - if (deleteDisk) for { - // check if disk is deletable in WSM if disk is being deleted - diskOpt <- persistentDiskQuery.getById(diskId).transaction - disk <- diskOpt.fold(F.raiseError[PersistentDisk](DiskNotFoundByIdException(diskId, ctx.traceId)))(F.pure) - diskIdToDelete <- - if (disk.wsmResourceId.isDefined && disk.status.isDeletable) { - for { - wsmState <- wsmClientProvider.getWsmState(userInfo.accessToken.token, - workspaceId, - wsmResourceId, - WsmResourceType.AzureDisk - ) - _ <- F - .raiseUnless(wsmState.isDeletable)( - DiskCannotBeDeletedWsmException(disk.id, wsmState, disk.cloudContext, ctx.traceId) - ) - _ <- persistentDiskQuery.markPendingDeletion(diskId, ctx.now).transaction - } yield if (wsmState.isDeleted) None else Some(diskId) - } else F.pure(none[DiskId]) - } yield diskIdToDelete - else F.pure(none[DiskId]) - - // only pass wsmResourceId if vm isn't already deleted in WSM - // won't send the delete to WSM if vm is deleted - wsmVMResourceSamId = if (wsmState.isDeleted) None else Some(wsmResourceId) - - // Query WSM for Landing Zone resources - workspaceDescOpt <- wsmClientProvider.getWorkspace(userInfo.accessToken.token, workspaceId) - workspaceDesc <- F.fromOption(workspaceDescOpt, WorkspaceNotFoundException(workspaceId, ctx.traceId)) - - // Update DB record to Deleting status - _ <- clusterQuery.markPendingDeletion(runtime.id, ctx.now).transaction - - _ <- publisherQueue.offer( - DeleteAzureRuntimeMessage( - runtime.id, - diskIdToDeleteOpt, - workspaceId, - wsmVMResourceSamId, - BillingProfileId(workspaceDesc.spendProfile), - Some(ctx.traceId) - ) - ) - } yield () - - override def deleteAllRuntimes(userInfo: UserInfo, workspaceId: WorkspaceId, deleteDisk: Boolean)(implicit - as: Ask[F, AppContext] - ): F[Unit] = - for { - samResources <- samService.listResources(userInfo.accessToken.token, RuntimeSamResource.resourceType) - runtimes <- RuntimeServiceDbQueries - .listRuntimes( - runtimeIds = samResources.map(RuntimeSamResourceId).toSet, - excludeStatuses = List(RuntimeStatus.Deleted), - workspaceId = Some(workspaceId) - ) - .map(_.toList) - .transaction - - nonDeletableRuntimes = runtimes.filterNot(r => r.status.isDeletable) - - _ <- - if (nonDeletableRuntimes.isEmpty) - runtimes - .map(r => r.clusterName) - .traverse(runtime_name => deleteRuntime(userInfo, runtime_name, workspaceId, deleteDisk)) - else - // Error out if any runtime is in a non deletable state - F.raiseError[Unit]( - NonDeletableRuntimesInWorkspaceFoundException( - workspaceId, - s"${nonDeletableRuntimes.map(r => r.clusterName)}" - ) - ) - } yield () - - override def updateDateAccessed(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)(implicit - as: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- as.ask - runtime <- RuntimeServiceDbQueries.getRuntimeByWorkspaceId(workspaceId, runtimeName).transaction - - _ <- SamUtils.checkRuntimeAction( - samService, - userInfo, - workspaceId, - runtimeName, - WsmResourceSamResourceId(WsmControlledResourceId(UUID.fromString(runtime.samResource.resourceId))), - RuntimeAction.ModifyRuntime - ) - - _ <- dateAccessUpdaterQueue.offer( - UpdateDateAccessedMessage(UpdateTarget.Runtime(runtimeName), runtime.cloudContext, ctx.now) - ) >> - log.info(s"Queued message to update dateAccessed for runtime ${runtime.cloudContext}/$runtimeName") - } yield () - - def startRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- as.ask - runtime <- getClusterRecordWithRequiredAction(userInfo, workspaceId, runtimeName, RuntimeAction.StopStartRuntime) - _ <- - if (runtime.status.isStartable) F.unit - else - F.raiseError[Unit](RuntimeCannotBeStartedException(runtime.cloudContext, runtime.runtimeName, runtime.status)) - _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.PreStarting, ctx.now).transaction - _ <- publisherQueue.offer(StartRuntimeMessage(runtime.id, Some(ctx.traceId))) - } yield () - - def stopRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- as.ask - - runtime <- getClusterRecordWithRequiredAction(userInfo, workspaceId, runtimeName, RuntimeAction.StopStartRuntime) - _ <- - if (runtime.status.isStoppable) F.unit - else - F.raiseError[Unit](RuntimeCannotBeStoppedException(runtime.cloudContext, runtime.runtimeName, runtime.status)) - _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.PreStopping, ctx.now).transaction - _ <- publisherQueue.offer(StopRuntimeMessage(runtime.id, Some(ctx.traceId))) - } yield () - - override def listRuntimes( - userInfo: UserInfo, - workspaceId: Option[WorkspaceId], - cloudProvider: Option[CloudProvider], - params: Map[String, String] - )(implicit as: Ask[F, AppContext]): F[Vector[ListRuntimeResponse2]] = - for { - ctx <- as.ask - - // Parameters: parse search filters from request - (labelMap, _, _) <- F.fromEither(processListParameters(params)) - excludeStatuses = List(RuntimeStatus.Deleted) - creatorEmail <- F.fromEither(processCreatorOnlyParameter(userInfo.userEmail, params, ctx.traceId)) - - samResources <- samService.listResources(userInfo.accessToken.token, RuntimeSamResource.resourceType) - runtimes <- RuntimeServiceDbQueries - .listRuntimes( - runtimeIds = samResources.map(RuntimeSamResourceId).toSet, - cloudProvider = cloudProvider, - creatorEmail = creatorEmail, - excludeStatuses = excludeStatuses, - labelMap = labelMap, - workspaceId = workspaceId - ) - .map(_.toList) - .transaction - - } yield runtimes.toVector - - private[service] def convertToDisk( - userEmail: WorkbenchEmail, - cloudContext: CloudContext, - diskName: DiskName, - samResource: PersistentDiskSamResourceId, - config: PersistentDiskConfig, - req: CreateAzureRuntimeRequest, - workspaceId: WorkspaceId, - now: Instant - ): PersistentDisk = { - // create a LabelMap of default labels - val defaultLabelMap: LabelMap = - Map( - "diskName" -> diskName.value, - "cloudContext" -> cloudContext.asString, - "creator" -> userEmail.value - ) - - // combine default and given labels - val allLabels = req.azureDiskConfig.labels ++ defaultLabelMap - - PersistentDisk( - DiskId(0), - cloudContext, - ZoneName("unset"), - diskName, - userEmail, - samResource, - DiskStatus.Creating, - AuditInfo(userEmail, now, None, now), - req.azureDiskConfig.size.getOrElse(config.defaultDiskSizeGb), - req.azureDiskConfig.diskType.getOrElse(config.defaultDiskType), - config.defaultBlockSizeBytes, - None, - None, - allLabels, - None, - None, - Some(workspaceId) - ) - } - - private def getClusterRecordWithRequiredAction( - userInfo: UserInfo, - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - action: RuntimeAction - )(implicit as: Ask[F, AppContext]): F[ClusterRecord] = - for { - runtime <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName).transaction - _ <- SamUtils.checkRuntimeAction(samService, - userInfo, - workspaceId, - runtimeName, - RuntimeSamResourceId(runtime.internalId), - action - ) - } yield runtime - - private def errorHandler(runtimeId: Long, ctx: AppContext): Throwable => F[Unit] = - e => - clusterErrorQuery - .save(runtimeId, RuntimeError(e.getMessage, None, ctx.now, Some(ctx.traceId))) - .transaction >> - clusterQuery.updateClusterStatus(runtimeId, RuntimeStatus.Error, ctx.now).transaction.void - - private def convertToRuntime( - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - cloudContext: CloudContext, - userEmail: WorkbenchEmail, - request: CreateAzureRuntimeRequest, - samResourceId: RuntimeSamResourceId, - runtimeImages: Set[RuntimeImage], - scopes: Set[String], - now: Instant - ): Runtime = { - // create a LabelMap of default labels - val defaultLabels = DefaultRuntimeLabels( - runtimeName, - None, - cloudContext, - userEmail, - // TODO: use an azure service account - Some(userEmail), - None, - None, - // TODO: Will need to be updated when we support RStudio on Azure or JupyterLab on GCP V2 endpoint - Some(Tool.JupyterLab) - ).toMap - - val allLabels = request.labels ++ defaultLabels - - Runtime( - 0, - Some(workspaceId), - samResource = samResourceId, - runtimeName = runtimeName, - cloudContext = cloudContext, - // TODO: use an azure service account - serviceAccount = userEmail, - asyncRuntimeFields = None, - auditInfo = AuditInfo(userEmail, now, None, now), - kernelFoundBusyDate = None, - proxyUrl = Runtime.getProxyUrl(config.proxyUrlBase, cloudContext, runtimeName, runtimeImages, None, allLabels), - status = RuntimeStatus.PreCreating, - labels = allLabels, - userScriptUri = None, - startUserScriptUri = None, - errors = List.empty, - userJupyterExtensionConfig = None, - autopauseThreshold = - request.autopauseThreshold.getOrElse(0), // TODO: default to 30 once we start supporting autopause - defaultClientId = None, - allowStop = false, - runtimeImages = runtimeImages, - scopes = scopes, - welderEnabled = true, - customEnvironmentVariables = request.customEnvironmentVariables, - runtimeConfigId = RuntimeConfigId(-1), - patchInProgress = false - ) - } - -} - -final case class WorkspaceNotFoundException(workspaceId: WorkspaceId, traceId: TraceId) - extends LeoException( - s"WorkspaceId not found in workspace manager for workspace ${workspaceId}", - StatusCodes.NotFound, - traceId = Some(traceId) - ) - -final case class CloudContextNotFoundException(workspaceId: WorkspaceId, traceId: TraceId) - extends LeoException( - s"Cloud context not found in workspace manager for workspace ${workspaceId}", - StatusCodes.NotFound, - traceId = Some(traceId) - ) - -final case class AzureRuntimeControlledResourceNotFoundException( - cloudContext: CloudContext, - runtimeName: RuntimeName, - traceId: TraceId -) extends LeoException( - s"Controlled resource record not found for runtime ${cloudContext.asStringWithProvider}/${runtimeName.asString}", - StatusCodes.NotFound, - traceId = Some(traceId) - ) - -final case class AzureRuntimeHasInvalidRuntimeConfig( - cloudContext: CloudContext, - runtimeName: RuntimeName, - traceId: TraceId -) extends LeoException( - s"Azure runtime ${cloudContext.asStringWithProvider}/${runtimeName.asString} was found with an invalid runtime config", - StatusCodes.InternalServerError, - traceId = Some(traceId) - ) - -case class MultiplePersistentDisksException(workspaceId: WorkspaceId, numDisks: Int, disks: List[PersistentDisk]) - extends LeoException( - s"Workspace: ${workspaceId.value} contains ${numDisks} persistent disks, must have only 1. Current PDs: ${disks - .map(disks => s"(${disks.name.value},${disks.id.value})")}. Runtime cannot be created with an existing disk ", - StatusCodes.PreconditionFailed, - traceId = None - ) - -case class NoPersistentDiskException(workspaceId: WorkspaceId) - extends LeoException( - s"Workspace: ${workspaceId.value} does not contain any persistent disks. Runtime cannot be created with an existing disk", - StatusCodes.PreconditionFailed, - traceId = None - ) - -case class PersistentDiskNotReadyException(diskId: DiskId, diskStatus: DiskStatus) - extends LeoException( - s"Existing disk: ${diskId.value} has status ${diskStatus}. Runtime cannot be created with an existing disk", - StatusCodes.PreconditionFailed, - traceId = None - ) - -case class OnlyOneRuntimePerWorkspacePerCreator( - workspaceId: WorkspaceId, - creator: WorkbenchEmail, - runtime: RuntimeName, - status: RuntimeStatus -) extends LeoException( - s"There is already an active runtime ${runtime.asString} in this workspace ${workspaceId.value} created by user ${creator.value} with the status ${status}. New runtime cannot be created until this one is deleted", - StatusCodes.PreconditionFailed, - traceId = None - ) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala index 91f20b03a1..35837a50f3 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala @@ -1,6 +1,7 @@ package org.broadinstitute.dsde.workbench.leonardo package monitor +import akka.http.scaladsl.model.StatusCodes import cats.effect.Async import cats.effect.std.Queue import cats.mtl.Ask @@ -10,14 +11,8 @@ import org.broadinstitute.dsde.workbench.google2.{GoogleComputeService, ZoneName import org.broadinstitute.dsde.workbench.leonardo.dao.{SamDAO, WsmApiClientProvider} import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.http._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.WorkspaceNotFoundException import org.broadinstitute.dsde.workbench.leonardo.model.LeoException -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAppMessage, - CreateAppV2Message, - DeleteAppMessage, - DeleteAppV2Message -} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, CreateAppV2Message, DeleteAppMessage, DeleteAppV2Message} import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics import org.typelevel.log4cats.Logger @@ -25,6 +20,14 @@ import org.typelevel.log4cats.Logger import java.util.UUID import scala.concurrent.ExecutionContext +// TODO delete with Azure code +final case class WorkspaceNotFoundException(workspaceId: WorkspaceId, traceId: TraceId) + extends LeoException( + s"WorkspaceId not found in workspace manager for workspace ${workspaceId}", + StatusCodes.NotFound, + traceId = Some(traceId) + ) + class MonitorAtBoot[F[_]](publisherQueue: Queue[F, LeoPubsubMessage], computeService: Option[GoogleComputeService[F]], samDAO: SamDAO[F], diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala index f529d0d643..f3181eb8b0 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala @@ -4,20 +4,11 @@ package util import cats.mtl.Ask import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, ContainerName} import org.broadinstitute.dsde.workbench.leonardo.WsmControlledResourceId +import org.broadinstitute.dsde.workbench.leonardo.config.PersistentDiskConfig import org.broadinstitute.dsde.workbench.leonardo.dao.{CreateDiskForRuntimeResult, StorageContainerResponse} -import org.broadinstitute.dsde.workbench.leonardo.http.service.AzureRuntimeDefaults -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAzureRuntimeMessage, - DeleteAzureRuntimeMessage, - DeleteDiskV2Message -} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAzureRuntimeMessage, DeleteAzureRuntimeMessage, DeleteDiskV2Message} import org.broadinstitute.dsde.workbench.leonardo.monitor.PollMonitorConfig -import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError.{ - AzureRuntimeCreationError, - AzureRuntimeDeletionError, - AzureRuntimeStartingError, - AzureRuntimeStoppingError -} +import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError.{AzureRuntimeCreationError, AzureRuntimeDeletionError, AzureRuntimeStartingError, AzureRuntimeStoppingError} import org.broadinstitute.dsp.ChartVersion import org.http4s.Uri @@ -138,6 +129,36 @@ final case class CreateStorageContainerResourcesResult(containerName: ContainerN resourceId: WsmControlledResourceId ) +final case class CustomScriptExtensionConfig(name: String, + publisher: String, + `type`: String, + version: String, + minorVersionAutoUpgrade: Boolean, + fileUris: List[String] + ) + +final case class AzureServiceConfig(diskConfig: PersistentDiskConfig, + image: AzureImage, + listenerImage: String, + welderImage: String + ) +final case class VMCredential(username: String, password: String) + +final case class AzureRuntimeDefaults(ipControlledResourceDesc: String, + ipNamePrefix: String, + networkControlledResourceDesc: String, + networkNamePrefix: String, + subnetNamePrefix: String, + addressSpaceCidr: CidrIP, + subnetAddressCidr: CidrIP, + diskControlledResourceDesc: String, + vmControlledResourceDesc: String, + image: AzureImage, + customScriptExtension: CustomScriptExtensionConfig, + listenerImage: String, + vmCredential: VMCredential + ) + final case class AzurePubsubHandlerConfig(samUrl: Uri, wsmUrl: Uri, welderAcrUri: String, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala index 43e505ddb9..d3d47327e7 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala @@ -2,24 +2,13 @@ package org.broadinstitute.dsde.workbench.leonardo package http import com.azure.core.management.AzureEnvironment -import org.broadinstitute.dsde.workbench.azure.{ - AzureAppRegistrationConfig, - AzureServiceBusPublisherConfig, - AzureServiceBusSubscriberConfig, - ClientId, - ClientSecret, - ManagedAppTenantId -} +import org.broadinstitute.dsde.workbench.azure.{AzureAppRegistrationConfig, AzureServiceBusPublisherConfig, AzureServiceBusSubscriberConfig, ClientId, ClientSecret, ManagedAppTenantId} import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName import org.broadinstitute.dsde.workbench.google2.ZoneName import org.broadinstitute.dsde.workbench.leonardo.config._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.{ - AzureRuntimeDefaults, - CustomScriptExtensionConfig, - VMCredential -} +import org.broadinstitute.dsde.workbench.leonardo.http.service.{AzureRuntimeDefaults, CustomScriptExtensionConfig, VMCredential} import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoMetricsMonitorConfig, PollMonitorConfig} -import org.broadinstitute.dsde.workbench.leonardo.util.{AzurePubsubHandlerConfig, TerraAppSetupChartConfig} +import org.broadinstitute.dsde.workbench.leonardo.util.{AzurePubsubHandlerConfig, AzureRuntimeDefaults, CustomScriptExtensionConfig, TerraAppSetupChartConfig, VMCredential} import org.broadinstitute.dsp._ import org.http4s.Uri import org.scalatest.flatspec.AnyFlatSpec diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala deleted file mode 100644 index 4412e46772..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala +++ /dev/null @@ -1,93 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import cats.effect.IO -import cats.mtl.Ask -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes -import org.broadinstitute.dsde.workbench.google2.MachineTypeName -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.model.UserInfo - -class MockRuntimeV2Interp extends RuntimeV2Service[IO] { - override def createRuntime(userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - useExistingDisk: Boolean, - req: CreateAzureRuntimeRequest - )(implicit as: Ask[IO, AppContext]): IO[CreateRuntimeResponse] = for { - ctx <- as.ask[AppContext] - } yield CreateRuntimeResponse(ctx.traceId) - - override def getRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[IO, AppContext] - ): IO[GetRuntimeResponse] = - IO.pure( - GetRuntimeResponse - .fromRuntime(CommonTestData.testCluster, - RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A0.toString), - Some(DiskId(-1)), - None - ), - None - ) - .copy(clusterName = RuntimeName("azureruntime1")) - ) - - override def updateRuntime(userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - req: UpdateAzureRuntimeRequest - )(implicit as: Ask[IO, AppContext]): IO[Unit] = IO.pure() - - override def deleteRuntime(userInfo: UserInfo, - runtimeName: RuntimeName, - workspaceId: WorkspaceId, - deleteDisk: Boolean - )(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = IO.pure() - - override def deleteAllRuntimes(userInfo: UserInfo, workspaceId: WorkspaceId, deleteDisk: Boolean)(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = IO.pure() - - override def listRuntimes( - userInfo: UserInfo, - workspaceId: Option[WorkspaceId], - cloudProvider: Option[CloudProvider], - params: Map[String, String] - )(implicit as: Ask[IO, AppContext]): IO[Vector[ListRuntimeResponse2]] = - IO.pure( - Vector( - ListRuntimeResponse2( - CommonTestData.testCluster.id, - Some(CommonTestData.workspaceId), - CommonTestData.testCluster.samResource, - RuntimeName("azureruntime1"), - CloudContext.Azure(azureCloudContext), - CommonTestData.testCluster.auditInfo, - RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A0.toString), - Some(DiskId(-1)), - None - ), - CommonTestData.testCluster.proxyUrl, - CommonTestData.testCluster.status, - CommonTestData.testCluster.labels, - CommonTestData.testCluster.patchInProgress - ) - ) - ) - - override def startRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = IO.unit - - override def stopRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = IO.unit - - override def updateDateAccessed(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = IO.unit -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala deleted file mode 100644 index 950ddd4d9f..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala +++ /dev/null @@ -1,1896 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.model.headers.OAuth2BearerToken -import cats.effect.IO -import cats.effect.std.Queue -import cats.mtl.Ask -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes -import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.google2.DiskName -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.{ - RuntimeSamResourceId, - WorkspaceResourceSamResourceId, - WsmResourceSamResourceId -} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.Config -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamException, SamService} -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.model.SamResource.RuntimeSamResource -import org.broadinstitute.dsde.workbench.leonardo.model._ -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAzureRuntimeMessage, - DeleteAzureRuntimeMessage, - StartRuntimeMessage, - StopRuntimeMessage -} -import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoPubsubMessage, UpdateDateAccessedMessage, UpdateTarget} -import org.broadinstitute.dsde.workbench.leonardo.util.QueueFactory -import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.broadinstitute.dsde.workbench.model.{TraceId, UserInfo, WorkbenchEmail, WorkbenchUserId} -import org.mockito.ArgumentMatchers.{any, eq => isEq} -import org.mockito.Mockito.when -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatestplus.mockito.MockitoSugar -import org.typelevel.log4cats.StructuredLogger - -import java.util.UUID -import scala.concurrent.ExecutionContext.Implicits.global - -class RuntimeV2ServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { - - val serviceConfig = RuntimeServiceConfig( - Config.proxyConfig.proxyUrlBase, - imageConfig, - autoFreezeConfig, - dataprocConfig, - Config.gceConfig, - azureServiceConfig - ) - - val wsmClientProvider = new MockWsmClientProvider() - - // used when we care about queue state - def makeInterp( - queue: Queue[IO, LeoPubsubMessage] = QueueFactory.makePublisherQueue(), - dateAccessedQueue: Queue[IO, UpdateDateAccessedMessage] = QueueFactory.makeDateAccessedQueue(), - wsmClientProvider: WsmApiClientProvider[IO] = wsmClientProvider, - samService: SamService[IO] = MockSamService - ) = - new RuntimeV2ServiceInterp[IO](serviceConfig, queue, dateAccessedQueue, wsmClientProvider, samService) - - // need to set previous runtime to deleted status before creating next to avoid exception - def setRuntimeDeleted(workspaceId: WorkspaceId, name: RuntimeName): IO[Long] = - for { - now <- IO.realTimeInstant - runtime <- RuntimeServiceDbQueries - .getRuntimeByWorkspaceId(workspaceId, name) - .transaction - - _ <- clusterQuery - .completeDeletion(runtime.id, now) - .transaction - } yield runtime.id - - def mockSamForCreateRuntime(userInfo: UserInfo): SamService[IO] = { - val samService = mock[SamService[IO]] - when(samService.checkAuthorized(any(), any(), any())(any())).thenReturn(IO.unit) - when(samService.getUserEmail(userInfo.accessToken.token)).thenReturn(IO.pure(userInfo.userEmail)) - when(samService.listResources(any(), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List.empty)) - when(samService.createResource(any(), any(), any(), any(), any())(any())).thenReturn(IO.unit) - samService - } - - def mockUserInfo(email: String = userEmail.toString()): UserInfo = - UserInfo(OAuth2BearerToken(""), WorkbenchUserId(s"userId-${email}"), WorkbenchEmail(email), 0) - - val runtimeV2Service = - new RuntimeV2ServiceInterp[IO]( - serviceConfig, - QueueFactory.makePublisherQueue(), - QueueFactory.makeDateAccessedQueue(), - wsmClientProvider, - MockSamService - ) - - val runtimeV2Service2 = - new RuntimeV2ServiceInterp[IO]( - serviceConfig, - QueueFactory.makePublisherQueue(), - QueueFactory.makeDateAccessedQueue(), - wsmClientProvider, - MockSamService - ) - - it should "submit a create azure runtime message properly" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - - val azureService = makeInterp(publisherQueue) - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - context <- appContext.ask[AppContext] - - r <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - .attempt - workspaceDesc <- wsmClientProvider.getWorkspace("token", workspaceId) - cloudContext = CloudContext.Azure(workspaceDesc.get.azureContext.get) - clusterRecOpt <- clusterQuery - .getActiveClusterRecordByName(cloudContext, runtimeName)(scala.concurrent.ExecutionContext.global) - .transaction - clusterRec = clusterRecOpt.get - clusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(cloudContext, runtimeName)(scala.concurrent.ExecutionContext.global) - .transaction - cluster = clusterOpt.get - runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(cluster.runtimeConfigId).transaction - message <- publisherQueue.take - azureRuntimeConfig = runtimeConfig.asInstanceOf[RuntimeConfig.AzureConfig] - fullClusterOpt <- clusterQuery.getClusterById(cluster.id).transaction - - diskOpt <- persistentDiskQuery.getById(azureRuntimeConfig.persistentDiskId.get).transaction - disk = diskOpt.get - } yield { - r shouldBe Right(CreateRuntimeResponse(context.traceId)) - cluster.cloudContext shouldBe cloudContext - cluster.runtimeName shouldBe runtimeName - cluster.status shouldBe RuntimeStatus.PreCreating - clusterRec.workspaceId shouldBe Some(workspaceId) - - azureRuntimeConfig.machineType.value shouldBe VirtualMachineSizeTypes.STANDARD_A1.toString - azureRuntimeConfig.region shouldBe None - disk.name.value shouldBe defaultCreateAzureRuntimeReq.azureDiskConfig.name.value - - val expectedRuntimeImage = Set( - RuntimeImage( - RuntimeImageType.Azure, - "microsoft-dsvm, ubuntu-2004, 2004-gen2, 23.04.24", - None, - context.now - ), - RuntimeImage( - RuntimeImageType.Listener, - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.listenerImage, - None, - context.now - ), - RuntimeImage( - RuntimeImageType.Welder, - ConfigReader.appConfig.azure.pubsubHandler.welderImageHash, - None, - context.now - ) - ) - - fullClusterOpt.map(_.runtimeImages) shouldBe Some(expectedRuntimeImage) - - val expectedMessage = CreateAzureRuntimeMessage( - cluster.id, - workspaceId, - false, - Some(context.traceId), - workspaceDesc.get.displayName, - BillingProfileId("spend-profile") - ) - message shouldBe expectedMessage - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to create a runtime when caller has no permission" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val samService = mock[SamService[IO]] - when( - samService.checkAuthorized(unauthorizedUserInfo.accessToken.token, - WorkspaceResourceSamResourceId(workspaceId), - WorkspaceAction.Compute - ) - ).thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - val runtimeV2Service = makeInterp(samService = samService) - - val thrown = the[ForbiddenError] thrownBy { - runtimeV2Service - .createRuntime(unauthorizedUserInfo, runtimeName, workspaceId, false, defaultCreateAzureRuntimeReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - thrown shouldBe ForbiddenError(unauthorizedEmail) - } - - it should "throw RuntimeAlreadyExistsException when creating a runtime with same name and context as an existing runtime" in isolatedDbTest { - runtimeV2Service - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val exc = runtimeV2Service - .createRuntime(userInfo2, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - exc shouldBe a[RuntimeAlreadyExistsException] - } - - it should "fail to create a runtime with existing disk if there are multiple disks" in isolatedDbTest { - runtimeV2Service - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - // set runtime status to deleted before creating next - setRuntimeDeleted(workspaceId, name0).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - runtimeV2Service - .createRuntime( - userInfo, - name1, - workspaceId, - false, - CreateAzureRuntimeRequest( - Map.empty, - VirtualMachineSizeTypes.STANDARD_A1, - Map.empty, - CreateAzureDiskRequest( - Map.empty, - AzureDiskName("diskName2"), - Some(DiskSize(100)), - None - ), - Some(0) - ) - ) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - // set runtime status to deleted before creating next - setRuntimeDeleted(workspaceId, name1).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val exc = runtimeV2Service - .createRuntime(userInfo, name2, workspaceId, true, defaultCreateAzureRuntimeReq) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - - exc shouldBe a[MultiplePersistentDisksException] - } - - it should "fail to create a runtime with existing disk if there are 0 disks" in isolatedDbTest { - val exc = runtimeV2Service - .createRuntime(userInfo, name0, workspaceId, true, defaultCreateAzureRuntimeReq) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - exc shouldBe a[NoPersistentDiskException] - } - - it should "fail to create a runtime with existing disk if disk isn't ready" in isolatedDbTest { - val res = for { - _ <- runtimeV2Service - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - disk = disks.head - now <- IO.realTimeInstant - _ <- persistentDiskQuery.updateStatus(disk.id, DiskStatus.Creating, now).transaction - - // set runtime to deleted so they dont get hit by `OnlyOneRuntimePerWorkspacePerCreator` - runtime <- clusterQuery.getClusterWithDiskId(disk.id).transaction - _ <- clusterQuery.updateClusterStatus(runtime.get.id, RuntimeStatus.Deleted, now).transaction - - _ <- runtimeV2Service - .createRuntime(userInfo, name1, workspaceId, true, defaultCreateAzureRuntimeReq) - } yield () - - val exc = res.attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - - exc shouldBe a[PersistentDiskNotReadyException] - } - - it should "fail to create a runtime with existing disk if disk is attached" in isolatedDbTest { - val res = for { - _ <- runtimeV2Service - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - disk = disks.head - now <- IO.realTimeInstant - _ <- persistentDiskQuery.updateStatus(disk.id, DiskStatus.Ready, now).transaction - - // set runtime to deleted so they dont get hit by `OnlyOneRuntimePerWorkspacePerCreator` - runtime <- clusterQuery.getClusterWithDiskId(disk.id).transaction - _ <- clusterQuery.updateClusterStatus(runtime.get.id, RuntimeStatus.Deleted, now).transaction - - err <- runtimeV2Service - .createRuntime(userInfo, name1, workspaceId, true, defaultCreateAzureRuntimeReq) - - } yield () - - val exc = res.attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - - exc shouldBe a[DiskAlreadyAttachedException] - } - - it should "fail to create a runtime with existing disk if disk is attached to non-deleted runtime" in isolatedDbTest { - val samService = mockSamForCreateRuntime(userInfo) - val runtimeV2Service = makeInterp(samService = samService) - val res = for { - _ <- runtimeV2Service - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - disk = disks.head - now <- IO.realTimeInstant - _ <- persistentDiskQuery.updateStatus(disk.id, DiskStatus.Ready, now).transaction - - runtime <- clusterQuery.getClusterWithDiskId(disk.id).transaction - _ = when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List(runtime.get.internalId))) - - err <- runtimeV2Service - .createRuntime(userInfo, name1, workspaceId, true, defaultCreateAzureRuntimeReq) - .attempt - - } yield err shouldBe Left( - OnlyOneRuntimePerWorkspacePerCreator(workspaceId, userInfo.userEmail, runtime.get.runtimeName, runtime.get.status) - ) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to create a runtime if one exists in the workspace" in isolatedDbTest { - val samService = mockSamForCreateRuntime(userInfo) - val runtimeV2Service = makeInterp(samService = samService) - - runtimeV2Service - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val runtime = - runtimeV2Service.getRuntime(userInfo, name0, workspaceId).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List(runtime.samResource.resourceId))) - - val exc = runtimeV2Service - .createRuntime(userInfo, name2, workspaceId, false, defaultCreateAzureRuntimeReq) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - - exc shouldBe a[OnlyOneRuntimePerWorkspacePerCreator] - } - - it should "create a runtime if one exists in the workspace but for another user" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue) - - azureService - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - context <- appContext.ask[AppContext] - - r <- azureService - .createRuntime( - userInfo2, - name2, - workspaceId, - false, - defaultCreateAzureRuntimeReq.copy( - azureDiskConfig = defaultCreateAzureRuntimeReq.azureDiskConfig.copy(name = AzureDiskName("diskName2")) - ) - ) - .attempt - workspaceDesc <- wsmClientProvider.getWorkspace("token", workspaceId) - cloudContext = CloudContext.Azure(workspaceDesc.get.azureContext.get) - clusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(cloudContext, name2)(scala.concurrent.ExecutionContext.global) - .transaction - cluster = clusterOpt.get - message <- publisherQueue.take - } yield { - r shouldBe Right(CreateRuntimeResponse(context.traceId)) - - val expectedMessage = CreateAzureRuntimeMessage( - cluster.id, - workspaceId, - false, - Some(context.traceId), - workspaceDesc.get.displayName, - BillingProfileId("spend-profile") - ) - message shouldBe expectedMessage - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "create a runtime if one exists but in deleting status" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val storageContainerResourceId = WsmControlledResourceId(UUID.randomUUID()) - - val azureService = makeInterp(publisherQueue) - - azureService - .createRuntime(userInfo, name0, workspaceId, false, defaultCreateAzureRuntimeReq) - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val updateRuntimeStatus = for { - now <- IO.realTimeInstant - runtime <- RuntimeServiceDbQueries - .getRuntimeByWorkspaceId(workspaceId, name0) - .transaction - _ <- clusterQuery - .updateClusterStatus(runtime.id, RuntimeStatus.Deleting, now) - .transaction - } yield () - updateRuntimeStatus.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - context <- appContext.ask[AppContext] - - r <- azureService - .createRuntime( - userInfo, - name2, - workspaceId, - false, - defaultCreateAzureRuntimeReq.copy( - azureDiskConfig = defaultCreateAzureRuntimeReq.azureDiskConfig.copy(name = AzureDiskName("diskName2")) - ) - ) - .attempt - workspaceDesc <- wsmClientProvider.getWorkspace("token", workspaceId) - cloudContext = CloudContext.Azure(workspaceDesc.get.azureContext.get) - clusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(cloudContext, name2)(scala.concurrent.ExecutionContext.global) - .transaction - cluster = clusterOpt.get - message <- publisherQueue.take - } yield { - r shouldBe Right(CreateRuntimeResponse(context.traceId)) - - val expectedMessage = CreateAzureRuntimeMessage( - cluster.id, - workspaceId, - false, - Some(context.traceId), - workspaceDesc.get.displayName, - BillingProfileId("spend-profile") - ) - message shouldBe expectedMessage - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "get a runtime" in isolatedDbTest { - val userInfo = UserInfo( - OAuth2BearerToken(""), - WorkbenchUserId("userId"), - WorkbenchEmail("user1@example.com"), - 0 - ) // this email is allowlisted - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - val samService = mockSamForCreateRuntime(userInfo) - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) - ).thenReturn(IO.unit) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue, samService = samService) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - azureCloudContext <- wsmClientProvider.getWorkspace("token", workspaceId).map(_.get.azureContext) - clusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(CloudContext.Azure(azureCloudContext.get), runtimeName)( - scala.concurrent.ExecutionContext.global - ) - .transaction - cluster = clusterOpt.get - getResponse <- azureService.getRuntime(userInfo, runtimeName, workspaceId) - } yield { - getResponse.clusterName shouldBe runtimeName - getResponse.auditInfo.creator shouldBe userInfo.userEmail - - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to get a non-existent runtime" in isolatedDbTest { - runtimeV2Service - .getRuntime(userInfo, RuntimeName("non-existent"), workspaceId) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get shouldBe a[RuntimeNotFoundByWorkspaceIdException] - } - - it should "fail to get a runtime when caller lacks permission" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val samService = mockSamForCreateRuntime(userInfo) - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) - ).thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - val publisherQueue = QueueFactory.makePublisherQueue() - - val testAzureService = makeInterp(publisherQueue, samService = samService) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - - _ <- testAzureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - _ <- testAzureService.getRuntime(userInfo, runtimeName, workspaceId) - } yield () - - the[RuntimeNotFoundByWorkspaceIdException] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "publish start a runtime message properly" in isolatedDbTest { - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue) - val res = for { - ctx <- appContext.ask[AppContext] - runtime <- IO( - makeCluster(0) - .copy( - status = RuntimeStatus.Stopped, - workspaceId = Some(workspaceId), - auditInfo = auditInfo.copy(creator = userInfo.userEmail) - ) - .save() - ) - _ <- azureService - .startRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) - msg <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - - } yield msg shouldBe Some(StartRuntimeMessage(runtime.id, Some(ctx.traceId))) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to start a runtime if permission denied" in isolatedDbTest { - // User is runtime creator, but does not have access to the workspace - val userInfo = UserInfo(OAuth2BearerToken(""), WorkbenchUserId("user"), WorkbenchEmail("email"), 0) - val workspaceId = WorkspaceId(UUID.randomUUID()) - val samService = mock[SamService[IO]] - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.StopStartRuntime))(any()) - ) - .thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) - ).thenReturn(IO.unit) - val interp = makeInterp(samService = samService) - - val res = for { - runtime <- IO( - makeCluster(0) - .copy( - status = RuntimeStatus.Running, - workspaceId = Some(workspaceId), - auditInfo = auditInfo.copy(creator = userInfo.userEmail) - ) - .save() - ) - r <- interp - .startRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) - .attempt - } yield { - val exception = r.swap.toOption.get - exception.getMessage shouldBe s"email is unauthorized. If you have proper permissions to use the workspace, make sure you are also added to the billing account" - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to start a runtime when runtime doesn't exist in DB" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val res = - runtimeV2Service - .startRuntime(userInfo, runtimeName, workspaceId) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val exception = res.swap.toOption.get - exception.isInstanceOf[RuntimeNotFoundByWorkspaceIdException] shouldBe true - exception.getMessage shouldBe s"Runtime clusterName1 not found in workspace ${workspaceId.value}" - } - - it should "fail to start a runtime when runtime is not in startable statuses" in isolatedDbTest { - val res = for { - runtime <- IO( - makeCluster(0) - .copy( - status = RuntimeStatus.Running, - workspaceId = Some(workspaceId), - auditInfo = auditInfo.copy(creator = userInfo.userEmail) - ) - .save() - ) - res <- runtimeV2Service - .startRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) - .attempt - } yield { - val exception = res.swap.toOption.get - exception.isInstanceOf[RuntimeCannotBeStartedException] shouldBe true - exception.getMessage shouldBe "Runtime Gcp/dsp-leo-test cannot be started in Running status" - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "publish stop a runtime message properly" in isolatedDbTest { - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue) - val res = for { - ctx <- appContext.ask[AppContext] - runtime <- IO( - makeCluster(0) - .copy( - status = RuntimeStatus.Running, - workspaceId = Some(workspaceId), - auditInfo = auditInfo.copy(creator = userInfo.userEmail) - ) - .save() - ) - _ <- azureService - .stopRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) - msg <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - - } yield msg shouldBe Some(StopRuntimeMessage(runtime.id, Some(ctx.traceId))) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to stop a runtime if permission denied" in isolatedDbTest { - val userInfo = UserInfo(OAuth2BearerToken(""), WorkbenchUserId("user"), WorkbenchEmail("email"), 0) - val workspaceId = WorkspaceId(UUID.randomUUID()) - val samService = mock[SamService[IO]] - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.StopStartRuntime))(any()) - ) - .thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) - ).thenReturn(IO.unit) - val interp = makeInterp(samService = samService) - - val res = for { - runtime <- IO( - makeCluster(0) - .copy( - status = RuntimeStatus.Running, - workspaceId = Some(workspaceId), - auditInfo = auditInfo.copy(creator = userInfo.userEmail) - ) - .save() - ) - r <- interp - .stopRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) - .attempt - } yield { - val exception = r.swap.toOption.get - exception.getMessage shouldBe s"email is unauthorized. If you have proper permissions to use the workspace, make sure you are also added to the billing account" - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to stop a runtime when runtime doesn't exist in DB" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val res = - runtimeV2Service - .stopRuntime(userInfo, runtimeName, workspaceId) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - val exception = res.swap.toOption.get - exception.isInstanceOf[RuntimeNotFoundByWorkspaceIdException] shouldBe true - exception.getMessage shouldBe s"Runtime clusterName1 not found in workspace ${workspaceId.value}" - } - - it should "fail to stop a runtime when runtime is not in startable statuses" in isolatedDbTest { - val res = for { - runtime <- IO( - makeCluster(0) - .copy( - status = RuntimeStatus.Stopped, - workspaceId = Some(workspaceId), - auditInfo = auditInfo.copy(creator = userInfo.userEmail) - ) - .save() - ) - res <- runtimeV2Service - .stopRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) - .attempt - } yield { - val exception = res.swap.toOption.get - exception.isInstanceOf[RuntimeCannotBeStoppedException] shouldBe true - exception.getMessage shouldBe s"Runtime Gcp/dsp-leo-test/${runtime.runtimeName.asString} cannot be stopped in Stopped status" - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete a runtime" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue) - - val res = for { - context <- appContext.ask[AppContext] - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - disk = disks.head - _ <- persistentDiskQuery.updateWSMResourceId(disk.id, wsmResourceId, context.now).transaction - - _ <- publisherQueue.tryTake // clean out create msg - preDeleteCluster <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName).transaction - - _ <- clusterQuery.updateClusterStatus(preDeleteCluster.id, RuntimeStatus.Running, context.now).transaction - _ <- persistentDiskQuery.updateStatus(disk.id, DiskStatus.Ready, context.now).transaction - - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - - message <- publisherQueue.take - - postDeleteClusterOpt <- clusterQuery - .getClusterById(preDeleteCluster.id) - .transaction - - wsmResourceId = WsmControlledResourceId(UUID.fromString(preDeleteCluster.internalId)) - - runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(preDeleteCluster.runtimeConfigId).transaction - diskOpt <- persistentDiskQuery - .getById(runtimeConfig.asInstanceOf[RuntimeConfig.AzureConfig].persistentDiskId.get) - .transaction - disk = diskOpt.get - } yield { - postDeleteClusterOpt.map(_.status) shouldBe Some(RuntimeStatus.Deleting) - disk.status shouldBe DiskStatus.Deleting - - val expectedMessage = - DeleteAzureRuntimeMessage( - preDeleteCluster.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - BillingProfileId("spend-profile"), - Some(context.traceId) - ) - message shouldBe expectedMessage - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete a runtime and not delete the disk if the disk is already deleted" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue) - - val res = for { - context <- appContext.ask[AppContext] - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - disk = disks.head - _ <- persistentDiskQuery.updateWSMResourceId(disk.id, wsmResourceId, context.now).transaction - _ <- persistentDiskQuery.delete(disk.id, context.now).transaction - - _ <- publisherQueue.tryTake // clean out create msg - preDeleteCluster <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName).transaction - - _ <- clusterQuery.updateClusterStatus(preDeleteCluster.id, RuntimeStatus.Running, context.now).transaction - - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - - message <- publisherQueue.take - - postDeleteClusterOpt <- clusterQuery - .getClusterById(preDeleteCluster.id) - .transaction - - wsmResourceId = WsmControlledResourceId(UUID.fromString(preDeleteCluster.internalId)) - - runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(preDeleteCluster.runtimeConfigId).transaction - diskOpt <- persistentDiskQuery - .getById(runtimeConfig.asInstanceOf[RuntimeConfig.AzureConfig].persistentDiskId.get) - .transaction - disk = diskOpt.get - } yield { - postDeleteClusterOpt.map(_.status) shouldBe Some(RuntimeStatus.Deleting) - - val expectedMessage = - DeleteAzureRuntimeMessage( - preDeleteCluster.id, - None, - workspaceId, - Some(wsmResourceId), - BillingProfileId("spend-profile"), - Some(context.traceId) - ) - message shouldBe expectedMessage - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not delete a runtime in a creating status in Wsm" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - IO.pure(WsmState(Some("CREATING"))) - } - val azureService = makeInterp(wsmClientProvider = wsmClientProvider) - - val res = for { - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - } yield () - - the[RuntimeCannotBeDeletedWsmException] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "not delete a runtime in a updating status in Wsm" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - IO.pure(WsmState(Some("UPDATING"))) - } - val azureService = makeInterp(wsmClientProvider = wsmClientProvider) - - val res = for { - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - } yield () - - the[RuntimeCannotBeDeletedWsmException] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "not delete a runtime in a deleting status in Wsm" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - IO.pure(WsmState(Some("DELETING"))) - } - val azureService = makeInterp(wsmClientProvider = wsmClientProvider) - - val res = for { - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - } yield () - - the[RuntimeCannotBeDeletedWsmException] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "not delete a runtime if the disk cannot be deleted in Wsm" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - if (wsmResourceType == WsmResourceType.AzureDisk) IO.pure(WsmState(Some("CREATING"))) - else IO.pure(WsmState(Some("RUNNING"))) - } - val azureService = makeInterp(wsmClientProvider = wsmClientProvider) - - val res = for { - context <- appContext.ask[AppContext] - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - disk = disks.head - _ <- persistentDiskQuery.updateWSMResourceId(disk.id, wsmResourceId, context.now).transaction - - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - } yield () - - the[RuntimeCannotBeDeletedWsmException] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "delete a runtime and not send wsmResourceId if runtime is deleted in WSM" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - - // make VM be deleted in WSM - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - IO.pure(WsmState(None)) - } - val azureService = makeInterp(publisherQueue, wsmClientProvider = wsmClientProvider) - - val res = for { - context <- appContext.ask[AppContext] - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - disk = disks.head - _ <- persistentDiskQuery.updateWSMResourceId(disk.id, wsmResourceId, context.now).transaction - _ <- persistentDiskQuery.updateStatus(disk.id, DiskStatus.Ready, context.now).transaction - - _ <- publisherQueue.tryTake // clean out create msg - azureCloudContext <- wsmClientProvider.getWorkspace("token", workspaceId).map(_.get.azureContext) - preDeleteClusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(CloudContext.Azure(azureCloudContext.get), runtimeName)( - scala.concurrent.ExecutionContext.global - ) - .transaction - preDeleteCluster = preDeleteClusterOpt.get - _ <- clusterQuery.updateClusterStatus(preDeleteCluster.id, RuntimeStatus.Running, context.now).transaction - - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - - message <- publisherQueue.take - - postDeleteClusterOpt <- clusterQuery - .getClusterById(preDeleteCluster.id) - .transaction - - runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(preDeleteCluster.runtimeConfigId).transaction - diskOpt <- persistentDiskQuery - .getById(runtimeConfig.asInstanceOf[RuntimeConfig.AzureConfig].persistentDiskId.get) - .transaction - disk = diskOpt.get - } yield { - postDeleteClusterOpt.map(_.status) shouldBe Some(RuntimeStatus.Deleting) - disk.status shouldBe DiskStatus.Deleting - - val expectedMessage = - DeleteAzureRuntimeMessage( - preDeleteCluster.id, - None, - workspaceId, - None, - BillingProfileId("spend-profile"), - Some(context.traceId) - ) - message shouldBe expectedMessage - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete a runtime but keep the disk if specified" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue) - - val res = for { - context <- appContext.ask[AppContext] - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - _ <- publisherQueue.tryTake // clean out create msg - preDeleteCluster <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName).transaction - - wsmResourceId = WsmControlledResourceId(UUID.fromString(preDeleteCluster.internalId)) - - _ <- clusterQuery.updateClusterStatus(preDeleteCluster.id, RuntimeStatus.Running, context.now).transaction - - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, false) - - message <- publisherQueue.take - - postDeleteClusterOpt <- clusterQuery - .getClusterById(preDeleteCluster.id) - .transaction - - runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(preDeleteCluster.runtimeConfigId).transaction - diskOpt <- persistentDiskQuery - .getById(runtimeConfig.asInstanceOf[RuntimeConfig.AzureConfig].persistentDiskId.get) - .transaction - disk = diskOpt.get - } yield { - postDeleteClusterOpt.map(_.status) shouldBe Some(RuntimeStatus.Deleting) - disk.status shouldBe DiskStatus.Creating - - val expectedMessage = - DeleteAzureRuntimeMessage( - preDeleteCluster.id, - None, - workspaceId, - Some(wsmResourceId), - BillingProfileId("spend-profile"), - Some(context.traceId) - ) - message shouldBe expectedMessage - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to delete a runtime when caller is missing delete permission" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val samService = mockSamForCreateRuntime(userInfo) - when(samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.DeleteRuntime))(any())) - .thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) - ).thenReturn(IO.unit) - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue, samService = samService) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - jobUUID <- IO.delay(UUID.randomUUID().toString()).map(WsmJobId) - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - azureCloudContext <- wsmClientProvider.getWorkspace("token", workspaceId).map(_.get.azureContext) - clusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(CloudContext.Azure(azureCloudContext.get), runtimeName)( - scala.concurrent.ExecutionContext.global - ) - .transaction - cluster = clusterOpt.get - now <- IO.realTimeInstant - _ <- clusterQuery.updateClusterStatus(cluster.id, RuntimeStatus.Running, now).transaction - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - } yield () - - the[ForbiddenError] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail to delete a runtime and not reveal its existence when user has no access to it" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val samService = mockSamForCreateRuntime(userInfo) - when(samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.DeleteRuntime))(any())) - .thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) - ).thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue, samService = samService) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - jobUUID <- IO.delay(UUID.randomUUID().toString()).map(WsmJobId) - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - azureCloudContext <- wsmClientProvider.getWorkspace("token", workspaceId).map(_.get.azureContext) - clusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(CloudContext.Azure(azureCloudContext.get), runtimeName)( - scala.concurrent.ExecutionContext.global - ) - .transaction - cluster = clusterOpt.get - now <- IO.realTimeInstant - _ <- clusterQuery.updateClusterStatus(cluster.id, RuntimeStatus.Running, now).transaction - _ <- azureService.deleteRuntime(userInfo, runtimeName, workspaceId, true) - } yield () - - the[RuntimeNotFoundByWorkspaceIdException] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "Azure V2 - deleteAllRuntimes, update all status appropriately, and queue multiple messages" in isolatedDbTest { - val runtimeName_1 = RuntimeName("clusterName1") - val runtimeName_2 = RuntimeName("clusterName2") - val runtimeName_3 = RuntimeName("clusterName3") - val workspaceId = WorkspaceId(UUID.randomUUID()) - val samService = mockSamForCreateRuntime(userInfo) - when(samService.getUserEmail(userInfo2.accessToken.token)).thenReturn(IO.pure(userInfo2.userEmail)) - when(samService.getUserEmail(userInfo3.accessToken.token)).thenReturn(IO.pure(userInfo3.userEmail)) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue, samService = samService) - - val res = for { - context <- appContext.ask[AppContext] - azureCloudContextOpt <- wsmClientProvider.getWorkspace("token", workspaceId).map(_.get.azureContext) - azureCloudContext = CloudContext.Azure(azureCloudContextOpt.get) - - _ <- azureService - .createRuntime( - userInfo, - runtimeName_1, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - - _ <- azureService - .createRuntime( - userInfo2, - runtimeName_2, - workspaceId, - false, - defaultCreateAzureRuntimeReq.copy( - azureDiskConfig = defaultCreateAzureRuntimeReq.azureDiskConfig.copy(name = AzureDiskName("diskName2")) - ) - ) - - _ <- azureService - .createRuntime( - userInfo3, - runtimeName_3, - workspaceId, - false, - defaultCreateAzureRuntimeReq.copy( - azureDiskConfig = defaultCreateAzureRuntimeReq.azureDiskConfig.copy(name = AzureDiskName("diskName3")) - ) - ) - - disks <- DiskServiceDbQueries - .listDisks(Map.empty, Some(userInfo.userEmail), None, Some(workspaceId)) - .transaction - - disk1 <- persistentDiskQuery.getActiveByName(azureCloudContext, DiskName("diskName1")).transaction - disk2 <- persistentDiskQuery.getActiveByName(azureCloudContext, DiskName("diskName2")).transaction - disk3 <- persistentDiskQuery.getActiveByName(azureCloudContext, DiskName("diskName3")).transaction - - _ <- persistentDiskQuery.updateWSMResourceId(disk1.get.id, wsmResourceId, context.now).transaction - _ <- persistentDiskQuery.updateWSMResourceId(disk2.get.id, wsmResourceId, context.now).transaction - _ <- persistentDiskQuery.updateWSMResourceId(disk3.get.id, wsmResourceId, context.now).transaction - - _ <- persistentDiskQuery.updateStatus(disk1.get.id, DiskStatus.Ready, context.now).transaction - _ <- persistentDiskQuery.updateStatus(disk2.get.id, DiskStatus.Ready, context.now).transaction - _ <- persistentDiskQuery.updateStatus(disk3.get.id, DiskStatus.Ready, context.now).transaction - - _ <- publisherQueue.tryTakeN(Some(3)) // clean out create msg - - preDeleteCluster_1 <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName_1).transaction - preDeleteCluster_2 <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName_2).transaction - preDeleteCluster_3 <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName_3).transaction - _ = when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn( - IO.pure(List(preDeleteCluster_1.internalId, preDeleteCluster_2.internalId, preDeleteCluster_3.internalId)) - ) - - _ <- clusterQuery.updateClusterStatus(preDeleteCluster_1.id, RuntimeStatus.Deleted, context.now).transaction - _ <- clusterQuery.updateClusterStatus(preDeleteCluster_2.id, RuntimeStatus.Running, context.now).transaction - _ <- clusterQuery.updateClusterStatus(preDeleteCluster_3.id, RuntimeStatus.Error, context.now).transaction - - wsmResourceId_2 = WsmControlledResourceId(UUID.fromString(preDeleteCluster_2.internalId)) - wsmResourceId_3 = WsmControlledResourceId(UUID.fromString(preDeleteCluster_3.internalId)) - - _ <- azureService.deleteAllRuntimes(userInfo, workspaceId, true) - - delete_messages <- publisherQueue.tryTakeN(Some(3)) - - postDeleteClusterOpt_1 <- clusterQuery - .getClusterById(preDeleteCluster_1.id) - .transaction - postDeleteClusterOpt_2 <- clusterQuery - .getClusterById(preDeleteCluster_2.id) - .transaction - postDeleteClusterOpt_3 <- clusterQuery - .getClusterById(preDeleteCluster_3.id) - .transaction - - runtimeConfig_2 <- RuntimeConfigQueries.getRuntimeConfig(preDeleteCluster_2.runtimeConfigId).transaction - runtimeConfig_3 <- RuntimeConfigQueries.getRuntimeConfig(preDeleteCluster_3.runtimeConfigId).transaction - - diskOpt_2 <- persistentDiskQuery - .getById(runtimeConfig_2.asInstanceOf[RuntimeConfig.AzureConfig].persistentDiskId.get) - .transaction - diskOpt_3 <- persistentDiskQuery - .getById(runtimeConfig_3.asInstanceOf[RuntimeConfig.AzureConfig].persistentDiskId.get) - .transaction - - disk_2 = diskOpt_2.get - disk_3 = diskOpt_3.get - } yield { - postDeleteClusterOpt_1.map(_.status) shouldBe Some(RuntimeStatus.Deleted) - postDeleteClusterOpt_2.map(_.status) shouldBe Some(RuntimeStatus.Deleting) - disk_2.status shouldBe DiskStatus.Deleting - postDeleteClusterOpt_3.map(_.status) shouldBe Some(RuntimeStatus.Deleting) - disk_3.status shouldBe DiskStatus.Deleting - - val expectedMessages = List( - DeleteAzureRuntimeMessage( - preDeleteCluster_2.id, - Some(disk_2.id), - workspaceId, - Some(wsmResourceId_2), - BillingProfileId("spend-profile"), - Some(context.traceId) - ), - DeleteAzureRuntimeMessage( - preDeleteCluster_3.id, - Some(disk_3.id), - workspaceId, - Some(wsmResourceId_3), - BillingProfileId("spend-profile"), - Some(context.traceId) - ) - ) - delete_messages should contain theSameElementsAs expectedMessages - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "Azure V2 - deleteAllRuntimes, error out if any runtime is not in a deletable status" in isolatedDbTest { - val runtimeName_1 = RuntimeName("clusterName1") - val runtimeName_2 = RuntimeName("clusterName2") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val samService = mockSamForCreateRuntime(userInfo) - when(samService.getUserEmail(userInfo2.accessToken.token)).thenReturn(IO.pure(userInfo2.userEmail)) - - val publisherQueue = QueueFactory.makePublisherQueue() - val azureService = makeInterp(publisherQueue, samService = samService) - - val res = for { - context <- appContext.ask[AppContext] - - _ <- azureService - .createRuntime( - userInfo, - runtimeName_1, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - - _ <- azureService - .createRuntime( - userInfo2, - runtimeName_2, - workspaceId, - false, - defaultCreateAzureRuntimeReq.copy( - azureDiskConfig = defaultCreateAzureRuntimeReq.azureDiskConfig.copy(name = AzureDiskName("diskName2")) - ) - ) - - _ <- publisherQueue.tryTakeN(Some(2)) // clean out create msg - azureCloudContext <- wsmClientProvider.getWorkspace("token", workspaceId).map(_.get.azureContext) - preDeleteClusterOpt_1 <- clusterQuery - .getActiveClusterByNameMinimal(CloudContext.Azure(azureCloudContext.get), runtimeName_1)( - scala.concurrent.ExecutionContext.global - ) - .transaction - preDeleteClusterOpt_2 <- clusterQuery - .getActiveClusterByNameMinimal(CloudContext.Azure(azureCloudContext.get), runtimeName_2)( - scala.concurrent.ExecutionContext.global - ) - .transaction - // Let's keep the first cluster in the creating status so we won't deleted it in this test - preDeleteCluster_1 = preDeleteClusterOpt_1.get - _ <- clusterQuery.updateClusterStatus(preDeleteCluster_1.id, RuntimeStatus.Creating, context.now).transaction - preDeleteCluster_2 = preDeleteClusterOpt_2.get - _ <- clusterQuery.updateClusterStatus(preDeleteCluster_2.id, RuntimeStatus.Running, context.now).transaction - _ = when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List(preDeleteCluster_1.samResource.resourceId, preDeleteCluster_2.samResource.resourceId))) - _ <- azureService.deleteAllRuntimes(userInfo, workspaceId, true) - - } yield () - - the[NonDeletableRuntimesInWorkspaceFoundException] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "list runtimes" in isolatedDbTest { - val runtimeId1 = UUID.randomUUID.toString - val runtimeId2 = UUID.randomUUID.toString - val projectIdGcp = cloudContextGcp.asString - val workspaceIdAzure = UUID.randomUUID.toString - - val samService = mock[SamService[IO]] - when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List(runtimeId1, runtimeId2))) - val testService = makeInterp(samService = samService) - - val res = for { - samResource1 <- IO(RuntimeSamResourceId(runtimeId1)) - samResource2 <- IO(RuntimeSamResourceId(runtimeId2)) - // GCP runtime - runtime1 <- IO(makeCluster(1).copy(samResource = samResource1, workspaceId = workspaceIdOpt).save()) - // Azure runtime - runtime2 <- IO( - makeCluster(2) - .copy( - samResource = samResource2, - cloudContext = CloudContext.Azure(CommonTestData.azureCloudContext), - workspaceId = Some(WorkspaceId(UUID.fromString(workspaceIdAzure))) - ) - .save() - ) - listResponse <- testService.listRuntimes(userInfo, None, None, Map.empty) - } yield { - listResponse.map(_.samResource).toSet shouldBe Set(samResource1, samResource2) - listResponse should contain( - ListRuntimeResponse2( - id = runtime1.id, - workspaceId = workspaceIdOpt, - samResource = runtime1.samResource, - clusterName = runtime1.runtimeName, - cloudContext = runtime1.cloudContext, - auditInfo = runtime1.auditInfo, - runtimeConfig = defaultDataprocRuntimeConfig, - proxyUrl = Runtime - .getProxyUrl(proxyUrlBase, cloudContextGcp, runtime1.runtimeName, Set(jupyterImage), None, Map.empty), - runtime1.status, - runtime1.labels, - runtime1.patchInProgress - ) - ) - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "list runtimes with a workspace and/or cloudProvider" in isolatedDbTest { - val runtimeId1 = UUID.randomUUID.toString - val runtimeId2 = UUID.randomUUID.toString - val runtimeId3 = UUID.randomUUID.toString - val runtimeId4 = UUID.randomUUID.toString - val runtimeId5 = UUID.randomUUID.toString - val projectIdGcp1 = "gcp-context-1" - val projectIdGcp2 = "gcp-context-2" - val workspaceId1 = UUID.randomUUID.toString - val workspaceId2 = UUID.randomUUID.toString - val workspaceId3 = UUID.randomUUID.toString - - val samService = mock[SamService[IO]] - when(samService.listResources(any(), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List(runtimeId1, runtimeId2, runtimeId3, runtimeId4, runtimeId5))) - - val testService = makeInterp(samService = samService) - - val res = for { - samResource1 <- IO(RuntimeSamResourceId(runtimeId1)) - samResource2 <- IO(RuntimeSamResourceId(runtimeId2)) - samResource3 <- IO(RuntimeSamResourceId(runtimeId3)) - samResource4 <- IO(RuntimeSamResourceId(runtimeId4)) - samResource5 <- IO(RuntimeSamResourceId(runtimeId5)) - workspace1 <- IO(WorkspaceId(UUID.fromString(workspaceId1))) - workspace2 <- IO(WorkspaceId(UUID.fromString(workspaceId2))) - workspace3 <- IO(WorkspaceId(UUID.fromString(workspaceId3))) - - // hidden runtime 1, owned workspace 1, Azure - _ <- IO( - makeCluster(1) - .copy( - samResource = samResource1, - workspaceId = Some(workspace1), - cloudContext = CloudContext.Azure( - AzureCloudContext( - TenantId(workspaceId1), - SubscriptionId(workspaceId1), - ManagedResourceGroupName(workspaceId1) - ) - ) - ) - .save() - ) - // hidden runtime 2, read workspace 2, owned project 1, Gcp - _ <- IO( - makeCluster(2) - .copy( - samResource = samResource2, - workspaceId = Some(workspace2), - cloudContext = CloudContext.Gcp(GoogleProject(projectIdGcp1)) - ) - .save() - ) - // read runtime 3, read workspace 2, owned project 1, Gcp - _ <- IO( - makeCluster(3) - .copy( - samResource = samResource3, - workspaceId = Some(workspace2), - cloudContext = CloudContext.Gcp(GoogleProject(projectIdGcp1)) - ) - .save() - ) - // read runtime 4, read workspace 3, Azure - _ <- IO( - makeCluster(4) - .copy( - samResource = samResource4, - workspaceId = Some(workspace3), - cloudContext = CloudContext.Azure( - AzureCloudContext( - TenantId(workspaceId3), - SubscriptionId(workspaceId3), - ManagedResourceGroupName(workspaceId3) - ) - ) - ) - .save() - ) - // read runtime 5, read project 2, Gcp - _ <- IO( - makeCluster(5) - .copy(samResource = samResource5, cloudContext = CloudContext.Gcp(GoogleProject(projectIdGcp2))) - .save() - ) - - responseIdsWorkspace1 <- testService.listRuntimes(userInfo, Some(workspace1), None, Map.empty) - responseIdsWorkspace2 <- testService.listRuntimes(userInfo, Some(workspace2), None, Map.empty) - responseIdsWorkspace3 <- testService.listRuntimes(userInfo, Some(workspace3), None, Map.empty) - responseIdsAzure <- testService.listRuntimes(userInfo, None, Some(CloudProvider.Azure), Map.empty) - responseIdsGcp <- testService.listRuntimes(userInfo, None, Some(CloudProvider.Gcp), Map.empty) - responseIdsAzureWorkspace1 <- testService.listRuntimes(userInfo, - Some(workspace1), - Some(CloudProvider.Azure), - Map.empty - ) - responseIdsAzureWorkspace2 <- testService.listRuntimes(userInfo, - Some(workspace2), - Some(CloudProvider.Azure), - Map.empty - ) - responseIdsGcpWorkspace1 <- testService.listRuntimes(userInfo, - Some(workspace1), - Some(CloudProvider.Gcp), - Map.empty - ) - responseIdsGcpWorkspace2 <- testService.listRuntimes(userInfo, - Some(workspace2), - Some(CloudProvider.Gcp), - Map.empty - ) - } yield { - responseIdsWorkspace1.map(_.samResource).toSet shouldBe Set(samResource1) - responseIdsWorkspace2.map(_.samResource).toSet shouldBe Set(samResource2, samResource3) - responseIdsWorkspace3.map(_.samResource).toSet shouldBe Set(samResource4) - responseIdsAzure.map(_.samResource).toSet shouldBe Set(samResource1, samResource4) - responseIdsGcp.map(_.samResource).toSet shouldBe Set(samResource2, samResource3, samResource5) - responseIdsAzureWorkspace1.map(_.samResource).toSet shouldBe Set(samResource1) - responseIdsAzureWorkspace2.map(_.samResource).toSet shouldBe Set.empty - responseIdsGcpWorkspace1.map(_.samResource).toSet shouldBe Set.empty - responseIdsGcpWorkspace2.map(_.samResource).toSet shouldBe Set(samResource2, samResource3) - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "list runtimes with parameters" in isolatedDbTest { - val runtimeId1 = RuntimeSamResourceId(UUID.randomUUID.toString) - val runtimeId2 = RuntimeSamResourceId(UUID.randomUUID.toString) - val workspaceId1 = WorkspaceId(UUID.randomUUID) - - val samService = mock[SamService[IO]] - when(samService.listResources(any(), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List(runtimeId1.resourceId, runtimeId2.resourceId))) - val testService = makeInterp(samService = samService) - val res = for { - samResource1 <- IO(runtimeId1) - samResource2 <- IO(runtimeId2) - runtime1 <- IO( - makeCluster(1) - .copy(samResource = samResource1, workspaceId = Some(workspaceId1)) - .save() - ) - _ <- setRuntimeDeleted(workspaceId1, runtime1.runtimeName) - - runtime2 <- IO(makeCluster(2).copy(samResource = samResource2, workspaceId = Some(workspaceId1)).save()) - _ <- labelQuery.save(runtime2.id, LabelResourceType.Runtime, "foo", "bar").transaction - listResponse1 <- testService.listRuntimes( - userInfo, - None, - None, - Map("foo" -> "bar") - ) // hit - listResponse2 <- testService.listRuntimes( - userInfo, - None, - None, - Map("FOO" -> "BAR") - ) // hit, case insensitive - listResponse3 <- testService.listRuntimes( - userInfo, - None, - None, - Map("foo!@#$%^&*()_+=';:\"" -> "!@#$%^&*()_+=';:\"bar") - ) // miss, with weird characters - listResponse4 <- testService.listRuntimes( - userInfo, - None, - None, - Map("foo" -> "not-bar") - ) // miss value - listResponse5 <- testService.listRuntimes( - userInfo, - None, - None, - Map("not-foo" -> "bar") - ) // miss key - } yield { - listResponse1.map(_.samResource).toSet shouldBe Set(samResource2) - listResponse2.map(_.samResource).toSet shouldBe Set(samResource2) - listResponse3.map(_.samResource).toSet shouldBe Set.empty - listResponse4.map(_.samResource).toSet shouldBe Set.empty - listResponse5.map(_.samResource).toSet shouldBe Set.empty - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "list runtimes filtered by creator" in isolatedDbTest { - val wsmId1 = WsmResourceSamResourceId(WsmControlledResourceId(UUID.randomUUID)) - val runtimeId2 = RuntimeSamResourceId(UUID.randomUUID.toString) - val runtimeId3 = RuntimeSamResourceId(UUID.randomUUID.toString) - val runtimeId4 = RuntimeSamResourceId(UUID.randomUUID.toString) - val workspaceId1 = WorkspaceId(UUID.randomUUID) - val userInfoCreator = mockUserInfo("karen@styx.hel") - val userInfoOther = mockUserInfo("mike@heavn.io") - val samService = mock[SamService[IO]] - when( - samService.listResources(isEq(userInfoCreator.accessToken.token), isEq(RuntimeSamResource.resourceType))(any()) - ) - .thenReturn(IO.pure(List(wsmId1.resourceId, runtimeId3.resourceId))) - - val testService = makeInterp(samService = samService) - val res = for { - // runtime 1: I created, in a workspace I can read => visible - samResource1 <- IO(RuntimeSamResourceId(wsmId1.resourceId.toString)) - runtime1 <- IO( - makeCluster(1, Some(userInfoCreator.userEmail)) - .copy(samResource = samResource1, workspaceId = Some(workspaceId1)) - .save() - ) - - // runtime 2: I created, but I don't have permission => hidden - samResource2 <- IO(runtimeId2) - runtime2 <- IO( - makeCluster(2, Some(userInfoCreator.userEmail)) - .copy(samResource = samResource2, workspaceId = Some(WorkspaceId(UUID.randomUUID))) - .save() - ) - - // runtime 3: someone else created, but I can read => hidden if role=creator, else visible - samResource3 <- IO(runtimeId3) - runtime3 <- IO( - makeCluster(3, Some(userInfoOther.userEmail)) - .copy(samResource = samResource3, workspaceId = Some(workspaceId1)) - .save() - ) - - listResponseCreator <- testService.listRuntimes(userInfoCreator, None, None, Map("role" -> "creator")) - listResponseAny <- testService.listRuntimes(userInfoCreator, None, None, Map.empty) - } yield { - listResponseCreator.map(_.samResource).toSet shouldBe Set(samResource1) - listResponseAny.map(_.samResource).toSet shouldBe Set(samResource1, samResource3) - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // See https://broadworkbench.atlassian.net/browse/PROD-440 - // AoU relies on the ability for project owners to list other users' runtimes. - it should "list runtimes belonging to other users" in isolatedDbTest { - val runtimeId1 = RuntimeSamResourceId(UUID.randomUUID.toString) - val runtimeId2 = RuntimeSamResourceId(UUID.randomUUID.toString) - val workspaceId1 = WorkspaceId(UUID.randomUUID) - val userInfo = mockUserInfo("karen@styx.hel") - val samService = mock[SamService[IO]] - when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) - .thenReturn(IO.pure(List(runtimeId1.resourceId, runtimeId2.resourceId))) - - val testService = makeInterp(samService = samService) - - // Make runtimes belonging to different users than the calling user - val res = for { - samResource1 <- IO(runtimeId1) - samResource2 <- IO(runtimeId2) - runtime1 = LeoLenses.runtimeToCreator.replace(WorkbenchEmail("different_user1@example.com"))( - makeCluster(1).copy(samResource = samResource1, workspaceId = Some(workspaceId1)) - ) - runtime2 = LeoLenses.runtimeToCreator.replace(WorkbenchEmail("different_user2@example.com"))( - makeCluster(2).copy(samResource = samResource2, workspaceId = Some(workspaceId1)) - ) - _ <- IO(runtime1.save()) - _ <- IO(runtime2.save()) - listResponse <- testService.listRuntimes(userInfo, None, None, Map.empty) - } yield listResponse.map(_.samResource).toSet shouldBe Set(samResource1, samResource2) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "list runtimes, rejecting invalid label parameters" in isolatedDbTest { - runtimeV2Service - .listRuntimes(userInfo, None, None, Map("_labels" -> "foo=bar;bam=yes")) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - .isInstanceOf[ParseLabelsException] shouldBe true - runtimeV2Service - .listRuntimes(userInfo, None, None, Map("_labels" -> "foo=bar,bam")) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - .isInstanceOf[ParseLabelsException] shouldBe true - - runtimeV2Service - .listRuntimes(userInfo, None, None, Map("_labels" -> "bogus")) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - .isInstanceOf[ParseLabelsException] shouldBe true - - runtimeV2Service - .listRuntimes(userInfo, None, None, Map("_labels" -> "a,b")) - .attempt - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - .swap - .toOption - .get - .isInstanceOf[ParseLabelsException] shouldBe true - } - - it should "update date accessed when user has permission" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val publisherQueue = QueueFactory.makePublisherQueue() - val dateAccessedQueue = QueueFactory.makeDateAccessedQueue() - val azureService = makeInterp(publisherQueue, dateAccessedQueue = dateAccessedQueue) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - azureCloudContext <- wsmClientProvider.getWorkspace("token", workspaceId).map(_.get.azureContext) - clusterOpt <- clusterQuery - .getActiveClusterByNameMinimal(CloudContext.Azure(azureCloudContext.get), runtimeName)( - scala.concurrent.ExecutionContext.global - ) - .transaction - cluster = clusterOpt.get - - ctx <- appContext.ask[AppContext] - _ <- azureService.updateDateAccessed(userInfo, workspaceId, runtimeName) - msg <- dateAccessedQueue.tryTake - } yield msg shouldBe Some( - UpdateDateAccessedMessage(UpdateTarget.Runtime(runtimeName), cluster.cloudContext, ctx.now) - ) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not update date accessed when user doesn't have permission" in isolatedDbTest { - val runtimeName = RuntimeName("clusterName1") - val workspaceId = WorkspaceId(UUID.randomUUID()) - - val samService = mockSamForCreateRuntime(userInfo) - when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.ModifyRuntime))( - any() - ) - ).thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) - val publisherQueue = QueueFactory.makePublisherQueue() - val dateAccessedQueue = QueueFactory.makeDateAccessedQueue() - val azureService = makeInterp(publisherQueue, dateAccessedQueue = dateAccessedQueue, samService = samService) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - - _ <- azureService - .createRuntime( - userInfo, - runtimeName, - workspaceId, - false, - defaultCreateAzureRuntimeReq - ) - _ <- azureService.updateDateAccessed(userInfo, workspaceId, runtimeName) - } yield () - - val thrown = the[ForbiddenError] thrownBy { - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - thrown shouldBe ForbiddenError(userInfo.userEmail) - } -} - -object TestContext extends Enumeration { - type Context = Value - val GoogleProject, GoogleWorkspace, AzureWorkspace = Value -} - -object TestContextAccess extends Enumeration { - type Role = Value - val Nothing, Reader, Owner = Value -} - -object TestRuntimeAccess extends Enumeration { - type Role = Value - val Nothing, Reader = Value -} From 048b5e52d392f8a30f425dbce37990f14a010e22 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 3 Jul 2025 11:39:49 -0400 Subject: [PATCH 07/43] Remove disk v2 routes and disk v2 service --- .../http/AppDependenciesBuilder.scala | 6 - .../dsde/workbench/leonardo/http/Boot.scala | 1 - .../http/CloudDependenciesBuilder.scala | 1 - .../leonardo/http/api/DiskV2Routes.scala | 105 ------ .../leonardo/http/api/HttpRoutes.scala | 11 +- .../leonardo/http/service/DiskV2Service.scala | 19 - .../http/service/DiskV2ServiceInterp.scala | 128 ------- .../service/DiskV2ServiceInterpSpec.scala | 342 ------------------ .../service/MockDiskV2ServiceInterp.scala | 37 -- .../leonardo/provider/LeoProvider.scala | 6 - 10 files changed, 3 insertions(+), 653 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/DiskV2Routes.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2Service.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterp.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterpSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockDiskV2ServiceInterp.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 46562af924..27fdbba39e 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -90,11 +90,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu dbReference: DbReference[IO] ): Resource[IO, ServicesDependencies] = { val statusService = new StatusService(baselineDependencies.samDAO, dbReference) - val diskV2Service = new DiskV2ServiceInterp[IO]( - baselineDependencies.publisherQueue, - baselineDependencies.wsmClientProvider, - baselineDependencies.samService - ) val adminService = new AdminServiceInterp[IO](baselineDependencies.authProvider, baselineDependencies.publisherQueue) @@ -110,7 +105,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu ServicesDependencies( statusService, dependenciesRegistry, - diskV2Service, leoKubernetesService, adminService, StandardUserInfoDirectives, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala index 65f98d064b..8b7837f783 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala @@ -62,7 +62,6 @@ object Boot extends IOApp { servicesDependencies.baselineDependencies.openIDConnectConfiguration, servicesDependencies.statusService, servicesDependencies.cloudSpecificDependenciesRegistry, - servicesDependencies.diskV2Service, servicesDependencies.kubernetesService, servicesDependencies.adminService, StandardUserInfoDirectives, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala index ec4fe27504..e27dace6c8 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala @@ -72,7 +72,6 @@ final case class LeoAppDependencies( final case class ServicesDependencies( statusService: StatusService, cloudSpecificDependenciesRegistry: ServicesRegistry, - diskV2Service: DiskV2Service[IO], kubernetesService: AppService[IO], adminService: AdminService[IO], userInfoDirectives: UserInfoDirectives, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/DiskV2Routes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/DiskV2Routes.scala deleted file mode 100644 index 7f3cb9244b..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/DiskV2Routes.scala +++ /dev/null @@ -1,105 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package api - -import java.util.UUID -import akka.http.scaladsl.marshalling.ToResponseMarshallable -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server -import akka.http.scaladsl.server.Directives._ -import cats.effect.IO -import cats.mtl.Ask -import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ -import io.circe.Encoder -import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ -import io.opencensus.scala.akka.http.TracingDirective.traceRequestForService -import org.broadinstitute.dsde.workbench.leonardo.http.api.DiskV2Routes._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.DiskV2Service -import org.broadinstitute.dsde.workbench.model.{TraceId, UserInfo} -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics - -class DiskV2Routes(diskV2Service: DiskV2Service[IO], userInfoDirectives: UserInfoDirectives)(implicit - metrics: OpenTelemetryMetrics[IO] -) { - val routes: server.Route = traceRequestForService(serviceData) { span => - extractAppContext(Some(span)) { implicit ctx => - userInfoDirectives.requireUserInfo { userInfo => - CookieSupport.setTokenCookie(userInfo) { - implicit val traceId = Ask.const[IO, TraceId](TraceId(UUID.randomUUID())) - pathPrefix("v2" / "disks" / diskIdSegment) { diskId => - pathEndOrSingleSlash { - get { - complete( - getDiskV2Handler(userInfo, diskId) - ) - } ~ - delete { - complete( - deleteDiskV2Handler(userInfo, diskId) - ) - } - } - } - } - } - } - } - private[api] def getDiskV2Handler(userInfo: UserInfo, diskId: DiskId)(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = diskV2Service.getDisk(userInfo, diskId) - _ <- metrics.incrementCounter("getDiskV2") - resp <- ctx.span.fold(apiCall)(span => - spanResource[IO](span, "getDiskV2") - .use(_ => apiCall) - ) - } yield StatusCodes.OK -> resp: ToResponseMarshallable - - private[api] def deleteDiskV2Handler(userInfo: UserInfo, diskId: DiskId)(implicit - ev: Ask[IO, AppContext] - ): IO[ToResponseMarshallable] = - for { - ctx <- ev.ask[AppContext] - apiCall = diskV2Service.deleteDisk(userInfo, diskId) - _ <- metrics.incrementCounter("deleteDiskV2") - _ <- ctx.span.fold(apiCall)(span => spanResource[IO](span, "deleteDiskV2").use(_ => apiCall)) - } yield StatusCodes.Accepted -} - -object DiskV2Routes { - implicit val getPersistentDiskV2ResponseEncoder: Encoder[GetPersistentDiskV2Response] = Encoder.forProduct14( - "id", - "cloudContext", - "zone", - "name", - "serviceAccount", - "samResource", - "status", - "auditInfo", - "size", - "diskType", - "blockSize", - "labels", - "workspaceId", - "formattedBy" - )(x => - ( - x.id, - x.cloudContext, - x.zone, - x.name, - x.serviceAccount, - x.samResource, - x.status, - x.auditInfo, - x.size, - x.diskType, - x.blockSize, - x.labels, - x.workspaceId, - x.formattedBy - ) - ) -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala index c3061c7853..6978a5f6a4 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala @@ -31,7 +31,6 @@ class HttpRoutes( oidcConfig: OpenIDConnectConfiguration, statusService: StatusService, gcpOnlyServicesRegistry: ServicesRegistry, - diskV2Service: DiskV2Service[IO], kubernetesService: AppService[IO], adminService: AdminService[IO], userInfoDirectives: UserInfoDirectives, @@ -44,7 +43,6 @@ class HttpRoutes( private val corsSupport = new CorsSupport(contentSecurityPolicy, refererConfig) private val kubernetesRoutes = new AppRoutes(kubernetesService, userInfoDirectives) private val appRoutes = createAppRoutesUsingServicesRegistry - private val diskV2Routes = new DiskV2Routes(diskV2Service, userInfoDirectives) private val adminRoutes = new AdminRoutes(adminService, userInfoDirectives) private val diskRoutes = createDiskRoutesUsingServicesRegistry private val runtimeRoutes = createRuntimeRoutesUsingServicesRegistry @@ -121,9 +119,8 @@ class HttpRoutes( "swagger/api-docs.yaml" ) ~ oidcConfig.oauth2Routes ~ proxyRoutes.get.route ~ statusRoutes.route ~ pathPrefix("api") { - runtimeRoutes.get.routes ~ - diskRoutes.get.routes ~ kubernetesRoutes.routes ~ diskV2Routes.routes ~ adminRoutes.routes ~ - resourcesRoutes.get.routes + runtimeRoutes.get.routes ~ diskRoutes.get.routes ~ kubernetesRoutes.routes ~ + adminRoutes.routes ~ resourcesRoutes.get.routes } ) case true => @@ -131,9 +128,7 @@ class HttpRoutes( oidcConfig .swaggerRoutes("swagger/api-docs.yaml") ~ oidcConfig.oauth2Routes ~ statusRoutes.route ~ pathPrefix("api") { - runtimeRoutes.get.routes ~ - diskRoutes.get.routes ~ diskV2Routes.routes ~ - appRoutes.get.routes ~ adminRoutes.routes + runtimeRoutes.get.routes ~ diskRoutes.get.routes ~ appRoutes.get.routes ~ adminRoutes.routes } ) } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2Service.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2Service.scala deleted file mode 100644 index f27653e046..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2Service.scala +++ /dev/null @@ -1,19 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.model.UserInfo - -trait DiskV2Service[F[_]] { - - // TODO: Implement rest of v2 routes: createDisk, listDisks, updateDisk - def getDisk(userInfo: UserInfo, diskId: DiskId)(implicit - as: Ask[F, AppContext] - ): F[GetPersistentDiskV2Response] - - def deleteDisk(userInfo: UserInfo, diskId: DiskId)(implicit - as: Ask[F, AppContext] - ): F[Unit] - -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterp.scala deleted file mode 100644 index f4640e4245..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterp.scala +++ /dev/null @@ -1,128 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import akka.http.scaladsl.model.StatusCodes -import cats.Parallel -import cats.effect.Async -import cats.effect.std.Queue -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamService, SamUtils} -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.model._ -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.DeleteDiskV2Message -import org.broadinstitute.dsde.workbench.model.{TraceId, UserInfo} -import org.typelevel.log4cats.StructuredLogger - -import scala.concurrent.ExecutionContext - -class DiskV2ServiceInterp[F[_]: Parallel]( - publisherQueue: Queue[F, LeoPubsubMessage], - wsmClientProvider: WsmApiClientProvider[F], - samService: SamService[F] -)(implicit - F: Async[F], - dbReference: DbReference[F], - ec: ExecutionContext, - log: StructuredLogger[F] -) extends DiskV2Service[F] { - - // backwards compatible with v1 getDisk route - override def getDisk(userInfo: UserInfo, diskId: DiskId)(implicit - as: Ask[F, AppContext] - ): F[GetPersistentDiskV2Response] = - for { - ctx <- as.ask - diskResp <- DiskServiceDbQueries - .getGetPersistentDiskResponseV2(diskId, ctx.traceId) - .transaction - - // check that workspaceId is not null - _ <- F.fromOption(diskResp.workspaceId, DiskWithoutWorkspaceException(diskId, ctx.traceId)) - - // check that user has read action on disk - _ <- SamUtils.checkDiskAction(samService, - userInfo, - diskId, - diskResp.samResource, - PersistentDiskAction.ReadPersistentDisk, - ctx.traceId - ) - - } yield diskResp - - override def deleteDisk(userInfo: UserInfo, diskId: DiskId)(implicit - as: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- as.ask - diskOpt <- persistentDiskQuery.getActiveById(diskId).transaction - - disk <- F.fromOption(diskOpt, DiskNotFoundByIdException(diskId, ctx.traceId)) - - _ <- SamUtils.checkDiskAction(samService, - userInfo, - diskId, - disk.samResource, - PersistentDiskAction.DeletePersistentDisk, - ctx.traceId - ) - - _ <- ctx.span.traverse(s => F.delay(s.addAnnotation("Done auth call for delete azure disk permission"))) - - // check that workspaceId is not null - workspaceId <- F.fromOption(disk.workspaceId, DiskWithoutWorkspaceException(diskId, ctx.traceId)) - - // check if disk resource is deletable in WSM - wsmDiskResourceId <- disk.wsmResourceId match { - case Some(wsmResourceId) => - for { - wsmStatus <- wsmClientProvider.getWsmState(userInfo.accessToken.token, - workspaceId, - wsmResourceId, - WsmResourceType.AzureDisk - ) - _ <- F.raiseUnless(wsmStatus.isDeletable)( - DiskCannotBeDeletedWsmException(disk.id, wsmStatus, disk.cloudContext, ctx.traceId) - ) - // only send wsmResourceId to back leo if disk isn't already deleted in WSM - } yield if (wsmStatus.isDeleted) None else Some(wsmResourceId) - // if disk hasn't been created in WSM, don't pass id to back leo - case None => F.pure(None) - } - - // check that disk isn't attached to a runtime - isAttached <- persistentDiskQuery.isDiskAttached(diskId).transaction - _ <- F - .raiseError[Unit](DiskCannotBeDeletedAttachedException(diskId, workspaceId, ctx.traceId)) - .whenA(isAttached) - - _ <- persistentDiskQuery.markPendingDeletion(disk.id, ctx.now).transaction - - _ <- publisherQueue.offer( - DeleteDiskV2Message( - disk.id, - workspaceId, - disk.cloudContext, - wsmDiskResourceId, - Some(ctx.traceId) - ) - ) - } yield () -} -case class DiskCannotBeDeletedAttachedException(id: DiskId, workspaceId: WorkspaceId, traceId: TraceId) - extends LeoException( - s"Persistent disk ${id.value} in workspace ${workspaceId.value} cannot be deleted. Disk is still attached to a runtime", - StatusCodes.Conflict, - traceId = Some(traceId) - ) - -case class DiskWithoutWorkspaceException(id: DiskId, traceId: TraceId) - extends LeoException( - s"Persistent disk ${id.value} cannot be deleted. Disk record has no workspaceId", - StatusCodes.Conflict, - traceId = Some(traceId) - ) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterpSpec.scala deleted file mode 100644 index 47cfaf14e2..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskV2ServiceInterpSpec.scala +++ /dev/null @@ -1,342 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.model.headers.OAuth2BearerToken -import cats.effect.IO -import cats.effect.std.Queue -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.google2.{MachineTypeName, ZoneName} -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.PersistentDiskSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamException, SamService} -import org.broadinstitute.dsde.workbench.leonardo.dao.{MockWsmClientProvider, MockWsmDAO, WsmApiClientProvider, WsmDao} -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.model.ForbiddenError -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.DeleteDiskV2Message -import org.broadinstitute.dsde.workbench.leonardo.util.QueueFactory -import org.broadinstitute.dsde.workbench.model.{UserInfo, WorkbenchEmail, WorkbenchUserId} -import org.mockito.Mockito.when -import org.mockito.ArgumentMatchers.{any, eq => isEq} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatestplus.mockito.MockitoSugar.mock -import org.typelevel.log4cats.StructuredLogger - -import java.util.UUID -import scala.concurrent.ExecutionContext.Implicits.global - -class DiskV2ServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent { - val wsmDao = new MockWsmDAO - val wsmClientProvider = new MockWsmClientProvider - - private def makeDiskV2Service(queue: Queue[IO, LeoPubsubMessage], - wsmDao: WsmDao[IO] = wsmDao, - wsmClientProvider: WsmApiClientProvider[IO] = wsmClientProvider, - samService: SamService[IO] = MockSamService - ) = - new DiskV2ServiceInterp[IO]( - queue, - wsmClientProvider, - samService - ) - - val diskV2Service = makeDiskV2Service(QueueFactory.makePublisherQueue(), wsmDao = new MockWsmDAO) - - it should "get a disk" in isolatedDbTest { - val userInfo = UserInfo(OAuth2BearerToken(""), - WorkbenchUserId("userId"), - WorkbenchEmail("user1@example.com"), - 0 - ) // this email is allowlisted - val publisherQueue = QueueFactory.makePublisherQueue() - val samService = mock[SamService[IO]] - val diskV2Service = makeDiskV2Service(publisherQueue, samService = samService) - - val res = for { - _ <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with - pd <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - _ = when( - samService.checkAuthorized(any(), isEq(pd.samResource), isEq(PersistentDiskAction.ReadPersistentDisk))(any()) - ).thenReturn(IO.unit) - - getResponse <- diskV2Service - .getDisk( - userInfo, - pd.id - ) - } yield { - getResponse.id shouldBe pd.id - getResponse.auditInfo.creator shouldBe userInfo.userEmail - - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail with DiskNotFound if disk does not exist" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val diskV2Service = makeDiskV2Service(publisherQueue) - val userInfo = UserInfo(OAuth2BearerToken(""), - WorkbenchUserId("userId"), - WorkbenchEmail("user1@example.com"), - 0 - ) // this email is allow-listed - val diskId = DiskId(-1) - - val res = for { - ctx <- appContext.ask[AppContext] - - getResponse <- diskV2Service - .getDisk( - userInfo, - diskId - ) - .attempt - } yield getResponse shouldBe Left(DiskNotFoundByIdException(diskId, ctx.traceId)) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail with DiskNotFound if user doesn't have permission" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val samService = mock[SamService[IO]] - val diskV2Service = makeDiskV2Service(publisherQueue, samService = samService) - val userInfo = - UserInfo(OAuth2BearerToken(""), WorkbenchUserId("stranger"), WorkbenchEmail("stranger@example.com"), 0) - - val res = for { - ctx <- appContext.ask[AppContext] - pd <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - _ = when( - samService.checkAuthorized(any(), isEq(pd.samResource), isEq(PersistentDiskAction.ReadPersistentDisk))(any()) - ).thenReturn(IO.raiseError(SamException.create("forbidden", StatusCodes.Forbidden.intValue, ctx.traceId))) - - getResponse <- diskV2Service - .getDisk( - userInfo, - pd.id - ) - .attempt - } yield getResponse shouldBe Left(DiskNotFoundByIdException(pd.id, ctx.traceId)) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete a disk" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val samService = mock[SamService[IO]] - when(samService.checkAuthorized(any(), any(), isEq(PersistentDiskAction.DeletePersistentDisk))(any())) - .thenReturn(IO.unit) - val diskV2Service = makeDiskV2Service(publisherQueue, samService = samService) - - val res = for { - ctx <- appContext.ask[AppContext] - diskSamResource <- IO(PersistentDiskSamResourceId(UUID.randomUUID.toString)) - disk <- makePersistentDisk() - .copy(samResource = diskSamResource) - .save() - - _ <- persistentDiskQuery.updateWSMResourceId(disk.id, wsmResourceId, ctx.now).transaction - - _ <- diskV2Service.deleteDisk(userInfo, disk.id) - dbDiskOpt <- persistentDiskQuery - .getActiveByName(disk.cloudContext, disk.name) - .transaction - dbDisk = dbDiskOpt.get - message <- publisherQueue.take - } yield { - dbDisk.status shouldBe DiskStatus.Deleting - message shouldBe DeleteDiskV2Message(disk.id, - workspaceId, - disk.cloudContext, - disk.wsmResourceId, - Some(ctx.traceId) - ) - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to delete a disk if its creating" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - IO.pure(WsmState(Some("CREATING"))) - } - - val samService = mock[SamService[IO]] - when(samService.checkAuthorized(any(), any(), isEq(PersistentDiskAction.DeletePersistentDisk))(any())) - .thenReturn(IO.unit) - val diskV2Service = - makeDiskV2Service(publisherQueue, wsmClientProvider = wsmClientProvider, samService = samService) - - val res = for { - ctx <- appContext.ask[AppContext] - diskSamResource <- IO(PersistentDiskSamResourceId(UUID.randomUUID.toString)) - disk <- makePersistentDisk(cloudContextOpt = Some(cloudContextAzure)).copy(samResource = diskSamResource).save() - - err <- diskV2Service.deleteDisk(userInfo, disk.id).attempt - } yield err shouldBe Left( - DiskCannotBeDeletedWsmException(disk.id, WsmState(Some("CREATING")), cloudContextAzure, ctx.traceId) - ) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete a disk and not send wsmResourceId if disk is deleted in WSM" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val wsmClientProvider = new MockWsmClientProvider() { - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - wsmResourceType: WsmResourceType - )(implicit ev: Ask[IO, AppContext], log: StructuredLogger[IO]): IO[WsmState] = - IO.pure(WsmState(None)) - } - - val samService = mock[SamService[IO]] - when(samService.checkAuthorized(any(), any(), isEq(PersistentDiskAction.DeletePersistentDisk))(any())) - .thenReturn(IO.unit) - val diskV2Service = - makeDiskV2Service(publisherQueue, wsmClientProvider = wsmClientProvider, samService = samService) - - val res = for { - ctx <- appContext.ask[AppContext] - diskSamResource <- IO(PersistentDiskSamResourceId(UUID.randomUUID.toString)) - disk <- makePersistentDisk(cloudContextOpt = Some(cloudContextAzure)).copy(samResource = diskSamResource).save() - - _ <- diskV2Service.deleteDisk(userInfo, disk.id) - message <- publisherQueue.take - } yield message shouldBe DeleteDiskV2Message( - disk.id, - workspaceId, - cloudContextAzure, - None, - Some(ctx.traceId) - ) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to delete a disk if it is attached to a runtime" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val samService = mock[SamService[IO]] - when(samService.checkAuthorized(any(), any(), isEq(PersistentDiskAction.DeletePersistentDisk))(any())) - .thenReturn(IO.unit) - val diskV2Service = makeDiskV2Service(publisherQueue, samService = samService) - - val res = for { - ctx <- appContext.ask[AppContext] - diskSamResource <- IO(PersistentDiskSamResourceId(UUID.randomUUID.toString)) - disk <- makePersistentDisk(None).copy(samResource = diskSamResource).save() - - _ <- IO( - makeCluster(1).saveWithRuntimeConfig( - RuntimeConfig.GceWithPdConfig(MachineTypeName("n1-standard-4"), - Some(disk.id), - bootDiskSize = DiskSize(50), - zone = ZoneName("us-west2-b"), - None - ) - ) - ) - err <- diskV2Service.deleteDisk(userInfo, disk.id).attempt - } yield err shouldBe Left(DiskCannotBeDeletedAttachedException(disk.id, workspaceId, ctx.traceId)) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to delete a disk if it has no workspaceId" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val samService = mock[SamService[IO]] - when(samService.checkAuthorized(any(), any(), isEq(PersistentDiskAction.DeletePersistentDisk))(any())) - .thenReturn(IO.unit) - val diskV2Service = makeDiskV2Service(publisherQueue, samService = samService) - - val res = for { - ctx <- appContext.ask[AppContext] - diskSamResource <- IO(PersistentDiskSamResourceId(UUID.randomUUID.toString)) - disk <- makePersistentDisk(workspaceId = None).copy(samResource = diskSamResource).save() - - _ <- IO( - makeCluster(1).saveWithRuntimeConfig( - RuntimeConfig.AzureConfig(MachineTypeName("n1-standard-4"), Some(disk.id), None) - ) - ) - err <- diskV2Service.deleteDisk(userInfo, disk.id).attempt - } yield err shouldBe Left( - DiskWithoutWorkspaceException(disk.id, ctx.traceId) - ) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "ffail to delete a disk if the user does not have delete access and not reveal its existence if the user cannot read it" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val samService = mock[SamService[IO]] - val diskV2service2 = makeDiskV2Service(publisherQueue, samService = samService) - - val res = for { - ctx <- appContext.ask[AppContext] - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - _ = when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), - isEq(disk.samResource), - isEq(PersistentDiskAction.DeletePersistentDisk) - )(any()) - ).thenReturn(IO.raiseError(SamException.create("forbidden", StatusCodes.Forbidden.intValue, ctx.traceId))) - _ = when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), - isEq(disk.samResource), - isEq(PersistentDiskAction.ReadPersistentDisk) - )(any()) - ).thenReturn(IO.raiseError(SamException.create("forbidden", StatusCodes.Forbidden.intValue, ctx.traceId))) - _ <- IO( - makeCluster(1).saveWithRuntimeConfig( - RuntimeConfig.AzureConfig(MachineTypeName("n1-standard-4"), Some(disk.id), None) - ) - ) - err <- diskV2service2.deleteDisk(userInfo, disk.id).attempt - } yield err shouldBe Left( - DiskNotFoundByIdException(disk.id, ctx.traceId) - ) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to delete a disk if the user does not have delete access" in isolatedDbTest { - val publisherQueue = QueueFactory.makePublisherQueue() - val samService = mock[SamService[IO]] - val diskV2service2 = makeDiskV2Service(publisherQueue, samService = samService) - - val res = for { - ctx <- appContext.ask[AppContext] - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - _ = when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), - isEq(disk.samResource), - isEq(PersistentDiskAction.DeletePersistentDisk) - )(any()) - ).thenReturn(IO.raiseError(SamException.create("forbidden", StatusCodes.Forbidden.intValue, ctx.traceId))) - _ = when( - samService.checkAuthorized(isEq(userInfo.accessToken.token), - isEq(disk.samResource), - isEq(PersistentDiskAction.ReadPersistentDisk) - )(any()) - ).thenReturn(IO.unit) - _ <- IO( - makeCluster(1).saveWithRuntimeConfig( - RuntimeConfig.AzureConfig(MachineTypeName("n1-standard-4"), Some(disk.id), None) - ) - ) - err <- diskV2service2.deleteDisk(userInfo, disk.id).attempt - } yield err shouldBe Left( - ForbiddenError(userInfo.userEmail) - ) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockDiskV2ServiceInterp.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockDiskV2ServiceInterp.scala deleted file mode 100644 index 1250d27bce..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockDiskV2ServiceInterp.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package http -package service - -import cats.effect.IO -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.model.UserInfo - -object MockDiskV2ServiceInterp extends DiskV2Service[IO] { - - def getDisk(userInfo: UserInfo, diskId: DiskId)(implicit - as: Ask[IO, AppContext] - ): IO[GetPersistentDiskV2Response] = - IO.pure( - GetPersistentDiskV2Response( - DiskId(-1), - CommonTestData.cloudContextAzure, - CommonTestData.zone, - CommonTestData.diskName, - CommonTestData.serviceAccount, - CommonTestData.diskSamResource, - DiskStatus.Ready, - CommonTestData.auditInfo, - CommonTestData.diskSize, - CommonTestData.diskType, - CommonTestData.blockSize, - Map.empty, - CommonTestData.workspaceIdOpt, - None - ) - ) - - def deleteDisk(userInfo: UserInfo, diskId: DiskId)(implicit - as: Ask[IO, AppContext] - ): IO[Unit] = IO.unit - -} diff --git a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala index 95a8eea1dd..7de59c7629 100644 --- a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala +++ b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala @@ -41,9 +41,7 @@ class LeoProvider extends AnyFlatSpec with BeforeAndAfterAll with PactVerifier { val mockProxyService: ProxyService = mock[ProxyService] val mockRuntimeService: RuntimeService[IO] = mock[RuntimeService[IO]] val mockDiskService: DiskService[IO] = mock[DiskService[IO]] - val mockDiskV2Service: DiskV2Service[IO] = mock[DiskV2Service[IO]] val mockAppService: AppService[IO] = mock[AppService[IO]] - val mockRuntimeV2Service: RuntimeV2Service[IO] = mock[RuntimeV2Service[IO]] val mockAdminService: AdminService[IO] = mock[AdminService[IO]] val mockResourcesService: ResourcesService[IO] = mock[ResourcesService[IO]] val mockContentSecurityPolicyConfig: ContentSecurityPolicyConfig = mock[ContentSecurityPolicyConfig] @@ -65,9 +63,7 @@ class LeoProvider extends AnyFlatSpec with BeforeAndAfterAll with PactVerifier { mockOpenIDConnectConfiguration, mockStatusService, gcpOnlyServicesRegistry, - mockDiskV2Service, mockAppService, - mockRuntimeV2Service, mockAdminService, mockUserInfoDirectives, mockContentSecurityPolicyConfig, @@ -151,9 +147,7 @@ class LeoProvider extends AnyFlatSpec with BeforeAndAfterAll with PactVerifier { reset(mockProxyService) reset(mockRuntimeService) reset(mockDiskService) - reset(mockDiskV2Service) reset(mockAppService) - reset(mockRuntimeV2Service) reset(mockAdminService) reset(mockContentSecurityPolicyConfig) when(metrics.incrementCounter(anyString(), anyLong(), any())).thenReturn(IO.pure(None)) From cbaafc96d22c18cf914c18f5fd95465c1d7d6a8e Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 3 Jul 2025 15:07:50 -0400 Subject: [PATCH 08/43] Cleanup --- .../workbench/leonardo/CommonTestData.scala | 51 +++---------------- .../leonardo/http/ConfigReaderSpec.scala | 1 - .../leonardo/http/api/HttpRoutesSpec.scala | 14 ----- .../leonardo/http/api/ProxyRoutesSpec.scala | 4 -- .../leonardo/http/api/TestLeoRoutes.scala | 16 +----- .../service/RuntimeServiceInterpSpec.scala | 3 +- 6 files changed, 8 insertions(+), 81 deletions(-) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index cdcce4ca6a..9650bd4800 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -13,47 +13,16 @@ import com.google.cloud.compute.v1._ import com.typesafe.config.ConfigFactory import net.ceedubs.ficus.Ficus._ import org.broadinstitute.dsde.workbench.google2.mock.BaseFakeGoogleStorage -import org.broadinstitute.dsde.workbench.google2.{ - DataprocRole, - DiskName, - MachineTypeName, - NetworkName, - OperationName, - RegionName, - SubnetworkName, - ZoneName -} +import org.broadinstitute.dsde.workbench.google2.{DataprocRole, DiskName, MachineTypeName, NetworkName, OperationName, RegionName, SubnetworkName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo import org.broadinstitute.dsde.workbench.leonardo.ContainerRegistry.DockerHub -import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{ - BootSource, - CryptoDetector, - Jupyter, - Proxy, - RStudio, - Welder -} +import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{BootSource, CryptoDetector, Jupyter, Proxy, RStudio, Welder} import org.broadinstitute.dsde.workbench.leonardo.SamResourceId._ import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config._ -import org.broadinstitute.dsde.workbench.leonardo.dao.{ - AccessScope, - CloningInstructions, - ControlledResourceDescription, - ControlledResourceIamRole, - ControlledResourceName, - InternalDaoControlledResourceCommonFields, - ManagedBy, - MockSamDAO, - PrivateResourceUser -} +import org.broadinstitute.dsde.workbench.leonardo.dao.{AccessScope, CloningInstructions, ControlledResourceDescription, ControlledResourceIamRole, ControlledResourceName, InternalDaoControlledResourceCommonFields, ManagedBy, MockSamDAO, PrivateResourceUser} import org.broadinstitute.dsde.workbench.leonardo.db.ClusterRecord -import org.broadinstitute.dsde.workbench.leonardo.http.{ - userScriptStartupOutputUriMetadataKey, - ConfigReader, - CreateRuntimeRequest, - RuntimeConfigRequest -} +import org.broadinstitute.dsde.workbench.leonardo.http.{ConfigReader, CreateRuntimeRequest, RuntimeConfigRequest, userScriptStartupOutputUriMetadataKey} import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google._ @@ -62,16 +31,8 @@ import java.time.Instant import java.time.temporal.ChronoUnit import java.util.{Date, UUID} import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes -import org.broadinstitute.dsde.workbench.azure.{ - ApplicationInsightsName, - AzureCloudContext, - BatchAccountName, - ManagedResourceGroupName, - RelayNamespace, - SubscriptionId, - TenantId -} -import org.broadinstitute.dsde.workbench.leonardo.http.service.AzureServiceConfig +import org.broadinstitute.dsde.workbench.azure.{ApplicationInsightsName, AzureCloudContext, BatchAccountName, ManagedResourceGroupName, RelayNamespace, SubscriptionId, TenantId} +import org.broadinstitute.dsde.workbench.leonardo.util.AzureServiceConfig import org.broadinstitute.dsde.workbench.oauth2.mock.FakeOpenIDConnectConfiguration import org.broadinstitute.dsde.workbench.util2.InstanceName diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala index d3d47327e7..8a34bba89e 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala @@ -6,7 +6,6 @@ import org.broadinstitute.dsde.workbench.azure.{AzureAppRegistrationConfig, Azur import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName import org.broadinstitute.dsde.workbench.google2.ZoneName import org.broadinstitute.dsde.workbench.leonardo.config._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.{AzureRuntimeDefaults, CustomScriptExtensionConfig, VMCredential} import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoMetricsMonitorConfig, PollMonitorConfig} import org.broadinstitute.dsde.workbench.leonardo.util.{AzurePubsubHandlerConfig, AzureRuntimeDefaults, CustomScriptExtensionConfig, TerraAppSetupChartConfig, VMCredential} import org.broadinstitute.dsp._ diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala index 44941c71f2..352f6f1c0b 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala @@ -62,9 +62,7 @@ class HttpRoutesSpec openIdConnectionConfiguration, statusService, createGcpOnlyServicesRegistry(), - MockDiskV2ServiceInterp, MockAppService, - new MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -75,9 +73,7 @@ class HttpRoutesSpec openIdConnectionConfiguration, statusService, createGcpOnlyServicesRegistry(), - MockDiskV2ServiceInterp, MockAppService, - new MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -90,9 +86,7 @@ class HttpRoutesSpec openIdConnectionConfiguration, statusService, createGcpOnlyServicesRegistry(), - MockDiskV2ServiceInterp, MockAppService, - new MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -104,9 +98,7 @@ class HttpRoutesSpec openIdConnectionConfiguration, statusService, createGcpOnlyServicesRegistry(), - MockDiskV2ServiceInterp, MockAppService, - new MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -118,9 +110,7 @@ class HttpRoutesSpec openIdConnectionConfiguration, statusService, createGcpOnlyServicesRegistry(), - MockDiskV2ServiceInterp, MockAppService, - new MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -967,9 +957,7 @@ class HttpRoutesSpec openIdConnectionConfiguration, statusService, gcpOnlyServicesRegistry, - MockDiskV2ServiceInterp, MockAppService, - runtimev2Service, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -988,9 +976,7 @@ class HttpRoutesSpec openIdConnectionConfiguration, statusService, gcpOnlyServicesRegistry, - MockDiskV2ServiceInterp, kubernetesService, - runtimev2Service, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala index c835322fd1..4488c33819 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala @@ -553,9 +553,7 @@ class ProxyRoutesSpec openIdConnectionConfiguration, statusService, gcpOnlyServicesRegistry, - MockDiskV2ServiceInterp, leoKubernetesService, - runtimev2Service, MockAdminServiceInterp, userInfoDirectives, contentSecurityPolicy, @@ -730,9 +728,7 @@ class ProxyRoutesSpec openIdConnectionConfiguration, statusService, gcpOnlyServicesRegistry, - MockDiskV2ServiceInterp, leoKubernetesService, - runtimev2Service, MockAdminServiceInterp, userInfoDirectives, contentSecurityPolicy, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala index d73ba194a1..7c2b707281 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala @@ -123,19 +123,9 @@ trait TestLeoRoutes { imageConfig, autoFreezeConfig, dataprocConfig, - Config.gceConfig, - azureServiceConfig + Config.gceConfig ) - val runtimev2Service = - new RuntimeV2ServiceInterp[IO]( - serviceConfig, - QueueFactory.makePublisherQueue(), - QueueFactory.makeDateAccessedQueue(), - wsmClientProvider, - MockSamService - ) - val underlyingRuntimeDnsCache = Caffeine.newBuilder().maximumSize(10000L).build[RuntimeDnsCacheKey, scalacache.Entry[HostStatus]]() val runtimeDnsCaffeineCache: Cache[IO, RuntimeDnsCacheKey, HostStatus] = @@ -208,9 +198,7 @@ trait TestLeoRoutes { openIdConnectionConfiguration, statusService, gcpOnlyServicesRegistry, - MockDiskV2ServiceInterp, leoKubernetesService, - runtimev2Service, MockAdminServiceInterp, userInfoDirectives, contentSecurityPolicy, @@ -222,9 +210,7 @@ trait TestLeoRoutes { openIdConnectionConfiguration, statusService, gcpOnlyServicesRegistry, - MockDiskV2ServiceInterp, leoKubernetesService, - runtimev2Service, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterpSpec.scala index 9f41bf38f7..f7293b369b 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterpSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeServiceInterpSpec.scala @@ -92,8 +92,7 @@ trait RuntimeServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with T imageConfig, autoFreezeConfig, dataprocConfig, - Config.gceConfig, - azureServiceConfig + Config.gceConfig ), ConfigReader.appConfig.persistentDisk, new MockDockerDAO, From d540984fac3ffbcd3a07c12102d0176ee735162d Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 3 Jul 2025 15:15:13 -0400 Subject: [PATCH 09/43] Remove machinery to perform Azure app install and delete --- .../workbench/leonardo/LeoPublisher.scala | 8 - .../workbench/leonardo/app/AppInstall.scala | 87 - .../leonardo/app/CromwellAppInstall.scala | 169 -- .../app/CromwellRunnerAppInstall.scala | 200 --- .../leonardo/app/HailBatchAppInstall.scala | 54 - .../leonardo/app/WdsAppInstall.scala | 119 -- .../leonardo/app/WorkflowsAppInstall.scala | 150 -- .../http/AppDependenciesBuilder.scala | 91 +- .../http/service/LeoAppServiceInterp.scala | 2 +- .../monitor/LeoPubsubMessageSubscriber.scala | 84 +- .../leonardo/monitor/MonitorAtBoot.scala | 63 +- .../leoPubsubMessageSubscriberModels.scala | 66 - .../workbench/leonardo/util/AKSAlgebra.scala | 38 - .../leonardo/util/AKSInterpreter.scala | 1437 ----------------- .../leonardo/util/AzurePubsubHandler.scala | 110 +- .../util/AzurePubsubHandlerAlgebra.scala | 28 - .../leonardo/app/BaseAppInstallSpec.scala | 183 --- .../leonardo/app/CromwellAppInstallSpec.scala | 183 --- .../app/CromwellRunnerAppInstallSpec.scala | 137 -- .../app/HailBatchAppInstallSpec.scala | 49 - .../leonardo/app/WdsAppInstallSpec.scala | 118 -- .../app/WorkflowsAppInstallSpec.scala | 87 - .../dsde/workbench/leonardo/mocks.scala | 11 - .../leonardo/monitor/LeoPubsubCodecSpec.scala | 23 - .../LeoPubsubMessageSubscriberSpec.scala | 336 +--- .../leonardo/monitor/MonitorAtBootSpec.scala | 104 +- .../leonardo/util/AKSInterpreterSpec.scala | 1404 ---------------- .../util/AzurePubsubHandlerSpec.scala | 45 +- 28 files changed, 19 insertions(+), 5367 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/AppInstall.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstall.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstall.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstall.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstall.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstall.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSAlgebra.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreter.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/BaseAppInstallSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstallSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstallSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstallSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstallSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstallSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreterSpec.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala index 5948776553..ed85bdc977 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala @@ -124,14 +124,6 @@ final class LeoPublisher[F[_]]( F.unit case _: LeoPubsubMessage.UpdateRuntimeMessage => F.unit - case m: LeoPubsubMessage.CreateAppV2Message => - KubernetesServiceDbQueries - .markPendingCreating(m.appId, None, None, None) - .transaction - case m: LeoPubsubMessage.DeleteAppV2Message => - KubernetesServiceDbQueries - .markPendingAppDeletion(m.appId, m.diskId, now) - .transaction } } yield () } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/AppInstall.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/AppInstall.scala deleted file mode 100644 index 36faa19632..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/AppInstall.scala +++ /dev/null @@ -1,87 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import bio.terra.workspace.model.CloningInstructionsEnum -import bio.terra.workspace.model.CloningInstructionsEnum.NOTHING -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.azure.AzureCloudContext -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceAccountName -import org.broadinstitute.dsde.workbench.leonardo.dao.StorageContainerResponse -import org.broadinstitute.dsde.workbench.leonardo.util.AKSInterpreterConfig -import org.broadinstitute.dsde.workbench.leonardo.{ - App, - AppContext, - AppType, - BillingProfileId, - LandingZoneResources, - ManagedIdentityName, - WorkspaceId, - WsmControlledDatabaseResource -} -import org.broadinstitute.dsp.Values -import org.http4s.Uri -import org.http4s.headers.Authorization - -/** - * Defines how to install a Kubernetes App. - */ -trait AppInstall[F[_]] { - - /** List of WSM-controlled databases the app requires. */ - def databases: List[Database] - - /** Builds helm values to be passed to the app. */ - def buildHelmOverrideValues(params: BuildHelmOverrideValuesParams)(implicit ev: Ask[F, AppContext]): F[Values] - - /** Checks status of the app. */ - def checkStatus(baseUri: Uri, authHeader: Authorization)(implicit ev: Ask[F, AppContext]): F[Boolean] -} - -object AppInstall { - - /** Maps AppType to AppInstall. */ - def appTypeToAppInstall[F[_]](wdsAppInstall: WdsAppInstall[F], - cromwellAppInstall: CromwellAppInstall[F], - workflowsAppInstall: WorkflowsAppInstall[F], - hailBatchAppInstall: HailBatchAppInstall[F], - cromwellRunnerAppInstall: CromwellRunnerAppInstall[F] - ): AppType => AppInstall[F] = _ match { - case AppType.Wds => wdsAppInstall - case AppType.Cromwell => cromwellAppInstall - case AppType.WorkflowsApp => workflowsAppInstall - case AppType.HailBatch => hailBatchAppInstall - case AppType.CromwellRunnerApp => cromwellRunnerAppInstall - case e => throw new IllegalArgumentException(s"Unexpected app type: ${e}") - } - - def getAzureDatabaseName(dbResources: List[WsmControlledDatabaseResource], dbPrefix: String): Option[String] = - dbResources.collectFirst { - case db if db.wsmDatabaseName.startsWith(dbPrefix) => db.azureDatabaseName - } -} - -sealed trait Database -object Database { - - /** A database attached to the lifecycle of app. */ - final case class ControlledDatabase(prefix: String, - allowAccessForAllWorkspaceUsers: Boolean = false, - cloningInstructions: CloningInstructionsEnum = NOTHING - ) extends Database - - /** A database that should _not_ be created as part of app creation, but referenced in k8s namespace creation. - * It is not tied to the lifecycle of app. */ - final case class ReferenceDatabase(name: String) extends Database -} - -final case class BuildHelmOverrideValuesParams(app: App, - workspaceId: WorkspaceId, - cloudContext: AzureCloudContext, - billingProfileId: BillingProfileId, - landingZoneResources: LandingZoneResources, - storageContainer: Option[StorageContainerResponse], - relayPath: Uri, - ksaName: ServiceAccountName, - managedIdentityName: ManagedIdentityName, - databaseNames: List[WsmControlledDatabaseResource], - config: AKSInterpreterConfig -) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstall.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstall.scala deleted file mode 100644 index 9f9c7b18f5..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstall.scala +++ /dev/null @@ -1,169 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.azure.{AzureApplicationInsightsService, AzureBatchService} -import org.broadinstitute.dsde.workbench.leonardo.app.AppInstall.getAzureDatabaseName -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, WsmControlledDatabaseResource} -import org.broadinstitute.dsde.workbench.leonardo.app.Database.ControlledDatabase -import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider -import org.broadinstitute.dsde.workbench.leonardo.config.{AzureEnvironmentConverter, CoaAppConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.http._ -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.broadinstitute.dsp.Values -import org.http4s.Uri -import org.http4s.headers.Authorization - -/** - * Legacy Cromwell-as-an-app. Replaced by WorkflowApp and CromwellRunner app types. - * Helm chart: https://github.com/broadinstitute/cromwhelm/tree/main/coa-helm - */ -class CromwellAppInstall[F[_]](config: CoaAppConfig, - drsConfig: DrsConfig, - samDao: SamDAO[F], - cromwellDao: CromwellDAO[F], - cbasDao: CbasDAO[F], - azureBatchService: AzureBatchService[F], - azureApplicationInsightsService: AzureApplicationInsightsService[F], - authProvider: SamAuthProvider[F] -)(implicit - F: Async[F] -) extends AppInstall[F] { - - override def databases: List[Database] = - List( - ControlledDatabase("cromwell"), - ControlledDatabase("cbas"), - ControlledDatabase("tes") - ) - - override def buildHelmOverrideValues( - params: BuildHelmOverrideValuesParams - )(implicit ev: Ask[F, AppContext]): F[Values] = for { - ctx <- ev.ask - - // Resolve batch account in Azure - batchAccount <- azureBatchService.getBatchAccount(params.landingZoneResources.batchAccountName, params.cloudContext) - - // Resolve application insights in Azure - applicationInsightsComponent <- azureApplicationInsightsService.getApplicationInsights( - params.landingZoneResources.applicationInsightsName, - params.cloudContext - ) - - // Storage container is required for Cromwell app - storageContainer <- F.fromOption( - params.storageContainer, - AppCreationException("Storage container required for Cromwell app", Some(ctx.traceId)) - ) - - // Databases required for Cromwell App - dbNames <- F.fromOption(toCromwellAppDatabaseNames(params.databaseNames), - AppCreationException("Database names required for Cromwell app", Some(ctx.traceId)) - ) - - // Postgres server required for Cromwell App - postgresServer <- F.fromOption(params.landingZoneResources.postgresServer, - AppCreationException("Postgres server required for Cromwell app", Some(ctx.traceId)) - ) - - // Get the pet userToken - tokenOpt <- samDao.getCachedArbitraryPetAccessToken(params.app.auditInfo.creator) - userToken <- ConfigReader.appConfig.azure.hostingModeConfig.enabled match { - case false => - F.fromOption( - tokenOpt, - AppCreationException(s"Pet not found for user ${params.app.auditInfo.creator}", Some(ctx.traceId)) - ) - case true => - F.pure("") // No pet user token in Azure. - } - - values = List( - // azure resources configs - raw"config.resourceGroup=${params.cloudContext.managedResourceGroupName.value}", - raw"config.batchAccountKey=${batchAccount.getKeys().primary}", - raw"config.batchAccountName=${params.landingZoneResources.batchAccountName.value}", - raw"config.batchNodesSubnetId=${params.landingZoneResources.batchNodesSubnetName.value}", - raw"config.drsUrl=${drsConfig.url}", - raw"config.landingZoneId=${params.landingZoneResources.landingZoneId}", - raw"config.subscriptionId=${params.cloudContext.subscriptionId.value}", - raw"config.region=${params.landingZoneResources.region}", - raw"config.applicationInsightsConnectionString=${applicationInsightsComponent.connectionString()}", - raw"config.azureEnvironment=${ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment}", - raw"config.azureManagementTokenScope=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getResourceManagerEndpoint}.default", - raw"config.batchAccountSuffix=${AzureEnvironmentConverter - .batchAccountSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}", - - // relay configs - raw"relay.path=${params.relayPath.renderString}", - - // persistence configs - raw"persistence.storageResourceGroup=${params.cloudContext.managedResourceGroupName.value}", - raw"persistence.storageAccount=${params.landingZoneResources.storageAccountName.value}", - raw"persistence.storageAccountSuffix=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix}", - raw"persistence.blobContainer=${storageContainer.name.value}", - raw"persistence.leoAppInstanceName=${params.app.appName.value}", - raw"persistence.workspaceManager.url=${params.config.wsmConfig.uri.renderString}", - raw"persistence.workspaceManager.workspaceId=${params.workspaceId.value}", - raw"persistence.workspaceManager.containerResourceId=${storageContainer.resourceId.value.toString}", - - // identity configs - raw"identity.enabled=false", - raw"workloadIdentity.enabled=true", - raw"workloadIdentity.serviceAccountName=${params.ksaName.value}", - raw"identity.name=${params.managedIdentityName.value}", - - // Sam configs - raw"sam.url=${params.config.samConfig.server}", - - // Leo configs - raw"leonardo.url=${params.config.leoUrlBase}", - - // Enabled services configs - raw"cbas.enabled=true", - raw"cromwell.enabled=true", - raw"dockstore.baseUrl=${config.dockstoreBaseUrl}", - - // general configs - raw"fullnameOverride=coa-${params.app.release.asString}", - raw"instrumentationEnabled=${config.instrumentationEnabled}", - - // provenance (app-cloning) configs - raw"provenance.userAccessToken=${userToken}", - - // Database configs - raw"postgres.podLocalDatabaseEnabled=false", - raw"postgres.host=${postgresServer.name}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}", - raw"postgres.pgbouncer.enabled=${postgresServer.pgBouncerEnabled}", - // convention is that the database user is the same as the service account name - raw"postgres.user=${params.ksaName.value}", - raw"postgres.dbnames.cromwell=${dbNames.cromwell}", - raw"postgres.dbnames.cbas=${dbNames.cbas}", - raw"postgres.dbnames.tes=${dbNames.tes}" - ) - } yield Values(values.mkString(",")) - - override def checkStatus(baseUri: Uri, authHeader: Authorization)(implicit ev: Ask[F, AppContext]): F[Boolean] = - List( - cromwellDao.getStatus(baseUri, authHeader).handleError(_ => false), - cbasDao.getStatus(baseUri, authHeader).handleError(_ => false) - ).sequence.map(_.forall(identity)) - - private def toCromwellAppDatabaseNames( - dbResources: List[WsmControlledDatabaseResource] - ): Option[CromwellAppDatabaseNames] = - (getAzureDatabaseName(dbResources, "cromwell"), - getAzureDatabaseName(dbResources, "cbas"), - getAzureDatabaseName(dbResources, "tes") - ).mapN(CromwellAppDatabaseNames) -} - -final case class CromwellAppDatabaseNames(cromwell: String, cbas: String, tes: String) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstall.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstall.scala deleted file mode 100644 index 63f2d9e4b1..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstall.scala +++ /dev/null @@ -1,200 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import scala.jdk.CollectionConverters._ -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.azure.{AzureApplicationInsightsService, AzureBatchService} -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, WsmControlledDatabaseResource} -import org.broadinstitute.dsde.workbench.leonardo.app.AppInstall.getAzureDatabaseName -import org.broadinstitute.dsde.workbench.leonardo.app.Database.{ControlledDatabase, ReferenceDatabase} -import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider -import org.broadinstitute.dsde.workbench.leonardo.config.{AzureEnvironmentConverter, CromwellRunnerAppConfig, SamConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao.{BpmApiClientProvider, CromwellDAO, SamDAO} -import org.broadinstitute.dsde.workbench.leonardo.http._ -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.broadinstitute.dsp.Values -import org.http4s.Uri -import org.http4s.headers.Authorization - -import java.util.UUID -import scala.util.Either - -/** - * Cromwell runner app type. - * Helm chart: https://github.com/broadinstitute/terra-helmfile/tree/master/charts/cromwell-runner-app - */ -class CromwellRunnerAppInstall[F[_]](config: CromwellRunnerAppConfig, - drsConfig: DrsConfig, - samConfig: SamConfig, - samDao: SamDAO[F], - cromwellDao: CromwellDAO[F], - azureBatchService: AzureBatchService[F], - azureApplicationInsightsService: AzureApplicationInsightsService[F], - bpmClient: BpmApiClientProvider[F], - authProvider: SamAuthProvider[F] -)(implicit - F: Async[F] -) extends AppInstall[F] { - override def databases: List[Database] = - List( - ControlledDatabase("cromwell"), - ControlledDatabase("tes"), - ReferenceDatabase("cromwellmetadata") - ) - - override def buildHelmOverrideValues(params: BuildHelmOverrideValuesParams)(implicit - ev: Ask[F, AppContext] - ): F[Values] = - for { - ctx <- ev.ask - // Resolve batch account in Azure - batchAccount <- azureBatchService.getBatchAccount(params.landingZoneResources.batchAccountName, - params.cloudContext - ) - - // Resolve application insights in Azure - applicationInsightsComponent <- azureApplicationInsightsService.getApplicationInsights( - params.landingZoneResources.applicationInsightsName, - params.cloudContext - ) - - // Storage container is required for Cromwell app - storageContainer <- F.fromOption( - params.storageContainer, - AppCreationException("Storage container required for Cromwell Runner app", Some(ctx.traceId)) - ) - - // Databases required for Cromwell App - dbNames <- F.fromOption( - toCromwellRunnerAppDatabaseNames(params.databaseNames), - AppCreationException(s"Database names required for Cromwell Runner app: ${params.databaseNames}", - Some(ctx.traceId) - ) - ) - - // Postgres server required for Cromwell App - postgresServer <- F.fromOption( - params.landingZoneResources.postgresServer, - AppCreationException("Postgres server required for Cromwell Runner app", Some(ctx.traceId)) - ) - - // Get the pet userToken - - leoAuth <- authProvider.getLeoAuthToken - - parsedUUID <- F.delay(Either.catchNonFatal(UUID.fromString(params.billingProfileId.value))) - profileAttempt <- parsedUUID.traverse { uuid => - bpmClient.getProfile(leoAuth, uuid) - } - - maybeLimits = profileAttempt.toOption.flatten.flatMap { profile => - profile.getOrganization.getLimits.asScala - .get("concurrentjoblimit") - .map(v => raw"config.concurrentJobLimit=${v}") - } - - // Get the pet userToken - tokenOpt <- samDao.getCachedArbitraryPetAccessToken(params.app.auditInfo.creator) - userToken <- ConfigReader.appConfig.azure.hostingModeConfig.enabled match { - case false => - F.fromOption( - tokenOpt, - AppCreationException(s"Pet not found for user ${params.app.auditInfo.creator}", Some(ctx.traceId)) - ) - case true => - F.pure("") // No pet user token in Azure. - } - - values = List( - // azure resources configs - raw"config.resourceGroup=${params.cloudContext.managedResourceGroupName.value}", - raw"config.batchAccountKey=${batchAccount.getKeys().primary}", - raw"config.batchAccountName=${params.landingZoneResources.batchAccountName.value}", - raw"config.batchNodesSubnetId=${params.landingZoneResources.batchNodesSubnetName.value}", - raw"config.drsUrl=${drsConfig.url}", - raw"config.landingZoneId=${params.landingZoneResources.landingZoneId}", - raw"config.subscriptionId=${params.cloudContext.subscriptionId.value}", - raw"config.region=${params.landingZoneResources.region}", - raw"config.applicationInsightsConnectionString=${applicationInsightsComponent.connectionString()}", - raw"config.azureEnvironment=${ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment}", - raw"config.azureManagementTokenScope=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getResourceManagerEndpoint}.default", - raw"config.batchAccountSuffix=${AzureEnvironmentConverter - .batchAccountSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}", - - // relay configs - raw"relay.path=${params.relayPath.renderString}", - - // persistence configs - raw"persistence.storageAccount=${params.landingZoneResources.storageAccountName.value}", - raw"persistence.storageAccountSuffix=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix}", - raw"persistence.blobContainer=${storageContainer.name.value}", - raw"persistence.leoAppInstanceName=${params.app.appName.value}", - raw"persistence.workspaceManager.url=${params.config.wsmConfig.uri.renderString}", - raw"persistence.workspaceManager.workspaceId=${params.workspaceId.value}", - raw"persistence.workspaceManager.containerResourceId=${storageContainer.resourceId.value.toString}", - - // identity configs - raw"workloadIdentity.serviceAccountName=${params.ksaName.value}", - raw"identity.name=${params.managedIdentityName.value}", - - // Enabled services configs - raw"cromwell.enabled=${config.enabled}", - - // general configs - raw"fullnameOverride=cra-${params.app.release.asString}", - raw"instrumentationEnabled=${config.instrumentationEnabled}", - - // provenance (app-cloning) configs - raw"provenance.userAccessToken=${userToken}", - - // database configs - raw"postgres.podLocalDatabaseEnabled=false", - raw"postgres.host=${postgresServer.name}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}", - raw"postgres.pgbouncer.enabled=${postgresServer.pgBouncerEnabled}", - // convention is that the database user is the same as the service account name - raw"postgres.user=${params.ksaName.value}", - raw"postgres.dbnames.cromwell=${dbNames.cromwell}", - raw"postgres.dbnames.tes=${dbNames.tes}", - raw"postgres.dbnames.cromwellMetadata=${dbNames.cromwellMetadata}", - - // ECM configs - raw"ecm.baseUri=${config.ecmBaseUri}", - - // Sam configs - raw"sam.baseUri=${samConfig.server}", - raw"sam.acrPullActionIdentityResourceId=${params.billingProfileId.value}", - - // Bard configs - raw"bard.bardUrl=${config.bardBaseUri}", - raw"bard.enabled=${config.bardEnabled}" - ) - - finalList = maybeLimits match { - case Some(str) => values :+ str - case _ => values - } - - } yield Values(finalList.mkString(",")) - - override def checkStatus(baseUri: Uri, authHeader: Authorization)(implicit ev: Ask[F, AppContext]): F[Boolean] = - cromwellDao.getStatus(baseUri, authHeader).handleError(_ => false) - - def toCromwellRunnerAppDatabaseNames( - dbResources: List[WsmControlledDatabaseResource] - ): Option[CromwellRunnerAppDatabaseNames] = - (dbResources - .find(db => db.wsmDatabaseName.startsWith("cromwell") && !db.wsmDatabaseName.startsWith("cromwellmetadata")) - .map(_.azureDatabaseName), - getAzureDatabaseName(dbResources, "tes"), - getAzureDatabaseName(dbResources, "cromwellmetadata") - ).mapN(CromwellRunnerAppDatabaseNames) - -} - -final case class CromwellRunnerAppDatabaseNames(cromwell: String, tes: String, cromwellMetadata: String) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstall.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstall.scala deleted file mode 100644 index 5e91371de5..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstall.scala +++ /dev/null @@ -1,54 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.broadinstitute.dsde.workbench.leonardo.config.{AzureEnvironmentConverter, HailBatchAppConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao.HailBatchDAO -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.broadinstitute.dsp.Values -import org.http4s.Uri -import org.http4s.headers.Authorization - -/** - * Hail Batch app. - */ -class HailBatchAppInstall[F[_]](config: HailBatchAppConfig, hailBatchDao: HailBatchDAO[F])(implicit F: Async[F]) - extends AppInstall[F] { - override def databases: List[Database] = List.empty - - override def buildHelmOverrideValues( - params: BuildHelmOverrideValuesParams - )(implicit ev: Ask[F, AppContext]): F[Values] = - for { - ctx <- ev.ask - // Storage container is required for Cromwell app - storageContainer <- F.fromOption( - params.storageContainer, - AppCreationException("Storage container required for Hail Batch app", Some(ctx.traceId)) - ) - values = - List( - raw"persistence.storageAccount=${params.landingZoneResources.storageAccountName.value}", - raw"persistence.blobContainer=${storageContainer.name.value}", - raw"persistence.workspaceManager.url=${params.config.wsmConfig.uri.renderString}", - raw"persistence.workspaceManager.workspaceId=${params.workspaceId.value}", - raw"persistence.workspaceManager.containerResourceId=${storageContainer.resourceId.value.toString}", - raw"persistence.workspaceManager.storageContainerUrl=https://${params.landingZoneResources.storageAccountName.value}.blob${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix}/${storageContainer.name.value}", - raw"persistence.leoAppName=${params.app.appName.value}", - - // identity configs - raw"workloadIdentity.serviceAccountName=${params.ksaName.value}", - - // relay configs - raw"relay.domain=${params.relayPath.authority.getOrElse("none")}", - raw"relay.subpath=/${params.relayPath.path.segments.last.toString}" - ) - } yield Values(values.mkString(",")) - - override def checkStatus(baseUri: Uri, authHeader: Authorization)(implicit ev: Ask[F, AppContext]): F[Boolean] = - hailBatchDao.getStatus(baseUri, authHeader).handleError(_ => false) -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstall.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstall.scala deleted file mode 100644 index c7ddb183cd..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstall.scala +++ /dev/null @@ -1,119 +0,0 @@ -package org.broadinstitute.dsde.workbench -package leonardo -package app - -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.azure.AzureApplicationInsightsService -import org.broadinstitute.dsde.workbench.leonardo.app.Database.ControlledDatabase -import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider -import org.broadinstitute.dsde.workbench.leonardo.config.{AzureEnvironmentConverter, WdsAppConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.http._ -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.broadinstitute.dsp.Values -import org.http4s.Uri -import org.http4s.headers.Authorization - -/** - * WDS app. - * Helm chart: https://github.com/broadinstitute/terra-helmfile/tree/master/charts/wds - */ -class WdsAppInstall[F[_]](config: WdsAppConfig, - tdrConfig: TdrConfig, - samDao: SamDAO[F], - wdsDao: WdsDAO[F], - azureApplicationInsightsService: AzureApplicationInsightsService[F], - authProvider: SamAuthProvider[F] -)(implicit - F: Async[F] -) extends AppInstall[F] { - override def databases: List[Database] = - List(ControlledDatabase("wds")) - - override def buildHelmOverrideValues( - params: BuildHelmOverrideValuesParams - )(implicit ev: Ask[F, AppContext]): F[Values] = - for { - ctx <- ev.ask - - // Resolve Application Insights in Azure - applicationInsightsComponent <- azureApplicationInsightsService.getApplicationInsights( - params.landingZoneResources.applicationInsightsName, - params.cloudContext - ) - - // Database required for WDS App - dbName <- F.fromOption(params.databaseNames.headOption.map(_.azureDatabaseName), - AppCreationException("Database names required for WDS app", Some(ctx.traceId)) - ) - - // Postgres server required for WDS App - postgresServer <- F.fromOption( - params.landingZoneResources.postgresServer, - AppCreationException("Postgres server required for WDS app", Some(ctx.traceId)) - ) - - // Get the pet userToken - - // Get Vpa enabled tag - vpaEnabled <- F.pure(params.landingZoneResources.aksCluster.tags.getOrElse("aks-cost-vpa-enabled", false)) - - // Get the pet userToken - tokenOpt <- samDao.getCachedArbitraryPetAccessToken(params.app.auditInfo.creator) - userToken <- ConfigReader.appConfig.azure.hostingModeConfig.enabled match { - case false => - F.fromOption( - tokenOpt, - AppCreationException(s"Pet not found for user ${params.app.auditInfo.creator}", Some(ctx.traceId)) - ) - case true => - F.pure("") // No pet user token in Azure. - } - - valuesList = - List( - // pass enviiroment information to wds so it can properly pick its config - raw"wds.environment=${config.environment}", - raw"wds.environmentBase=${config.environmentBase}", - - // azure resources configs - raw"config.resourceGroup=${params.cloudContext.managedResourceGroupName.value}", - raw"config.applicationInsightsConnectionString=${applicationInsightsComponent.connectionString()}", - raw"config.aks.vpaEnabled=${vpaEnabled}", - - // Azure subscription configs currently unused - raw"config.subscriptionId=${params.cloudContext.subscriptionId.value}", - raw"config.region=${params.landingZoneResources.region}", - - // persistence configs - raw"general.leoAppInstanceName=${params.app.appName.value}", - raw"general.workspaceManager.workspaceId=${params.workspaceId.value}", - - // identity configs - raw"identity.enabled=false", - raw"workloadIdentity.enabled=true", - raw"workloadIdentity.serviceAccountName=${params.ksaName.value}", - - // general configs - raw"fullnameOverride=wds-${params.app.release.asString}", - raw"instrumentationEnabled=${config.instrumentationEnabled}", - - // provenance (app-cloning) configs - raw"provenance.userAccessToken=${userToken}", - raw"provenance.sourceWorkspaceId=${params.app.sourceWorkspaceId.map(_.value).getOrElse("")}", - - // database configs - raw"postgres.host=${postgresServer.name}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}", - raw"postgres.pgbouncer.enabled=${postgresServer.pgBouncerEnabled}", - raw"postgres.dbname=$dbName", - // convention is that the database user is the same as the service account name - raw"postgres.user=${params.ksaName.value}" - ) - } yield Values(valuesList.mkString(",")) - - override def checkStatus(baseUri: Uri, authHeader: Authorization)(implicit ev: Ask[F, AppContext]): F[Boolean] = - wdsDao.getStatus(baseUri, authHeader).handleError(_ => false) -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstall.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstall.scala deleted file mode 100644 index abaf0cf897..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstall.scala +++ /dev/null @@ -1,150 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import bio.terra.workspace.model.CloningInstructionsEnum -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.azure.{AzureApplicationInsightsService, AzureBatchService} -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, WsmControlledDatabaseResource} -import org.broadinstitute.dsde.workbench.leonardo.app.AppInstall.getAzureDatabaseName -import org.broadinstitute.dsde.workbench.leonardo.app.Database.ControlledDatabase -import org.broadinstitute.dsde.workbench.leonardo.config.{AzureEnvironmentConverter, WorkflowsAppConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.http._ -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.broadinstitute.dsp.Values -import org.http4s.Uri -import org.http4s.headers.Authorization - -/** - * Workflows app. - * Helm chart: https://github.com/broadinstitute/terra-helmfile/tree/master/charts/workflows-app - */ -class WorkflowsAppInstall[F[_]](config: WorkflowsAppConfig, - drsConfig: DrsConfig, - samDao: SamDAO[F], - cromwellDao: CromwellDAO[F], - cbasDao: CbasDAO[F], - azureBatchService: AzureBatchService[F], - azureApplicationInsightsService: AzureApplicationInsightsService[F] -)(implicit - F: Async[F] -) extends AppInstall[F] { - - override def databases: List[Database] = - List( - ControlledDatabase("cbas", cloningInstructions = CloningInstructionsEnum.RESOURCE), - // Cromwell metadata database is also accessed by the cromwell-runner app - ControlledDatabase("cromwellmetadata", allowAccessForAllWorkspaceUsers = true) - ) - - override def buildHelmOverrideValues( - params: BuildHelmOverrideValuesParams - )(implicit ev: Ask[F, AppContext]): F[Values] = - for { - ctx <- ev.ask - - // Resolve application insights in Azure - applicationInsightsComponent <- azureApplicationInsightsService.getApplicationInsights( - params.landingZoneResources.applicationInsightsName, - params.cloudContext - ) - - // Storage container is required for Workflows app - storageContainer <- F.fromOption( - params.storageContainer, - AppCreationException("Storage container required for Workflows app", Some(ctx.traceId)) - ) - - // Databases required for Workflows App - dbNames <- F.fromOption(toWorkflowsAppDatabaseNames(params.databaseNames), - AppCreationException("Database names required for Workflows app", Some(ctx.traceId)) - ) - - // Postgres server required for Workflows App - postgresServer <- F.fromOption( - params.landingZoneResources.postgresServer, - AppCreationException("Postgres server required for Workflows app", Some(ctx.traceId)) - ) - - // Get the pet userToken - tokenOpt <- samDao.getCachedArbitraryPetAccessToken(params.app.auditInfo.creator) - userToken <- ConfigReader.appConfig.azure.hostingModeConfig.enabled match { - case false => - F.fromOption( - tokenOpt, - AppCreationException(s"Pet not found for user ${params.app.auditInfo.creator}", Some(ctx.traceId)) - ) - case true => - F.pure("") // No pet user token in Azure. - } - - values = - List( - // azure resources configs - raw"config.drsUrl=${drsConfig.url}", - raw"config.applicationInsightsConnectionString=${applicationInsightsComponent.connectionString()}", - - // relay configs - raw"relay.path=${params.relayPath.renderString}", - - // persistence configs - raw"persistence.storageAccount=${params.landingZoneResources.storageAccountName.value}", - raw"persistence.blobContainer=${storageContainer.name.value}", - raw"persistence.leoAppInstanceName=${params.app.appName.value}", - raw"persistence.workspaceManager.url=${params.config.wsmConfig.uri.renderString}", - raw"persistence.workspaceManager.workspaceId=${params.workspaceId.value}", - - // identity configs - raw"workloadIdentity.serviceAccountName=${params.ksaName.value}", - - // Sam configs - raw"sam.url=${params.config.samConfig.server}", - - // Leo configs - raw"leonardo.url=${params.config.leoUrlBase}", - - // Enabled services configs - raw"dockstore.baseUrl=${config.dockstoreBaseUrl}", - - // general configs - raw"fullnameOverride=wfa-${params.app.release.asString}", - raw"instrumentationEnabled=${config.instrumentationEnabled}", - - // provenance (app-cloning) configs - raw"provenance.userAccessToken=${userToken}", - - // database configs - raw"postgres.podLocalDatabaseEnabled=false", - raw"postgres.host=${postgresServer.name}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}", - raw"postgres.pgbouncer.enabled=${postgresServer.pgBouncerEnabled}", - // convention is that the database user is the same as the service account name - raw"postgres.user=${params.ksaName.value}", - raw"postgres.dbnames.cromwellMetadata=${dbNames.cromwellMetadata}", - raw"postgres.dbnames.cbas=${dbNames.cbas}", - - // ECM configs - raw"ecm.baseUri=${config.ecmBaseUri}", - - // Bard configs - raw"bard.baseUri=${config.bardBaseUri}", - raw"bard.enabled=${config.bardEnabled}" - ) - } yield Values(values.mkString(",")) - - override def checkStatus(baseUri: Uri, authHeader: Authorization)(implicit ev: Ask[F, AppContext]): F[Boolean] = - List(cromwellDao.getStatus(baseUri, authHeader).handleError(_ => false), - cbasDao.getStatus(baseUri, authHeader).handleError(_ => false) - ).sequence - .map(_.forall(identity)) - - private def toWorkflowsAppDatabaseNames( - dbResources: List[WsmControlledDatabaseResource] - ): Option[WorkflowsAppDatabaseNames] = - (getAzureDatabaseName(dbResources, "cromwellmetadata"), getAzureDatabaseName(dbResources, "cbas")).mapN( - WorkflowsAppDatabaseNames - ) -} - -final case class WorkflowsAppDatabaseNames(cromwellMetadata: String, cbas: String) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 27fdbba39e..95d36c7910 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -5,23 +5,7 @@ import cats.effect.std.Semaphore import cats.effect.{IO, Resource} import fs2.Stream import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor -import org.broadinstitute.dsde.workbench.leonardo.app._ -import org.broadinstitute.dsde.workbench.leonardo.config.Config.{ - appMonitorConfig, - applicationConfig, - asyncTaskProcessorConfig, - autoFreezeConfig, - autodeleteConfig, - contentSecurityPolicy, - dateAccessUpdaterConfig, - dbConcurrency, - leoExecutionModeConfig, - leoPubsubMessageSubscriberConfig, - liquibaseConfig, - prometheusConfig, - refererConfig, - samConfig -} +import org.broadinstitute.dsde.workbench.leonardo.config.Config.{applicationConfig, asyncTaskProcessorConfig, autoFreezeConfig, autodeleteConfig, contentSecurityPolicy, dateAccessUpdaterConfig, dbConcurrency, leoExecutionModeConfig, leoPubsubMessageSubscriberConfig, liquibaseConfig, prometheusConfig, refererConfig} import org.broadinstitute.dsde.workbench.leonardo.config.LeoExecutionModeConfig import org.broadinstitute.dsde.workbench.leonardo.dao.ToolDAO import org.broadinstitute.dsde.workbench.leonardo.db.DbReference @@ -167,78 +151,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu baselineDependencies.azureContainerService ) - val cromwellAppInstall = new CromwellAppInstall[IO]( - ConfigReader.appConfig.azure.coaAppConfig, - ConfigReader.appConfig.drs, - baselineDependencies.samDAO, - baselineDependencies.cromwellDAO, - baselineDependencies.cbasDAO, - baselineDependencies.azureBatchService, - baselineDependencies.azureApplicationInsightsService, - baselineDependencies.authProvider - ) - - val cromwellRunnerAppInstall = - new CromwellRunnerAppInstall[IO]( - ConfigReader.appConfig.azure.cromwellRunnerAppConfig, - ConfigReader.appConfig.drs, - samConfig, - baselineDependencies.samDAO, - baselineDependencies.cromwellDAO, - baselineDependencies.azureBatchService, - baselineDependencies.azureApplicationInsightsService, - baselineDependencies.bpmClientProvider, - baselineDependencies.authProvider - ) - val hailBatchAppInstall = - new HailBatchAppInstall[IO](ConfigReader.appConfig.azure.hailBatchAppConfig, baselineDependencies.hailBatchDAO) - val wdsAppInstall = new WdsAppInstall[IO]( - ConfigReader.appConfig.azure.wdsAppConfig, - ConfigReader.appConfig.azure.tdr, - baselineDependencies.samDAO, - baselineDependencies.wdsDAO, - baselineDependencies.azureApplicationInsightsService, - baselineDependencies.authProvider - ) - val workflowsAppInstall = - new WorkflowsAppInstall[IO]( - ConfigReader.appConfig.azure.workflowsAppConfig, - ConfigReader.appConfig.drs, - baselineDependencies.samDAO, - baselineDependencies.cromwellDAO, - baselineDependencies.cbasDAO, - baselineDependencies.azureBatchService, - baselineDependencies.azureApplicationInsightsService - ) - - implicit val appTypeToAppInstall = AppInstall.appTypeToAppInstall(wdsAppInstall, - cromwellAppInstall, - workflowsAppInstall, - hailBatchAppInstall, - cromwellRunnerAppInstall - ) - - val aksAlg = new AKSInterpreter[IO]( - AKSInterpreterConfig( - samConfig, - appMonitorConfig, - ConfigReader.appConfig.azure.wsm, - applicationConfig.leoUrlBase, - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.listenerImage, - ConfigReader.appConfig.azure.listenerChartConfig - ), - baselineDependencies.helmClient, - baselineDependencies.azureContainerService, - baselineDependencies.azureRelay, - baselineDependencies.samDAO, - baselineDependencies.wsmDAO, - kubeAlg, - baselineDependencies.wsmClientProvider, - baselineDependencies.wsmDAO, - baselineDependencies.authProvider, - baselineDependencies.samService - ) - val azureAlg = new AzurePubsubHandlerInterp[IO]( ConfigReader.appConfig.azure.pubsubHandler, applicationConfig, @@ -250,7 +162,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu baselineDependencies.jupyterDAO, baselineDependencies.azureRelay, baselineDependencies.azureVmService, - aksAlg, refererConfig, baselineDependencies.wsmClientProvider ) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala index 30a0eef108..7acf468b11 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala @@ -81,7 +81,7 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, enableIntraNodeVisibility = req.labels.get(AOU_UI_LABEL).exists(x => x == "true") _ <- req.appType match { case AppType.Galaxy | AppType.HailBatch | AppType.Wds | AppType.Cromwell | AppType.WorkflowsApp | - AppType.CromwellRunnerApp => + AppType.CromwellRunnerApp => F.unit case AppType.Allowed => req.allowedChartName match { diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala index 8dc09b7228..181f1fca97 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala @@ -128,8 +128,6 @@ class LeoPubsubMessageSubscriber[F[_]]( e.getMessage ) } - case msg: CreateAppV2Message => handleCreateAppV2Message(msg) - case msg: DeleteAppV2Message => handleDeleteAppV2Message(msg) case msg: DeleteDiskV2Message => handleDeleteDiskV2Message(msg) } } yield resp @@ -1533,9 +1531,8 @@ class LeoPubsubMessageSubscriber[F[_]]( .updateAndPollApp( UpdateAppParams(msg.appId, msg.appName, latestAppChartVersion, msg.googleProject) ) - case CloudContext.Azure(azureContext) => - azurePubsubHandler - .updateAndPollApp(msg.appId, msg.appName, latestAppChartVersion, msg.workspaceId, azureContext) + case CloudContext.Azure(_) => + F.raiseError(new NotImplementedError("Azure functionality not implemented.")) }).flatMap { _ => updateAppLogQuery .update(msg.appId, msg.jobId, UpdateAppJobStatus.Success, endTime = Some(ctx.now)) @@ -1773,83 +1770,6 @@ class LeoPubsubMessageSubscriber[F[_]]( } } yield res - private[monitor] def handleCreateAppV2Message( - msg: CreateAppV2Message - )(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - ctx <- ev.ask - _ <- msg.cloudContext match { - case CloudContext.Azure(c) => - val task = - azurePubsubHandler.createAndPollApp(msg.appId, msg.appName, msg.workspaceId, c, msg.billingProfileId) - asyncTasks.offer( - Task(ctx.traceId, - task, - Some(handleKubernetesError), - ctx.now, - TaskMetricsTags("createAppV2", None, Some(false), CloudProvider.Azure) - ) - ) - case CloudContext.Gcp(c) => - F.raiseError( - PubsubKubernetesError( - AppError( - s"Error creating GCP app with id ${msg.appId} and cloudContext ${c.value}: CreateAppV2 not supported for GCP", - ctx.now, - ErrorAction.CreateApp, - ErrorSource.App, - None, - Some(ctx.traceId) - ), - Some(msg.appId), - false, - None, - None, - None - ) - ) - } - } yield () - - private[monitor] def handleDeleteAppV2Message( - msg: DeleteAppV2Message - )(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - ctx <- ev.ask - _ <- msg.cloudContext match { - case CloudContext.Azure(c) => - val task = - azurePubsubHandler.deleteApp(msg.appId, msg.appName, msg.workspaceId, c, msg.billingProfileId) - asyncTasks.offer( - Task(ctx.traceId, - task, - Some(handleKubernetesError), - ctx.now, - TaskMetricsTags("deleteAppV2", None, Some(false), CloudProvider.Azure) - ) - ) - - case CloudContext.Gcp(c) => - F.raiseError( - PubsubKubernetesError( - AppError( - s"Error creating GCP app with id ${msg.appId} and cloudContext ${c.value}: DeleteAppV2 not supported for GCP", - ctx.now, - ErrorAction.DeleteApp, - ErrorSource.App, - None, - Some(ctx.traceId) - ), - Some(msg.appId), - false, - None, - None, - None - ) - ) - } - } yield () - private[monitor] def handleDeleteDiskV2Message( msg: DeleteDiskV2Message )(implicit ev: Ask[F, AppContext]): F[Unit] = diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala index 35837a50f3..2686f2c3d7 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala @@ -12,7 +12,7 @@ import org.broadinstitute.dsde.workbench.leonardo.dao.{SamDAO, WsmApiClientProvi import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.http._ import org.broadinstitute.dsde.workbench.leonardo.model.LeoException -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, CreateAppV2Message, DeleteAppMessage, DeleteAppV2Message} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, DeleteAppMessage} import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics import org.typelevel.log4cats.Logger @@ -217,32 +217,7 @@ class MonitorAtBoot[F[_]](publisherQueue: Queue[F, LeoPubsubMessage], } yield msg case CloudContext.Azure(_) => - for { - workspaceId <- F.fromOption( - app.workspaceId, - MonitorAtBootException( - s"WorkspaceId not found for app ${app.id} in Provisioning status", - appContext.traceId - ) - ) - token <- getAuthToken(app.auditInfo.creator) - workspaceDescOpt <- wsmClientProvider.getWorkspace( - token, - workspaceId - ) - workspaceDesc <- F.fromOption(workspaceDescOpt, - WorkspaceNotFoundException(workspaceId, appContext.traceId) - ) - - msg = CreateAppV2Message( - app.id, - app.appName, - workspaceId, - cluster.cloudContext, - BillingProfileId(workspaceDesc.spendProfile), - Some(appContext.traceId) - ) - } yield msg + F.raiseError(new NotImplementedError("Azure functionality not implemented.")) } @@ -261,39 +236,7 @@ class MonitorAtBoot[F[_]](publisherQueue: Queue[F, LeoPubsubMessage], ) ) case CloudContext.Azure(_) => - for { - workspaceId <- F.fromOption(app.workspaceId, - MonitorAtBootException( - s"WorkspaceId not found for app ${app.id} in Provisioning status", - appContext.traceId - ) - ) - token <- getAuthToken(app.auditInfo.creator) - workspaceDescOpt <- wsmClientProvider.getWorkspace( - token, - workspaceId - ) - workspaceDesc <- F.fromOption(workspaceDescOpt, - WorkspaceNotFoundException(workspaceId, appContext.traceId) - ) - - diskOpt <- appQuery.getDiskId(app.id).transaction - workspaceId <- F.fromOption(app.workspaceId, - MonitorAtBootException( - s"WorkspaceId not found for app ${app.id} in Provisioning status", - appContext.traceId - ) - ) - msg = DeleteAppV2Message( - app.id, - app.appName, - workspaceId, - cluster.cloudContext, - diskOpt, - BillingProfileId(workspaceDesc.spendProfile), - Some(appContext.traceId) - ) - } yield msg + F.raiseError(new NotImplementedError("Azure functionality not implemented.")) } case x => F.raiseError(MonitorAtBootException(s"Unexpected status for app ${app.id}: ${x}", appContext.traceId)) } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala index 863185a3e7..39da2536b5 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala @@ -170,14 +170,6 @@ object LeoPubsubMessageType extends Enum[LeoPubsubMessageType] { final case object DeleteDiskV2 extends LeoPubsubMessageType { val asString = "deleteDiskV2" } - - final case object CreateAppV2 extends LeoPubsubMessageType { - val asString = "createAppV2" - } - - final case object DeleteAppV2 extends LeoPubsubMessageType { - val asString = "deleteAppV2" - } } sealed trait LeoPubsubMessage { @@ -281,17 +273,6 @@ object LeoPubsubMessage { val messageType: LeoPubsubMessageType = LeoPubsubMessageType.CreateApp } - final case class CreateAppV2Message( - appId: AppId, - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: CloudContext, - billingProfileId: BillingProfileId, - traceId: Option[TraceId] - ) extends LeoPubsubMessage { - val messageType: LeoPubsubMessageType = LeoPubsubMessageType.CreateAppV2 - } - final case class DeleteAppMessage(appId: AppId, appName: AppName, project: GoogleProject, @@ -301,17 +282,6 @@ object LeoPubsubMessage { val messageType: LeoPubsubMessageType = LeoPubsubMessageType.DeleteApp } - final case class DeleteAppV2Message(appId: AppId, - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: CloudContext, - diskId: Option[DiskId], - billingProfileId: BillingProfileId, - traceId: Option[TraceId] - ) extends LeoPubsubMessage { - val messageType: LeoPubsubMessageType = LeoPubsubMessageType.DeleteAppV2 - } - final case class StopAppMessage(appId: AppId, appName: AppName, project: GoogleProject, traceId: Option[TraceId]) extends LeoPubsubMessage { val messageType: LeoPubsubMessageType = LeoPubsubMessageType.StopApp @@ -597,16 +567,6 @@ object LeoPubsubCodec { implicit val storageContainerResponseDecoder: Decoder[StorageContainerResponse] = Decoder.forProduct2("name", "resourceId")(StorageContainerResponse.apply) - implicit val createAppV2Decoder: Decoder[CreateAppV2Message] = - Decoder.forProduct6("appId", "appName", "workspaceId", "cloudContext", "billingProfileId", "traceId")( - CreateAppV2Message.apply - ) - - implicit val deleteAppV2Decoder: Decoder[DeleteAppV2Message] = - Decoder.forProduct7("appId", "appName", "workspaceId", "cloudContext", "diskId", "billingProfileId", "traceId")( - DeleteAppV2Message.apply - ) - implicit val deleteDiskV2Decoder: Decoder[DeleteDiskV2Message] = Decoder.forProduct5("diskId", "workspaceId", "cloudContext", "wsmResourceId", "traceId")( DeleteDiskV2Message.apply @@ -631,8 +591,6 @@ object LeoPubsubCodec { case LeoPubsubMessageType.UpdateApp => message.as[UpdateAppMessage] case LeoPubsubMessageType.CreateAzureRuntime => message.as[CreateAzureRuntimeMessage] case LeoPubsubMessageType.DeleteAzureRuntime => message.as[DeleteAzureRuntimeMessage] - case LeoPubsubMessageType.CreateAppV2 => message.as[CreateAppV2Message] - case LeoPubsubMessageType.DeleteAppV2 => message.as[DeleteAppV2Message] case LeoPubsubMessageType.DeleteDiskV2 => message.as[DeleteDiskV2Message] } @@ -989,28 +947,6 @@ object LeoPubsubCodec { (x.messageType, x.runtimeId, x.diskIdToDelete, x.workspaceId, x.wsmResourceId, x.billingProfileId, x.traceId) ) - implicit val createAppV2MessageEncoder: Encoder[CreateAppV2Message] = - Encoder.forProduct7( - "messageType", - "appId", - "appName", - "workspaceId", - "cloudContext", - "billingProfileId", - "traceId" - )(x => (x.messageType, x.appId, x.appName, x.workspaceId, x.cloudContext, x.billingProfileId, x.traceId)) - - implicit val deleteAppV2MessageEncoder: Encoder[DeleteAppV2Message] = - Encoder.forProduct8("messageType", - "appId", - "appName", - "workspaceId", - "cloudContext", - "diskId", - "billingProfileId", - "traceId" - )(x => (x.messageType, x.appId, x.appName, x.workspaceId, x.cloudContext, x.diskId, x.billingProfileId, x.traceId)) - implicit val deleteDiskV2MessageEncoder: Encoder[DeleteDiskV2Message] = Encoder.forProduct6("messageType", "diskId", "workspaceId", "cloudContext", "wsmResourceId", "traceId")(x => (x.messageType, x.diskId, x.workspaceId, x.cloudContext, x.wsmResourceId, x.traceId) @@ -1032,8 +968,6 @@ object LeoPubsubCodec { case m: UpdateAppMessage => m.asJson case m: CreateAzureRuntimeMessage => m.asJson case m: DeleteAzureRuntimeMessage => m.asJson - case m: CreateAppV2Message => m.asJson - case m: DeleteAppV2Message => m.asJson case m: DeleteDiskV2Message => m.asJson } } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSAlgebra.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSAlgebra.scala deleted file mode 100644 index c7821a03bc..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSAlgebra.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.util - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.azure.AzureCloudContext -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, AppId, AppName, BillingProfileId, WorkspaceId} -import org.broadinstitute.dsp.ChartVersion - -trait AKSAlgebra[F[_]] { - - /** Creates an app and polls it for completion */ - def createAndPollApp(params: CreateAKSAppParams)(implicit ev: Ask[F, AppContext]): F[Unit] - - def deleteApp(params: DeleteAKSAppParams)(implicit ev: Ask[F, AppContext]): F[Unit] - - def updateAndPollApp(params: UpdateAKSAppParams)(implicit ev: Ask[F, AppContext]): F[Unit] - -} - -final case class CreateAKSAppParams(appId: AppId, - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: AzureCloudContext, - billingProfileId: BillingProfileId -) - -final case class UpdateAKSAppParams(appId: AppId, - appName: AppName, - appChartVersion: ChartVersion, - workspaceId: Option[WorkspaceId], - cloudContext: AzureCloudContext -) - -final case class DeleteAKSAppParams( - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: AzureCloudContext, - billingProfileId: BillingProfileId -) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreter.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreter.scala deleted file mode 100644 index 3313aebec2..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreter.scala +++ /dev/null @@ -1,1437 +0,0 @@ -package org.broadinstitute.dsde.workbench -package leonardo -package util - -import akka.http.scaladsl.model.StatusCodes -import bio.terra.workspace.api.{ControlledAzureResourceApi, ResourceApi, WorkspaceApi} -import bio.terra.workspace.client.ApiException -import bio.terra.workspace.model._ -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import com.azure.core.management.exception.ManagementException -import fs2.io.file.Files -import org.broadinstitute.dsde.workbench.DoneCheckableSyntax._ -import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.google2.KubernetesModels.{KubernetesNamespace, PodStatus} -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.{NamespaceName, ServiceAccountName} -import org.broadinstitute.dsde.workbench.google2.{streamFUntilDone, streamUntilDoneOrTimeout, RegionName} -import org.broadinstitute.dsde.workbench.leonardo.app.Database.{ControlledDatabase, ReferenceDatabase} -import org.broadinstitute.dsde.workbench.leonardo.app.{AppInstall, BuildHelmOverrideValuesParams} -import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider -import org.broadinstitute.dsde.workbench.leonardo.config.Config.refererConfig -import org.broadinstitute.dsde.workbench.leonardo.config._ -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.AppNotFoundException -import org.broadinstitute.dsde.workbench.model.{IP, WorkbenchEmail} -import org.broadinstitute.dsp._ -import org.http4s.Uri -import org.typelevel.log4cats.StructuredLogger - -import java.net.URL -import java.util.{Base64, UUID} -import scala.concurrent.ExecutionContext -import scala.jdk.CollectionConverters._ -import scala.util.Try - -class AKSInterpreter[F[_]](config: AKSInterpreterConfig, - helmClient: HelmAlgebra[F], - azureContainerService: AzureContainerService[F], - azureRelayService: AzureRelayService[F], - samDao: SamDAO[F], - wsmDao: WsmDao[F], - kubeAlg: KubernetesAlgebra[F], - wsmClientProvider: WsmApiClientProvider[F], - legacyWsmDao: WsmDao[F], - authProvider: SamAuthProvider[F], - samService: SamService[F] -)(implicit - appTypeToAppInstall: AppType => AppInstall[F], - executionContext: ExecutionContext, - logger: StructuredLogger[F], - dbRef: DbReference[F], - F: Async[F], - files: Files[F] -) extends AKSAlgebra[F] { - implicit private def booleanDoneCheckable: DoneCheckable[Boolean] = identity[Boolean] - - implicit private def listDoneCheckable[A: DoneCheckable]: DoneCheckable[List[A]] = as => as.forall(_.isDone) - - private[util] def isPodDone(podStatus: PodStatus): Boolean = - podStatus == PodStatus.Failed || podStatus == PodStatus.Succeeded - - implicit private def podDoneCheckable: DoneCheckable[List[PodStatus]] = - (ps: List[PodStatus]) => ps.forall(isPodDone) - - implicit private def createDatabaseDoneCheckable: DoneCheckable[CreatedControlledAzureDatabaseResult] = - _.getJobReport.getStatus != JobReport.StatusEnum.RUNNING - - implicit private def createKubernetesNamespaceDoneCheckable - : DoneCheckable[CreatedControlledAzureKubernetesNamespaceResult] = - _.getJobReport.getStatus != JobReport.StatusEnum.RUNNING - - implicit private def deleteWsmResourceDoneCheckable: DoneCheckable[DeleteControlledAzureResourceResult] = - _.getJobReport.getStatus != JobReport.StatusEnum.RUNNING - - private def getListenerReleaseName(appReleaseName: Release): Release = - Release(s"${appReleaseName.asString}-listener-rls") - - /** Creates an app and polls it for completion */ - override def createAndPollApp(params: CreateAKSAppParams)(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - ctx <- ev.ask - - // Grab records from the database - dbAppOpt <- KubernetesServiceDbQueries - .getFullAppById(CloudContext.Azure(params.cloudContext), params.appId) - .transaction - dbApp <- F.fromOption(dbAppOpt, - AppNotFoundException(CloudContext.Azure(params.cloudContext), - params.appName, - ctx.traceId, - "No active app found in DB" - ) - ) - app = dbApp.app - namespacePrefix = app.appResources.namespace.value - - _ <- logger.info(ctx.loggingCtx)( - s"Begin app creation for app ${params.appName.value} in cloud context ${params.cloudContext.asString}" - ) - - // Query the Landing Zone service for the landing zone resources - leoAuth <- samDao.getLeoAuthToken - landingZoneResources <- childSpan("getLandingZoneResources").use { implicit ev => - legacyWsmDao.getLandingZoneResources(params.billingProfileId, leoAuth) - } - - // Get the optional storage container for the workspace - storageContainerOpt <- childSpan("getWorkspaceStorageContainer").use { implicit ev => - wsmDao.getWorkspaceStorageContainer( - params.workspaceId, - leoAuth - ) - } - - wsmResourceApi <- buildWsmResourceApiClient - - // Create or fetch WSM managed identity (if shared app) - // The managed identity name is either: - // shared apps --> the WSM identity --> shared apps - // private apps --> pet managed identity (stored in the googleServiceAccount' column in the APP table) - // for private apps, set the managedIdentity to None so it can be supplied below - wsmManagedIdentityOpt <- app.samResourceId.resourceType match { - case SamResourceType.SharedApp => - // if a managed identity has already been created in the workspace use that otherwise create a new managed identity - createOrFetchWsmManagedIdentity(app, wsmResourceApi, params.workspaceId, namespacePrefix) - case _ => F.pure(None) - } - managedIdentityName = ManagedIdentityName( - wsmManagedIdentityOpt - .map(_.managedIdentityName) - .getOrElse(app.googleServiceAccount.value.split('/').last) - ) - - // create any missing AppControlledResources - _ <- childSpan("createMissingAppControlledResources").use { implicit ev => - createMissingAppControlledResources( - app, - app.appType, - params.workspaceId, - landingZoneResources, - wsmResourceApi - ) - } - - // Create or fetch WSM databases - wsmDatabases <- childSpan("createWsmDatabaseResources").use { implicit ev => - createOrFetchWsmDatabaseResources( - app, - app.appType, - params.workspaceId, - namespacePrefix, - wsmManagedIdentityOpt.map(_.wsmResourceName), - landingZoneResources, - wsmResourceApi - ) - } - - // get ReferenceDatabases from WSM - referenceDatabaseNames = app.appType.databases.collect { case ReferenceDatabase(name) => name }.toSet - referenceDatabases <- - if (referenceDatabaseNames.nonEmpty) { - retrieveWsmDatabases(wsmResourceApi, referenceDatabaseNames, params.workspaceId.value) - } else F.pure(List.empty) - - // Create or fetch WSM kubernetes namespace - namespace <- childSpan("createWsmNamespaceResource").use { implicit ev => - createOrFetchWsmNamespace(app, - wsmDatabases, - wsmResourceApi, - namespacePrefix, - params.workspaceId, - wsmManagedIdentityOpt - ) - } - - // Create relay hybrid connection pool - // TODO: make into a WSM resource - hcName = RelayHybridConnectionName(s"${params.appName.value}-${params.workspaceId.value}") - relayPrimaryKey <- childSpan("createRelayHybridConnection").use { implicit ev => - azureRelayService.createRelayHybridConnection(landingZoneResources.relayNamespace, hcName, params.cloudContext) - } - relayDomain = s"${landingZoneResources.relayNamespace.value}${AzureEnvironmentConverter.relaySuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}" - relayEndpoint = s"https://${relayDomain}/" - relayPath = Uri.unsafeFromString(relayEndpoint) / hcName.value - - // Authenticate helm client - authContext <- getHelmAuthContext(landingZoneResources.aksCluster.asClusterName, - params.cloudContext, - namespace.name - ) - - // Build listener helm values - values = BuildHelmChartValues.buildListenerChartOverrideValuesString( - app.release, - app.samResourceId, - landingZoneResources.relayNamespace, - hcName, - relayPrimaryKey, - app.appType, - params.workspaceId, - app.appName, - refererConfig.validHosts + relayDomain, - config.samConfig, - config.listenerImage, - config.leoUrlBase - ) - - _ <- logger.info(ctx.loggingCtx)( - s"Relay listener values for app ${params.appName.value} are ${values.asString}" - ) - - // Install listener helm chart - _ <- childSpan("helmInstallRelayListener").use { _ => - helmClient - .installChart( - getListenerReleaseName(app.release), - config.listenerChartConfig.chartName, - config.listenerChartConfig.chartVersion, - values, - false - ) - .run(authContext) - } - - // Build app helm values - helmOverrideValueParams = BuildHelmOverrideValuesParams( - app, - params.workspaceId, - params.cloudContext, - params.billingProfileId, - landingZoneResources, - storageContainerOpt, - relayPath, - namespace.serviceAccountName, - managedIdentityName, - wsmDatabases ++ referenceDatabases, - config - ) - values <- app.appType.buildHelmOverrideValues(helmOverrideValueParams) - - // Install app chart - _ <- childSpan("helmInstallApp").use { _ => - helmClient - .installChart( - app.release, - app.chart.name, - app.chart.version, - values, - createNamespace = false - ) - .run(authContext) - } - - appOk <- childSpan("pollAppCreation").use { implicit ev => - pollAppCreation(app.auditInfo.creator, relayPath, app.appType) - } - _ <- F.raiseWhen(!appOk)( - AppCreationException( - s"App ${params.appName.value} failed to start in cluster ${landingZoneResources.aksCluster.name} in cloud context ${params.cloudContext.asString}", - Some(ctx.traceId) - ) - ) - - // Populate async fields in the KUBERNETES_CLUSTER table. - // For Azure we don't need each field, but we do need the relay https endpoint. - _ <- kubernetesClusterQuery - .updateAsyncFields( - dbApp.cluster.id, - KubernetesClusterAsyncFields( - IP(relayEndpoint), - IP("[unset]"), - NetworkFields( - landingZoneResources.vnetName, - landingZoneResources.aksSubnetName, - IpRange("[unset]") - ) - ) - ) - .transaction - - _ <- kubernetesClusterQuery - .updateRegion(dbApp.cluster.id, RegionName(landingZoneResources.region.name)) - .transaction - - // If we've got here, update the App status to Running. - _ <- appQuery.updateStatus(params.appId, AppStatus.Running).transaction - - _ <- logger.info(ctx.loggingCtx)( - s"Finished app creation for app ${params.appName.value} in cluster ${landingZoneResources.aksCluster.name} in cloud context ${params.cloudContext.asString}" - ) - } yield () - - override def updateAndPollApp(params: UpdateAKSAppParams)(implicit ev: Ask[F, AppContext]): F[Unit] = { - for { - ctx <- ev.ask - - // Build WSM client - wsmControlledResourceApi <- buildWsmControlledResourceApiClient - wsmResourceApi <- buildWsmResourceApiClient - - workspaceId <- F.fromOption( - params.workspaceId, - AppUpdateException( - s"${params.appName} must have a Workspace in the Azure cloud context", - Some(ctx.traceId) - ) - ) - - // Grab records from the database - dbAppOpt <- KubernetesServiceDbQueries - .getActiveFullAppByName(CloudContext.Azure(params.cloudContext), params.appName) - .transaction - dbApp <- F.fromOption( - dbAppOpt, - AppNotFoundException(CloudContext.Azure(params.cloudContext), - params.appName, - ctx.traceId, - "No active app found in DB" - ) - ) - _ <- logger.info(ctx.loggingCtx)(s"Updating app ${params.appName} in workspace ${params.workspaceId}") - - app = dbApp.app - leoAuth <- samDao.getLeoAuthToken - token <- authProvider.getLeoAuthToken - - workspaceDescOpt <- childSpan("getWorkspace").use { implicit ev => - wsmClientProvider.getWorkspace(token, workspaceId) - } - workspaceDesc <- F.fromOption( - workspaceDescOpt, - AppUpdateException(s"Workspace ${workspaceId.value.toString} not found in WSM", Some(ctx.traceId)) - ) - // Query the Landing Zone service for the landing zone resources - billingProfileId = BillingProfileId(workspaceDesc.spendProfile) - landingZoneResources <- childSpan("getLandingZoneResources").use { implicit ev => - legacyWsmDao.getLandingZoneResources(billingProfileId, leoAuth) - } - - // Get the optional storage container for the workspace - storageContainerOpt <- childSpan("getWorkspaceStorageContainer").use { implicit ev => - wsmDao.getWorkspaceStorageContainer( - workspaceId, - leoAuth - ) - } - - // Call WSM to get the managed identity for the app. - // This is optional because a WSM identity is only created for shared apps. - wsmIdentities <- appControlledResourceQuery - .getAllForAppByType(app.id.id, WsmResourceType.AzureManagedIdentity) - .transaction - wsmIdentityOpt <- wsmIdentities.headOption.traverse { wsmIdentity => - F.blocking(wsmControlledResourceApi.getAzureManagedIdentity(workspaceId.value, wsmIdentity.resourceId.value)) - } - - // create any missing AppControlledResources - _ <- createMissingAppControlledResources( - app, - app.appType, - workspaceId, - landingZoneResources, - wsmResourceApi - ) - - // get list of APP_CONTROLLED_RESOURCES - controlledDatabases <- appControlledResourceQuery - .getAllForAppByType(app.id.id, WsmResourceType.AzureDatabase) - .transaction - // Call WSM to get more info about each database (by resourceId) that exists in APP_CONTROLLED_RESOURCE - wsmDatabases <- controlledDatabases.traverse { controlledDatabase => - F.blocking(wsmControlledResourceApi.getAzureDatabase(workspaceId.value, controlledDatabase.resourceId.value)) - .map(db => - WsmControlledDatabaseResource(db.getMetadata.getName, - db.getAttributes.getDatabaseName, - db.getMetadata.getResourceId - ) - ) - } - - // call WSM resource API to get list of ReferenceDatabases - referenceDatabaseNames = app.appType.databases.collect { case ReferenceDatabase(name) => name }.toSet - referenceDatabases <- - if (referenceDatabaseNames.nonEmpty) { - retrieveWsmDatabases(wsmResourceApi, referenceDatabaseNames, workspaceId.value) - } else F.pure(List.empty) - - // Call WSM to get the Kubernetes namespace (required) - wsmNamespaces <- appControlledResourceQuery - .getAllForAppByType(app.id.id, WsmResourceType.AzureKubernetesNamespace) - .transaction - wsmNamespaceOpt <- wsmNamespaces.headOption.traverse { wsmNamespace => - F.blocking( - wsmControlledResourceApi.getAzureKubernetesNamespace(workspaceId.value, wsmNamespace.resourceId.value) - ) - } - wsmNamespace <- F.fromOption(wsmNamespaceOpt, - AppUpdateException("WSM namespace required for app", Some(ctx.traceId)) - ) - - // The k8s namespace name and service account name are in the WSM response - namespaceName = NamespaceName(wsmNamespace.getAttributes.getKubernetesNamespace) - ksaName = ServiceAccountName(wsmNamespace.getAttributes.getKubernetesServiceAccount) - - // The managed identity name is either the WSM identity (for shared apps) or the - // pet managed identity (for private apps). The latter is confusingly stored in the - // 'googleServiceAccount' column in the APP table. - managedIdentityName = ManagedIdentityName( - wsmIdentityOpt - .map(_.getAttributes.getManagedIdentityName) - .getOrElse(app.googleServiceAccount.value.split('/').last) - ) - - // Get relay hybrid connection information - hcName = RelayHybridConnectionName(s"${params.appName.value}-${workspaceId.value}") - relayDomain = s"${landingZoneResources.relayNamespace.value}${AzureEnvironmentConverter.relaySuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}" - relayEndpoint = s"https://${relayDomain}/" - relayPath = Uri.unsafeFromString(relayEndpoint) / hcName.value - relayPrimaryKey <- azureRelayService.getRelayHybridConnectionKey(landingZoneResources.relayNamespace, - hcName, - params.cloudContext - ) - - // Authenticate helm client - authContext <- getHelmAuthContext(landingZoneResources.aksCluster.asClusterName, - params.cloudContext, - namespaceName - ) - - // Check if the app is alive before attempting to update. We transition the app to error status if this fails - isAppAlive <- childSpan("isAppAlive").use { implicit ev => - isAppAlive(app.auditInfo.creator, relayPath, app.appType) - } - _ <- - if (isAppAlive) - F.unit - else - F.raiseError[Unit]( - AppUpdatePollingException( - s"App ${params.appName.value} was not alive and therefore failed to update in cluster ${landingZoneResources.aksCluster.name} in cloud context ${params.cloudContext.asString}", - Some(ctx.traceId) - ) - ) - - // The app is blocked in updating now (as in, if leo restarts between here and the app transitioning back to `Running`, it is unusable - // See: https://broadworkbench.atlassian.net/browse/IA-4867 - _ <- appQuery.updateStatus(app.id, AppStatus.Updating).transaction - - // Update the relay listener deployment - _ <- childSpan("helmUpdateListener").use { implicit ev => - updateListener(authContext, - app, - landingZoneResources, - workspaceId, - hcName, - relayPrimaryKey, - relayDomain, - config.listenerChartConfig - ) - } - - // Build app helm values - helmOverrideValueParams = BuildHelmOverrideValuesParams( - app, - workspaceId, - params.cloudContext, - billingProfileId, - landingZoneResources, - storageContainerOpt, - relayPath, - ksaName, - managedIdentityName, - wsmDatabases ++ referenceDatabases, - config - ) - values <- app.appType.buildHelmOverrideValues(helmOverrideValueParams) - - // Upgrade app chart version and explicitly pass the values - _ <- childSpan("helmUpdateApp").use { _ => - helmClient - .upgradeChart( - app.release, - app.chart.name, - params.appChartVersion, - values - ) - .run(authContext) - } - - // Poll until all pods in the app namespace are running - // TODO: we should be able to test this method and the subsequent `AppUpdatePollingException` more easily when we start to implement rollbacks - appOk <- childSpan("pollAppUpdate").use { implicit ev => - pollAppUpdate(app.auditInfo.creator, relayPath, app.appType) - } - _ <- - if (appOk) - F.unit - else - F.raiseError[Unit]( - AppUpdatePollingException( - s"App ${params.appName.value} failed to update in cluster ${landingZoneResources.aksCluster.name} in cloud context ${params.cloudContext.asString}", - Some(ctx.traceId) - ) - ) - - _ <- logger.info( - s"Update app operation has finished for app ${app.appName.value} in cluster ${landingZoneResources.aksCluster.name}" - ) - - // Update app chart version in the DB - _ <- appQuery.updateChart(app.id, Chart(app.chart.name, params.appChartVersion)).transaction - // Put app status back to running - _ <- appQuery.updateStatus(app.id, AppStatus.Running).transaction - - _ <- logger.info(s"Done updating app ${params.appName} in workspace ${params.workspaceId}") - } yield () - } - - override def deleteApp(params: DeleteAKSAppParams)(implicit ev: Ask[F, AppContext]): F[Unit] = { - val DeleteAKSAppParams(appName, workspaceId, cloudContext, billingProfileId) = params - for { - ctx <- ev.ask - - // Grab records from the database - dbAppOpt <- KubernetesServiceDbQueries - .getActiveFullAppByName(CloudContext.Azure(cloudContext), params.appName) - .transaction - dbApp <- F.fromOption( - dbAppOpt, - AppNotFoundException(CloudContext.Azure(cloudContext), params.appName, ctx.traceId, "No active app found in DB") - ) - _ <- logger.info(ctx.loggingCtx)(s"Deleting app $appName in workspace $workspaceId") - - app = dbApp.app - - // Query the Landing Zone service for the landing zone resources - leoAuth <- samDao.getLeoAuthToken - token = leoAuth.credentials.toString().split(" ")(1) - landingZoneResources <- childSpan("getLandingZoneResources").use { implicit ev => - legacyWsmDao.getLandingZoneResources(billingProfileId, leoAuth) - } - - wsmResourceApi <- buildWsmResourceApiClient - - // create any missing AppControlledResources - _ <- createMissingAppControlledResources( - app, - app.appType, - workspaceId, - landingZoneResources, - wsmResourceApi - ) - - // WSM deletion order matters here. Delete WSM database resources first. - wsmDatabases <- appControlledResourceQuery - .getAllForAppByType(app.id.id, WsmResourceType.AzureDatabase) - .transaction - _ <- childSpan("deleteWsmDatabases").use { implicit ev => - wsmDatabases.traverse { database => - deleteWsmResource(workspaceId, app, database) - } - } - - // Then delete namespace resources - wsmNamespaces <- appControlledResourceQuery - .getAllForAppByType(app.id.id, WsmResourceType.AzureKubernetesNamespace) - .transaction - deletedNamespace <- childSpan("deleteWsmNamespace").use { implicit ev => - wsmNamespaces - .traverse { namespace => - deleteWsmResource(workspaceId, app, namespace) - } - .map(_.nonEmpty) - } - - // Then delete identity resources - wsmIdentities <- appControlledResourceQuery - .getAllForAppByType(app.id.id, WsmResourceType.AzureManagedIdentity) - .transaction - _ <- childSpan("deleteWsmIdentity").use { implicit ev => - wsmIdentities.traverse { identity => - deleteWsmResource(workspaceId, app, identity) - } - } - - // If this app did not have a WSM-tracked kubernetes namespace, delete it explicitly - _ <- - if (deletedNamespace) F.unit - else { - for { - client <- kubeAlg.createAzureClient(cloudContext, landingZoneResources.aksCluster.asClusterName) - - kubernetesNamespace = KubernetesNamespace(app.appResources.namespace) - - // Delete the namespace which should delete all resources in it - _ <- kubeAlg.deleteNamespace(client, kubernetesNamespace) - - // Poll until the namespace is actually deleted - // Mapping to inverse because booleanDoneCheckable defines `Done` when it becomes `true` - fa = kubeAlg.namespaceExists(client, kubernetesNamespace).map(exists => !exists) - _ <- streamUntilDoneOrTimeout(fa, - config.appMonitorConfig.deleteApp.maxAttempts, - config.appMonitorConfig.deleteApp.initialDelay, - "delete namespace timed out" - ) - } yield () - } - - // Delete hybrid connection for this app - // for backwards compatibility, name used to be just the appName - // TODO: make relay hybrid connection a WSM resource - name = app.customEnvironmentVariables.getOrElse("RELAY_HYBRID_CONNECTION_NAME", app.appName.value) - - _ <- childSpan("deleteRelayHybridConnection").use { implicit ev => - azureRelayService - .deleteRelayHybridConnection( - landingZoneResources.relayNamespace, - RelayHybridConnectionName(name), - cloudContext - ) - .handleErrorWith { - case e: ManagementException if e.getResponse.getStatusCode == StatusCodes.NotFound.intValue => - logger.info(ctx.loggingCtx)(s"${name} does not exist to delete in ${cloudContext}") - case e => F.raiseError[Unit](e) - } - } - - // Delete the Sam resource getCachedArbitraryPetAccessToken - _ <- childSpan("deleteSamResource").use { implicit ev => - samService.deleteResource(token, dbApp.app.samResourceId) - } - - _ <- logger.info( - s"Delete app operation has finished for app ${app.appName.value} in workspace ${app.workspaceId}" - ) - - _ <- appQuery.markAsDeleted(app.id, ctx.now).transaction - - _ <- logger.info(s"Done deleting app $appName in workspace $workspaceId") - } yield () - } - - private[util] def pollApp(userEmail: WorkbenchEmail, relayBaseUri: Uri, appInstall: AppInstall[F])(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = for { - authHeader <- samDao.getLeoAuthToken - - res <- appInstall.checkStatus(relayBaseUri, authHeader) - } yield res - - private[util] def pollAppCreation(userEmail: WorkbenchEmail, relayBaseUri: Uri, appInstall: AppInstall[F])(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - for { - _ <- ev.ask - op = pollApp(userEmail, relayBaseUri, appInstall) - - appOk <- streamFUntilDone( - op, - maxAttempts = config.appMonitorConfig.createApp.maxAttempts, - delay = config.appMonitorConfig.createApp.interval - ).interruptAfter(config.appMonitorConfig.createApp.interruptAfter).compile.lastOrError - } yield appOk.isDone - - private[util] def pollAppUpdate(userEmail: WorkbenchEmail, relayBaseUri: Uri, appInstall: AppInstall[F])(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - for { - _ <- ev.ask - op = pollApp(userEmail, relayBaseUri, appInstall) - appOk <- streamFUntilDone( - op, - maxAttempts = config.appMonitorConfig.updateApp.maxAttempts, - delay = config.appMonitorConfig.updateApp.interval - ).interruptAfter(config.appMonitorConfig.updateApp.interruptAfter).compile.lastOrError - } yield appOk.isDone - - private[util] def isAppAlive(userEmail: WorkbenchEmail, relayBaseUri: Uri, appInstall: AppInstall[F])(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - for { - _ <- ev.ask - op = pollApp(userEmail, relayBaseUri, appInstall) - appOk <- streamFUntilDone( - op, - maxAttempts = config.appMonitorConfig.appLiveness.maxAttempts, - delay = config.appMonitorConfig.appLiveness.interval - ).compile.lastOrError - } yield appOk.isDone - - private[util] def getHelmAuthContext(clusterName: AKSClusterName, - cloudContext: AzureCloudContext, - namespaceName: NamespaceName - )(implicit ev: Ask[F, AppContext]): F[AuthContext] = - for { - ctx <- ev.ask - - credentials <- azureContainerService.getClusterCredentials(clusterName, cloudContext) - - // Don't use AppContext.now for the tmp file name because we want it to be unique - // for each helm invocation - now <- nowInstant - - // The helm client requires the ca cert passed as a file - hence writing a temp file before helm invocation. - caCertFile <- writeTempFile(s"aks_ca_cert_${now.toEpochMilli}", - Base64.getDecoder.decode(credentials.certificate.value) - ) - - authContext = AuthContext( - Namespace(namespaceName.value), - KubeToken(credentials.token.value), - KubeApiServer(credentials.server.value), - CaCertFile(caCertFile.toAbsolutePath) - ) - - _ <- logger.info(ctx.loggingCtx)( - s"Helm auth context for cluster ${clusterName.value} in cloud context ${cloudContext.asString}: ${authContext - .copy(kubeToken = org.broadinstitute.dsp.KubeToken(""))}" - ) - - } yield authContext - - private def getWsmCommonFields(name: String, - description: String, - app: App, - cloningInstructions: CloningInstructionsEnum - ): bio.terra.workspace.model.ControlledResourceCommonFields = { - val commonFieldsBase = new bio.terra.workspace.model.ControlledResourceCommonFields() - .resourceId(UUID.randomUUID()) - .name(name) - .description(description) - .managedBy(bio.terra.workspace.model.ManagedBy.APPLICATION) - .cloningInstructions(cloningInstructions) - app.samResourceId.accessScope match { - case Some(AppAccessScope.WorkspaceShared) => - commonFieldsBase.accessScope(bio.terra.workspace.model.AccessScope.SHARED_ACCESS) - case _ => - commonFieldsBase - .accessScope(bio.terra.workspace.model.AccessScope.PRIVATE_ACCESS) - .privateResourceUser( - new bio.terra.workspace.model.PrivateResourceUser() - .userName(app.auditInfo.creator.value) - .privateResourceIamRole(bio.terra.workspace.model.ControlledResourceIamRole.WRITER) - ) - } - } - - private def generateWsmNameForIdentity(appType: AppType): String = s"id${appType.toString.toLowerCase}" - - private[util] def createAzureManagedIdentity(app: App, namespacePrefix: String, workspaceId: WorkspaceId)(implicit - ev: Ask[F, AppContext] - ): F[Option[WsmManagedAzureIdentity]] = - childSpan("createWsmIdentityResource").use { implicit ev => - createWsmIdentityResource(app, namespacePrefix, workspaceId) - .map(i => - WsmManagedAzureIdentity(i.getAzureManagedIdentity.getMetadata.getName, - i.getAzureManagedIdentity.getAttributes.getManagedIdentityName - ) - ) - .map(_.some) - } - - private[util] def createWsmIdentityResource(app: App, namespacePrefix: String, workspaceId: WorkspaceId)(implicit - ev: Ask[F, AppContext] - ): F[CreatedControlledAzureManagedIdentity] = - for { - ctx <- ev.ask - _ <- logger.info(ctx.loggingCtx)( - s"Creating WSM identity for app ${app.appName.value} in cloud workspace ${workspaceId.value}" - ) - - // Build WSM client - wsmApi <- buildWsmControlledResourceApiClient - - // Name of the managed identity. Must be unique per landing zone. - identityName = s"id${namespacePrefix.split('-').headOption.getOrElse(namespacePrefix)}" - - // Name of the WSM resource. Must be unique per workspace. - // For shared apps, name it by the appType so it's semantically meaningful. - // There can only be at most 1 shared app type per workspace anyway. - // For private apps, use managed identity name to ensure uniqueness. - wsmResourceName = app.samResourceId.resourceType match { - case SamResourceType.SharedApp => generateWsmNameForIdentity(app.appType) - case _ => identityName - } - - cloningInstructions = - if (app.appType == AppType.WorkflowsApp) CloningInstructionsEnum.RESOURCE else CloningInstructionsEnum.NOTHING - - identityCommonFields = getWsmCommonFields(wsmResourceName, - s"Identity for Leo app ${app.appName.value}", - app, - cloningInstructions - ) - createIdentityParams = new AzureManagedIdentityCreationParameters().name( - identityName - ) - createIdentityRequest = new CreateControlledAzureManagedIdentityRequestBody() - .common(identityCommonFields) - .azureManagedIdentity(createIdentityParams) - - _ <- logger.info(ctx.loggingCtx)(s"WSM create identity request: ${createIdentityRequest}") - - // Execute WSM call - createIdentityResponse <- F.blocking(wsmApi.createAzureManagedIdentity(createIdentityRequest, workspaceId.value)) - - _ <- logger.info(ctx.loggingCtx)(s"WSM create identity response: ${createIdentityResponse}") - - // Save record in APP_CONTROLLED_RESOURCE table - _ <- appControlledResourceQuery - .insert( - app.id.id, - WsmControlledResourceId(createIdentityResponse.getResourceId), - WsmResourceType.AzureManagedIdentity, - AppControlledResourceStatus.Created - ) - .transaction - - } yield createIdentityResponse - - private[util] def createOrFetchWsmDatabaseResources(app: App, - appInstall: AppInstall[F], - workspaceId: WorkspaceId, - namespacePrefix: String, - owner: Option[String], - landingZoneResources: LandingZoneResources, - wsmResourceApi: ResourceApi - )(implicit ev: Ask[F, AppContext]): F[List[WsmControlledDatabaseResource]] = - for { - ctx <- ev.ask - _ <- F.raiseWhen(landingZoneResources.postgresServer.isEmpty)( - AppCreationException("Postgres server not found in landing zone", Some(ctx.traceId)) - ) - wsmApi <- buildWsmControlledResourceApiClient - - // get a list of database types required for this app - controlledDbsForApp = appInstall.databases.collect { case d @ ControlledDatabase(_, _, _) => d } - // retrieve databases that might already be created in workspace - existingControlledDbsInWorkspace <- retrieveWsmDatabases(wsmResourceApi, - controlledDbsForApp.map(_.prefix).toSet, - workspaceId.value - ) - wsmControlledDBResources <- controlledDbsForApp - .traverse { controlledDbForApp => - // if a database already exists (because of workspace cloning or Leo restarting mid-app creation) use that otherwise create a new one - if ( - existingControlledDbsInWorkspace - .exists(existingDb => controlledDbForApp.prefix == existingDb.wsmDatabaseName) - ) { - logger.info( - s"Database found in WSM for app ${app.appName}, using previously created database: $existingControlledDbsInWorkspace" - ) >> - F.pure( - existingControlledDbsInWorkspace - .find(clonedDatabase => controlledDbForApp.prefix == clonedDatabase.wsmDatabaseName) - .get - ) - } else { - logger.info(s"Creating databases for app ${app.appName}") >> - createWsmDatabaseResource(app, workspaceId, controlledDbForApp, namespacePrefix, owner, wsmApi).map { - db => - WsmControlledDatabaseResource(db.getAzureDatabase.getMetadata.getName, - db.getAzureDatabase.getAttributes.getDatabaseName - ) - } - } - } - } yield wsmControlledDBResources - - private[util] def createMissingAppControlledResources(app: App, - appInstall: AppInstall[F], - workspaceId: WorkspaceId, - landingZoneResources: LandingZoneResources, - wsmResourceApi: ResourceApi - )(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - ctx <- ev.ask - _ <- F.raiseWhen(landingZoneResources.postgresServer.isEmpty)( - AppCreationException("Postgres server not found in landing zone", Some(ctx.traceId)) - ) - - // get a list of database types required for this app - controlledDbsRequiredForApp = appInstall.databases.collect { case d @ ControlledDatabase(_, _, _) => d } - - // retrieve set of databases WSM has created for this workspace (name, wsmDatabaseName, resource_id) - existingWsmDbsInWorkspace: List[WsmControlledDatabaseResource] <- retrieveWsmDatabases( - wsmResourceApi, - controlledDbsRequiredForApp.map(_.prefix).toSet, - workspaceId.value - ) - - // Get list of APP_CONTROLLED_RESOURCE for this app (appId, resource_id, state) - appControlledResources: List[AppControlledResourceRecord] <- appControlledResourceQuery - .getAllForAppByType(app.id.id, WsmResourceType.AzureDatabase) - .transaction - - // Find WSM databases in workspace that do not exist in the appControlledResources list based on resourceId - wsmDbsNotinAppResources = existingWsmDbsInWorkspace.filterNot { wsmDb => - appControlledResources.exists(appRes => appRes.resourceId.value.equals(wsmDb.controlledResourceId)) - } - - // create a APP_CONTROLLED_RESOURCE for any wsm database that does not have one - _ <- wsmDbsNotinAppResources.traverse { db => - for { - res <- appControlledResourceQuery - .insert( - app.id.id, - WsmControlledResourceId(db.controlledResourceId), - WsmResourceType.AzureDatabase, - AppControlledResourceStatus.Created - ) - .transaction - } yield db - } - - } yield () - - private[util] def createWsmDatabaseResource(app: App, - workspaceId: WorkspaceId, - database: ControlledDatabase, - namespacePrefix: String, - owner: Option[String], - wsmApi: ControlledAzureResourceApi - )(implicit - ev: Ask[F, AppContext] - ): F[CreatedControlledAzureDatabaseResult] = { - // Build create DB request - - // Name of the database. Must be unique per landing zone. - val dbName = s"${database.prefix}_${namespacePrefix.split('-').headOption.getOrElse(namespacePrefix)}" - - // Name of the WSM resource. Must be unique per workspace. - // For shared apps, name it by the databasePrefix so it's semantically meaningful. - // There can only be at most 1 shared app type per workspace anyway. - // For private apps, use the database name to ensure uniqueness. - val wsmResourceName = app.samResourceId.resourceType match { - case SamResourceType.SharedApp => database.prefix - case _ => dbName - } - - val databaseCommonFields = - getWsmCommonFields(wsmResourceName, - s"${database.prefix} database for Leo app ${app.appName.value}", - app, - database.cloningInstructions - ) - val createDatabaseParams = new AzureDatabaseCreationParameters() - .name(dbName) - .allowAccessForAllWorkspaceUsers(database.allowAccessForAllWorkspaceUsers) - owner.foreach(createDatabaseParams.setOwner) - val createDatabaseJobControl = new JobControl().id(dbName) - val createDatabaseRequest = new CreateControlledAzureDatabaseRequestBody() - .common(databaseCommonFields) - .azureDatabase(createDatabaseParams) - .jobControl(createDatabaseJobControl) - - for { - ctx <- ev.ask - _ <- logger.info(ctx.loggingCtx)( - s"Creating ${database.prefix} database for app ${app.appName.value} in cloud workspace ${workspaceId.value}" - ) - _ <- logger.info(ctx.loggingCtx)(s"WSM create database request: ${createDatabaseRequest}") - - _ <- appControlledResourceQuery - .insert( - app.id.id, - WsmControlledResourceId(createDatabaseRequest.getCommon.getResourceId), - WsmResourceType.AzureDatabase, - AppControlledResourceStatus.Creating - ) - .transaction - - // Execute WSM call - createDatabaseResponse <- F.blocking(wsmApi.createAzureDatabase(createDatabaseRequest, workspaceId.value)) - - _ <- logger.info(ctx.loggingCtx)(s"WSM create database response: ${createDatabaseResponse}") - - // Poll for DB creation - // We don't actually care about the JobReport - just that it succeeded. - op = F.blocking(wsmApi.getCreateAzureDatabaseResult(workspaceId.value, dbName)) - result <- streamFUntilDone( - op, - config.appMonitorConfig.createApp.maxAttempts, - config.appMonitorConfig.createApp.interval - ).interruptAfter(config.appMonitorConfig.createApp.interruptAfter).compile.lastOrError - - _ <- logger.info(ctx.loggingCtx)(s"WSM create database job result: ${result}") - - _ <- F.raiseWhen(result.getJobReport.getStatus != JobReport.StatusEnum.SUCCEEDED)( - AppCreationException( - s"WSM database creation failed for app ${app.appName.value}. WSM response: ${result}", - Some(ctx.traceId) - ) - ) - - // Save record in APP_CONTROLLED_RESOURCE table - _ <- appControlledResourceQuery - .updateStatus( - WsmControlledResourceId(result.getAzureDatabase.getMetadata.getResourceId), - AppControlledResourceStatus.Created - ) - .transaction - } yield result - } - - private[util] def retrieveWsmManagedIdentity(resourceApi: ResourceApi, - appType: AppType, - workspaceId: UUID - ): F[Option[WsmManagedAzureIdentity]] = { - val wsmResourceName = generateWsmNameForIdentity(appType) - F.blocking( - getWorkspaceResourceByName(workspaceId, wsmResourceName, resourceApi).map { identity => - WsmManagedAzureIdentity( - wsmResourceName, - identity.getResourceAttributes.getAzureManagedIdentity.getManagedIdentityName - ) - } - ) - } - - private[util] def retrieveWsmDatabases(resourceApi: ResourceApi, - databaseNames: Set[String], - workspaceId: UUID - ): F[List[WsmControlledDatabaseResource]] = - // TODO: this currently matches on the 'name' (actually type) of database so for example - // it compares for a 'cbas' or 'cromwellmetadata' database. In the future, their maybe - // multiple of those in a workspace so this approach will have to be re-considered - // see https://broadworkbench.atlassian.net/browse/IA-4844 - F.blocking( - databaseNames - .flatMap(dbName => - getWorkspaceResourceByName(workspaceId, dbName, resourceApi).map { r => - WsmControlledDatabaseResource(r.getMetadata.getName, - r.getResourceAttributes.getAzureDatabase.getDatabaseName, - r.getMetadata.getResourceId - ) - } - ) - .toList - ) - - private[util] def createOrFetchWsmManagedIdentity(app: App, - resourceApi: ResourceApi, - workspaceId: WorkspaceId, - namespacePrefix: String - )(implicit ev: Ask[F, AppContext]): F[Option[WsmManagedAzureIdentity]] = - for { - wsmManagedIdentityOpt <- - retrieveWsmManagedIdentity(resourceApi, app.appType, workspaceId.value).flatMap { - case Some(identity) => - logger.info( - s"Managed ID found in WSM app ${app.appName}, using previously created identity: ${identity.managedIdentityName}" - ) >> - F.pure(Option(identity)) - case None => - createAzureManagedIdentity(app, namespacePrefix, workspaceId) - } - } yield wsmManagedIdentityOpt - - private[util] def createOrFetchWsmNamespace(app: App, - wsmDatabases: List[WsmControlledDatabaseResource], - resourceApi: ResourceApi, - namespacePrefix: String, - workspaceId: WorkspaceId, - wsmManagedIdentityOpt: Option[WsmManagedAzureIdentity] - )(implicit ev: Ask[F, AppContext]): F[WsmControlledKubernetesNamespaceResource] = - for { - wsmNamespace <- retrieveWsmNamespace(resourceApi, namespacePrefix, workspaceId.value) - namespace <- wsmNamespace match { - case Some(ns) => - logger.info( - s"Namespace found in WSM for app ${app.appName}, using previously created namespace: ${ns.name}" - ) - F.pure(ns) - case None => - createWsmKubernetesNamespaceResource( - app, - workspaceId, - namespacePrefix, - wsmDatabases.map(_.wsmDatabaseName), - wsmManagedIdentityOpt.map(_.wsmResourceName) - ) - } - } yield namespace - - private[util] def retrieveWsmNamespace(resourceApi: ResourceApi, - namespacePrefix: String, - workspaceId: UUID - ): F[Option[WsmControlledKubernetesNamespaceResource]] = { - // The full namespace name will be {namespacePrefix}-{workspaceId}, - // and the resource name is the same as the kubernetes namespace - // The construction of this is done in ControlledAzureResourceApiController.createAzureKubernetesNamespace - val namespaceName = s"$namespacePrefix-$workspaceId" - F.blocking( - getWorkspaceResourceByName(workspaceId, namespaceName, resourceApi).map { ns => - WsmControlledKubernetesNamespaceResource( - NamespaceName(ns.getResourceAttributes.getAzureKubernetesNamespace.getKubernetesNamespace), - WsmControlledResourceId(ns.getMetadata.getResourceId), - ServiceAccountName( - ns.getResourceAttributes.getAzureKubernetesNamespace.getKubernetesServiceAccount - ) - ) - } - ) - } - - private[util] def createWsmKubernetesNamespaceResource(app: App, - workspaceId: WorkspaceId, - namespacePrefix: String, - databases: List[String], - identity: Option[String] - )(implicit ev: Ask[F, AppContext]): F[WsmControlledKubernetesNamespaceResource] = - for { - ctx <- ev.ask - - _ <- logger.info(ctx.loggingCtx)( - s"Creating $namespacePrefix namespace for app ${app.appName.value} in cloud workspace ${workspaceId.value}" - ) - - // Build WSM client - wsmApi <- buildWsmControlledResourceApiClient - - // Name of the WSM resource. Must be unique per workspace. - // For shared apps, name it by the appType so it's semantically meaningful. - // There can only be at most 1 shared app type per workspace anyway. - // For private apps, use the namespacePrefix to ensure uniqueness. - wsmResourceName = app.samResourceId.resourceType match { - case SamResourceType.SharedApp => s"${app.appType.toString.toLowerCase}-ns" - case _ => namespacePrefix - } - - // Build common fields - namespaceCommonFields = - getWsmCommonFields(wsmResourceName, - s"$namespacePrefix kubernetes namespace for Leo app ${app.appName.value}", - app, - CloningInstructionsEnum.NOTHING - ) - - // Build createNamespace fields - appExternalDatabaseNames = app.appType.databases.collect { case ReferenceDatabase(name) => name }.toSet - createNamespaceParams = new AzureKubernetesNamespaceCreationParameters() - .namespacePrefix(namespacePrefix) - .databases((databases ++ appExternalDatabaseNames).asJava) - - _ = identity.foreach(createNamespaceParams.setManagedIdentity) - - // Build request - createNamespaceJobControl = new JobControl().id( - namespacePrefix - ) - createNamespaceRequest = new CreateControlledAzureKubernetesNamespaceRequestBody() - .common(namespaceCommonFields) - .azureKubernetesNamespace(createNamespaceParams) - .jobControl(createNamespaceJobControl) - - _ <- logger.info(ctx.loggingCtx)(s"WSM create namespace request: $createNamespaceRequest") - - _ <- appControlledResourceQuery - .insert( - app.id.id, - WsmControlledResourceId(createNamespaceRequest.getCommon.getResourceId), - WsmResourceType.AzureKubernetesNamespace, - AppControlledResourceStatus.Creating - ) - .transaction - - // Execute WSM call - createNamespaceResponse <- F.blocking( - wsmApi.createAzureKubernetesNamespace(createNamespaceRequest, workspaceId.value) - ) - - _ <- logger.info(ctx.loggingCtx)(s"WSM create namespace response: ${createNamespaceResponse}") - - // Poll for namespace creation - op = F.blocking(wsmApi.getCreateAzureKubernetesNamespaceResult(workspaceId.value, namespacePrefix)) - result <- streamFUntilDone( - op, - config.appMonitorConfig.createApp.maxAttempts, - config.appMonitorConfig.createApp.interval - ).interruptAfter(config.appMonitorConfig.createApp.interruptAfter).compile.lastOrError - - _ <- logger.info(ctx.loggingCtx)(s"WSM create namespace job result: ${result}") - - _ <- F.raiseWhen(result.getJobReport.getStatus != JobReport.StatusEnum.SUCCEEDED)( - AppCreationException( - s"WSM namespace creation failed for app ${app.appName.value}. WSM response: ${result}", - Some(ctx.traceId) - ) - ) - - resourceId = WsmControlledResourceId(result.getAzureKubernetesNamespace.getMetadata.getResourceId) - - // Save record in APP_CONTROLLED_RESOURCE table - _ <- appControlledResourceQuery - .updateStatus( - resourceId, - AppControlledResourceStatus.Created - ) - .transaction - namespaceAttributes = result.getAzureKubernetesNamespace.getAttributes - } yield WsmControlledKubernetesNamespaceResource( - NamespaceName(namespaceAttributes.getKubernetesNamespace), - resourceId, - ServiceAccountName(namespaceAttributes.getKubernetesServiceAccount) - ) - - private[util] def deleteAndPollWsmNamespaceResource(workspaceId: WorkspaceId, - app: App, - wsmResource: AppControlledResourceRecord, - jobId: UUID, - deleteResourceRequest: DeleteControlledAzureResourceRequest, - wsmApi: ControlledAzureResourceApi - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - - _ <- logger.info(ctx.loggingCtx)(s"WSM delete namespace request: ${deleteResourceRequest}") - - // Execute WSM call - result <- F.blocking( - wsmApi.deleteAzureKubernetesNamespace(deleteResourceRequest, workspaceId.value, wsmResource.resourceId.value) - ) - - _ <- logger.info(ctx.loggingCtx)(s"WSM delete namespace response: ${result}") - - // Update record in APP_CONTROLLED_RESOURCE table - _ <- appControlledResourceQuery - .updateStatus( - wsmResource.resourceId, - AppControlledResourceStatus.Deleting - ) - .transaction - - // Poll for namespace deletion - op = F.blocking( - wsmApi.getDeleteAzureKubernetesNamespaceResult(workspaceId.value, jobId.toString) - ) - - result <- streamFUntilDone( - op, - config.appMonitorConfig.deleteApp.maxAttempts, - config.appMonitorConfig.deleteApp.interval - ).compile.lastOrError - - _ <- logger.info(ctx.loggingCtx)(s"WSM delete namespace job result: $result") - - _ <- F.raiseWhen(result.getJobReport.getStatus != JobReport.StatusEnum.SUCCEEDED)( - AppDeletionException( - s"WSM namespace deletion failed for app ${app.appName.value}. WSM response: $result" - ) - ) - - // record in APP_CONTROLLED_RESOURCE table is set to deleted by caller - } yield () - - private[util] def deleteAndPollWsmDatabaseResource(workspaceId: WorkspaceId, - app: App, - wsmResource: AppControlledResourceRecord, - jobId: UUID, - deleteResourceRequest: DeleteControlledAzureResourceRequest, - wsmApi: ControlledAzureResourceApi - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - - _ <- logger.info(ctx.loggingCtx)(s"WSM delete database request: ${deleteResourceRequest}") - - // Execute WSM call - result <- F.blocking( - wsmApi.deleteAzureDatabaseAsync(deleteResourceRequest, workspaceId.value, wsmResource.resourceId.value) - ) - - _ <- logger.info(ctx.loggingCtx)(s"WSM delete database response: ${result}") - - // Update record in APP_CONTROLLED_RESOURCE table - _ <- appControlledResourceQuery - .updateStatus( - wsmResource.resourceId, - AppControlledResourceStatus.Deleting - ) - .transaction - - // Poll for database deletion - op = F.blocking( - wsmApi.getDeleteAzureDatabaseResult(workspaceId.value, jobId.toString) - ) - - result <- streamFUntilDone( - op, - config.appMonitorConfig.deleteApp.maxAttempts, - config.appMonitorConfig.deleteApp.interval - ).compile.lastOrError - - _ <- logger.info(ctx.loggingCtx)(s"WSM delete database job result: $result") - - _ <- F.raiseWhen(result.getJobReport.getStatus != JobReport.StatusEnum.SUCCEEDED)( - AppDeletionException( - s"WSM database deletion failed for app ${app.appName.value}. WSM response: $result" - ) - ) - - // record in APP_CONTROLLED_RESOURCE table is set to deleted by caller - } yield () - - private[util] def deleteWsmResource(workspaceId: WorkspaceId, app: App, wsmResource: AppControlledResourceRecord)( - implicit ev: Ask[F, AppContext] - ): F[Unit] = { - val delete = for { - ctx <- ev.ask - wsmApi <- buildWsmControlledResourceApiClient - _ <- logger.info(ctx.loggingCtx)( - s"Deleting WSM resource ${wsmResource.resourceId.value} for app ${app.appName.value} in workspace ${workspaceId.value}" - ) - - jobId = UUID.randomUUID() - deleteResourceRequest = new DeleteControlledAzureResourceRequest().jobControl( - new JobControl().id(jobId.toString) - ) - _ <- wsmResource.resourceType match { - case WsmResourceType.AzureManagedIdentity => - F.blocking(wsmApi.deleteAzureManagedIdentity(workspaceId.value, wsmResource.resourceId.value)) - case WsmResourceType.AzureDatabase => - deleteAndPollWsmDatabaseResource(workspaceId, app, wsmResource, jobId, deleteResourceRequest, wsmApi) - case WsmResourceType.AzureKubernetesNamespace => - deleteAndPollWsmNamespaceResource(workspaceId, app, wsmResource, jobId, deleteResourceRequest, wsmApi) - case _ => - F.raiseError(AppDeletionException(s"Unexpected WSM resource type ${wsmResource.resourceType}")) - } - // Update record in APP_CONTROLLED_RESOURCE table - _ <- appControlledResourceQuery.delete(wsmResource.resourceId).transaction - } yield () - - delete.handleErrorWith { - case e: ApiException if e.getCode == StatusCodes.NotFound.intValue => - // If the resource doesn't exist, that's fine. We're deleting it anyway. - for { - _ <- logger.info(s"No-op for delete WSM app resource ${wsmResource.resourceId.value}") - _ <- appControlledResourceQuery.delete(wsmResource.resourceId).transaction - } yield () - case e => F.raiseError(e) - } - } - - // This should probably be moved to WsmDao, if HttpWsmDao switches to using the WSM api clients - private def getWorkspaceResourceByName( - workspaceId: UUID, - resourceName: String, - resourceApi: ResourceApi - ): Option[ResourceDescription] = - Try(resourceApi.getResourceByName(workspaceId, resourceName)) - .map(Option.apply) - .handleError { - case e: ApiException if e.getCode == StatusCodes.NotFound.intValue => None - } - .get - - private[util] def updateListener(authContext: AuthContext, - app: App, - landingZoneResources: LandingZoneResources, - workspaceId: WorkspaceId, - hcName: RelayHybridConnectionName, - primaryKey: PrimaryKey, - relayDomain: String, - listenerChartConfig: ListenerChartConfig - )(implicit ev: Ask[F, AppContext]): F[Unit] = - // Update the Relay Listener if the app tracks it as a service. - // We're not tracking the listener version in the DB so we can't really pick and choose which versions to update. - // We started tracking it as a service when we switched the chart over to terra-helmfile. - if (app.appResources.services.exists(s => s.config.name == listenerChartConfig.service.config.name)) { - val values = BuildHelmChartValues.buildListenerChartOverrideValuesString( - app.release, - app.samResourceId, - landingZoneResources.relayNamespace, - hcName, - primaryKey, - app.appType, - workspaceId, - app.appName, - refererConfig.validHosts + relayDomain, - config.samConfig, - config.listenerImage, - config.leoUrlBase - ) - for { - ctx <- ev.ask - _ <- logger.info(ctx.loggingCtx)( - s"Listener values for app ${app.appName.value} are ${values.asString}" - ) - _ <- helmClient - .upgradeChart( - getListenerReleaseName(app.release), - config.listenerChartConfig.chartName, - config.listenerChartConfig.chartVersion, - values - ) - .run(authContext) - } yield () - } else { - ev.ask.flatMap(ctx => logger.warn(ctx.loggingCtx)(s"Not updating relay listener for app ${app.appName.value}")) - } - - private def buildWsmControlledResourceApiClient(implicit ev: Ask[F, AppContext]): F[ControlledAzureResourceApi] = - for { - token <- authProvider.getLeoAuthToken - wsmApi <- wsmClientProvider.getControlledAzureResourceApi(token) - } yield wsmApi - - private def buildWsmResourceApiClient(implicit ev: Ask[F, AppContext]): F[ResourceApi] = - for { - token <- authProvider.getLeoAuthToken - wsmApi <- wsmClientProvider.getResourceApi(token) - } yield wsmApi - - private def buildWsmWorkspaceApiClient(implicit ev: Ask[F, AppContext]): F[WorkspaceApi] = - for { - token <- authProvider.getLeoAuthToken - wsmApi <- wsmClientProvider.getWorkspaceApi(token) - } yield wsmApi -} - -final case class AKSInterpreterConfig( - samConfig: SamConfig, - appMonitorConfig: AppMonitorConfig, - wsmConfig: HttpWsmDaoConfig, - leoUrlBase: URL, - listenerImage: String, - listenerChartConfig: ListenerChartConfig -) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala index aebea6ba9b..174fd578cd 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala @@ -3,25 +3,7 @@ package leonardo package util import bio.terra.workspace.api.ControlledAzureResourceApi -import bio.terra.workspace.model.{ - AzureDiskCreationParameters, - AzureStorageContainerCreationParameters, - AzureVmCreationParameters, - AzureVmCustomScriptExtension, - AzureVmCustomScriptExtensionSetting, - AzureVmUser, - AzureVmUserAssignedIdentities, - CloningInstructionsEnum, - ControlledResourceCommonFields, - CreateControlledAzureDiskRequestV2Body, - CreateControlledAzureResourceResult, - CreateControlledAzureStorageContainerRequestBody, - CreateControlledAzureVmRequestBody, - CreatedControlledAzureVmResult, - DeleteControlledAzureResourceResult, - JobControl, - JobReport -} +import bio.terra.workspace.model._ import cats.Parallel import cats.effect.Async import cats.effect.std.Queue @@ -29,28 +11,18 @@ import cats.mtl.Ask import cats.syntax.all._ import com.azure.resourcemanager.compute.models.{PowerState, VirtualMachine, VirtualMachineSizeTypes} import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.google2.{streamFUntilDone, streamUntilDoneOrTimeout, RegionName} +import org.broadinstitute.dsde.workbench.google2.{RegionName, streamFUntilDone, streamUntilDoneOrTimeout} import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor.{Task, TaskMetricsTags} import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.PrivateAzureStorageAccountSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.config.{ - ApplicationConfig, - AzureEnvironmentConverter, - ContentSecurityPolicyConfig, - RefererConfig -} +import org.broadinstitute.dsde.workbench.leonardo.config.{ApplicationConfig, AzureEnvironmentConverter, ContentSecurityPolicyConfig, RefererConfig} import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http.{ctxConversion, dbioToIO, ConfigReader} -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAzureRuntimeMessage, - DeleteAzureRuntimeMessage, - DeleteDiskV2Message -} +import org.broadinstitute.dsde.workbench.leonardo.http.{ConfigReader, ctxConversion, dbioToIO} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAzureRuntimeMessage, DeleteAzureRuntimeMessage, DeleteDiskV2Message} import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError._ import org.broadinstitute.dsde.workbench.model.{IP, WorkbenchEmail} import org.broadinstitute.dsde.workbench.util2.InstanceName -import org.broadinstitute.dsp.ChartVersion import org.typelevel.log4cats.StructuredLogger import reactor.core.publisher.Mono @@ -70,7 +42,6 @@ class AzurePubsubHandlerInterp[F[_]: Parallel]( jupyterDAO: JupyterDAO[F], azureRelay: AzureRelayService[F], azureVmServiceInterp: AzureVmService[F], - aksAlgebra: AKSAlgebra[F], refererConfig: RefererConfig, wsmClientProvider: WsmApiClientProvider[F] )(implicit val executionContext: ExecutionContext, dbRef: DbReference[F], logger: StructuredLogger[F], F: Async[F]) @@ -1209,77 +1180,6 @@ class AzurePubsubHandlerInterp[F[_]: Parallel]( } } yield () - override def createAndPollApp(appId: AppId, - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: AzureCloudContext, - billingProfileId: BillingProfileId - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - params = CreateAKSAppParams(appId, appName, workspaceId, cloudContext, billingProfileId) - _ <- aksAlgebra.createAndPollApp(params).adaptError { case e => - PubsubKubernetesError( - AppError( - s"Error creating Azure app with id ${appId.id} and cloudContext ${cloudContext.asString}: ${e.getMessage}", - ctx.now, - ErrorAction.CreateApp, - ErrorSource.App, - None, - Some(ctx.traceId) - ), - Some(appId), - isRetryable = false, - None, - None, - None - ) - } - } yield () - - override def updateAndPollApp(appId: AppId, - appName: AppName, - appChartVersion: ChartVersion, - workspaceId: Option[WorkspaceId], - cloudContext: AzureCloudContext - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - aksAlgebra.updateAndPollApp(UpdateAKSAppParams(appId, appName, appChartVersion, workspaceId, cloudContext)) - - override def deleteApp( - appId: AppId, - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: AzureCloudContext, - billingProfileId: BillingProfileId - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - params = DeleteAKSAppParams(appName, workspaceId, cloudContext, billingProfileId) - _ <- aksAlgebra.deleteApp(params).adaptError { case e => - PubsubKubernetesError( - AppError( - s"Error deleting Azure app with id ${appId.id} and cloudContext ${cloudContext.asString}: ${e.getMessage}", - ctx.now, - ErrorAction.DeleteApp, - ErrorSource.App, - None, - Some(ctx.traceId) - ), - Some(appId), - isRetryable = false, - None, - None, - None - ) - } - } yield () - private def deleteDiskInWSM(diskId: DiskId, wsmResourceId: WsmControlledResourceId, workspaceId: WorkspaceId, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala index f3181eb8b0..239eac3924 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala @@ -9,7 +9,6 @@ import org.broadinstitute.dsde.workbench.leonardo.dao.{CreateDiskForRuntimeResul import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAzureRuntimeMessage, DeleteAzureRuntimeMessage, DeleteDiskV2Message} import org.broadinstitute.dsde.workbench.leonardo.monitor.PollMonitorConfig import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError.{AzureRuntimeCreationError, AzureRuntimeDeletionError, AzureRuntimeStartingError, AzureRuntimeStoppingError} -import org.broadinstitute.dsp.ChartVersion import org.http4s.Uri import java.security.SecureRandom @@ -34,33 +33,6 @@ trait AzurePubsubHandlerAlgebra[F[_]] { def deleteDisk(msg: DeleteDiskV2Message)(implicit ev: Ask[F, AppContext]): F[Unit] - def createAndPollApp(appId: AppId, - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: AzureCloudContext, - billingProfileId: BillingProfileId - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - def updateAndPollApp(appId: AppId, - appName: AppName, - appChartVersion: ChartVersion, - workspaceId: Option[WorkspaceId], - cloudContext: AzureCloudContext - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - def deleteApp(appId: AppId, - appName: AppName, - workspaceId: WorkspaceId, - cloudContext: AzureCloudContext, - billingProfileId: BillingProfileId - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] - def handleAzureRuntimeStartError(e: AzureRuntimeStartingError, now: Instant)(implicit ev: Ask[F, AppContext] ): F[Unit] diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/BaseAppInstallSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/BaseAppInstallSpec.scala deleted file mode 100644 index 17c80b23c2..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/BaseAppInstallSpec.scala +++ /dev/null @@ -1,183 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import bio.terra.profile.model.{Organization, ProfileModel} -import cats.effect.IO -import com.azure.resourcemanager.applicationinsights.models.ApplicationInsightsComponent -import com.azure.resourcemanager.batch.models.{BatchAccount, BatchAccountKeys} -import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceAccountName -import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{azureRegion, billingProfileId, tokenValue} -import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.makeApp -import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider -import org.broadinstitute.dsde.workbench.leonardo.config.Config.appMonitorConfig -import org.broadinstitute.dsde.workbench.leonardo.config.SamConfig -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader -import org.broadinstitute.dsde.workbench.leonardo.util.AKSInterpreterConfig -import org.broadinstitute.dsde.workbench.leonardo.{ - AKSCluster, - LandingZoneResources, - LeonardoTestSuite, - ManagedIdentityName, - NodepoolLeoId, - PostgresServer, - StorageAccountName, - WorkspaceId, - WsmControlledDatabaseResource, - WsmControlledResourceId -} -import org.broadinstitute.dsp.Release -import org.http4s.Uri -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.when -import org.scalatest.flatspec.AnyFlatSpecLike -import org.scalatestplus.mockito.MockitoSugar - -import scala.jdk.CollectionConverters._ -import java.net.URL -import java.util.UUID - -class BaseAppInstallSpec extends AnyFlatSpecLike with LeonardoTestSuite with MockitoSugar { - - val mockSamDAO = setUpMockSamDAO - val mockCromwellDAO = setUpMockCromwellDAO - val mockCbasDAO = setUpMockCbasDAO - val mockAzureApplicationInsightsService = setUpMockAzureApplicationInsightsService - val mockAzureBatchService = setUpMockAzureBatchService - val mockBpmClientProvider = setUpMockBpmProvider - val mockSamAuthProvider = setUpMockSamAuthProvider - - val cloudContext = AzureCloudContext( - TenantId("tenant"), - SubscriptionId("sub"), - ManagedResourceGroupName("mrg") - ) - - val lzResources = LandingZoneResources( - UUID.fromString("5c12f64b-f4ac-4be1-ae4a-4cace5de807d"), - AKSCluster("cluster", Map.empty[String, Boolean]), - BatchAccountName("batch"), - RelayNamespace("relay"), - StorageAccountName("storage"), - NetworkName("network"), - SubnetworkName("subnet1"), - SubnetworkName("subnet2"), - azureRegion, - ApplicationInsightsName("lzappinsights"), - Some(PostgresServer("postgres", true)) - ) - - val storageContainer = StorageContainerResponse( - ContainerName("sc-container"), - WsmControlledResourceId(UUID.randomUUID) - ) - - val app = makeApp(1, NodepoolLeoId(-1)).copy( - release = Release("rel-1") - ) - - val workspaceId = WorkspaceId(UUID.randomUUID) - val workspaceCreatedDate = java.time.OffsetDateTime.parse("1970-01-01T12:15:30-07:00") - - val aksInterpConfig = AKSInterpreterConfig( - SamConfig("https://sam.dsde-dev.broadinstitute.org/"), - appMonitorConfig, - ConfigReader.appConfig.azure.wsm, - new URL("https://leo-dummy-url.org"), - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.listenerImage, - ConfigReader.appConfig.azure.listenerChartConfig - ) - - def buildHelmOverrideValuesParams(databases: List[WsmControlledDatabaseResource]): BuildHelmOverrideValuesParams = - BuildHelmOverrideValuesParams( - app, - workspaceId, - cloudContext, - billingProfileId, - lzResources, - Some(storageContainer), - Uri.unsafeFromString("https://relay.com/app"), - ServiceAccountName("ksa-1"), - ManagedIdentityName("mi-1"), - databases, - aksInterpConfig - ) - - private def setUpMockSamDAO: SamDAO[IO] = { - val sam = mock[SamDAO[IO]] - when { - sam.getCachedArbitraryPetAccessToken(any)(any) - } thenReturn IO.pure(Some("accessToken")) - sam - } - - private def setUpMockCromwellDAO: CromwellDAO[IO] = { - val cromwell = mock[CromwellDAO[IO]] - when { - cromwell.getStatus(any, any)(any) - } thenReturn IO.pure(true) - cromwell - } - - private def setUpMockCbasDAO: CbasDAO[IO] = { - val cbas = mock[CbasDAO[IO]] - when { - cbas.getStatus(any, any)(any) - } thenReturn IO.pure(true) - cbas - } - - private def setUpMockBpmProvider: BpmApiClientProvider[IO] = { - val bpm = mock[BpmApiClientProvider[IO]] - when { - bpm.getProfile(any, any)(any) - } thenReturn IO.pure( - Some( - new ProfileModel() - .id(UUID.randomUUID()) - .organization(new Organization().limits(Map("concurrentjoblimit" -> "100").asJava)) - ) - ) - bpm - } - - private def setUpMockAzureApplicationInsightsService: AzureApplicationInsightsService[IO] = { - val service = mock[AzureApplicationInsightsService[IO]] - val applicationInsightsComponent = mock[ApplicationInsightsComponent] - when { - service.getApplicationInsights(any[String].asInstanceOf[ApplicationInsightsName], any)(any) - } thenReturn IO.pure(applicationInsightsComponent) - when { - applicationInsightsComponent.connectionString() - } thenReturn "applicationInsightsConnectionString" - service - } - - private def setUpMockAzureBatchService: AzureBatchService[IO] = { - val service = mock[AzureBatchService[IO]] - val batchAccountKeys = mock[BatchAccountKeys] - val batchAccount = mock[BatchAccount] - when { - service.getBatchAccount(any[String].asInstanceOf[BatchAccountName], any[String].asInstanceOf[AzureCloudContext])( - any - ) - } thenReturn IO.pure(batchAccount) - when { - batchAccount.getKeys() - } thenReturn batchAccountKeys - when { - batchAccountKeys.primary() - } thenReturn "batchKey" - service - } - - private def setUpMockSamAuthProvider: SamAuthProvider[IO] = { - val mockSamAuth = mock[SamAuthProvider[IO]] - - when { - mockSamAuth.getLeoAuthToken - } thenReturn IO.pure(tokenValue) - mockSamAuth - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstallSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstallSpec.scala deleted file mode 100644 index 12be250fe1..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellAppInstallSpec.scala +++ /dev/null @@ -1,183 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import cats.effect.IO -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceAccountName -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{ - azureRegion, - billingProfileId, - landingZoneResources, - petUserInfo -} -import org.broadinstitute.dsde.workbench.leonardo.{ManagedIdentityName, PostgresServer, WsmControlledDatabaseResource} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.AzureEnvironmentConverter -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.http4s.Uri - -class CromwellAppInstallSpec extends BaseAppInstallSpec { - - val cromwellAppInstall = new CromwellAppInstall[IO]( - ConfigReader.appConfig.azure.coaAppConfig, - ConfigReader.appConfig.drs, - mockSamDAO, - mockCromwellDAO, - mockCbasDAO, - mockAzureBatchService, - mockAzureApplicationInsightsService, - mockSamAuthProvider - ) - - val cromwellAzureDbName = "cromwell_tghfgi" - val cbasAzureDbName = "cbas_edfgvb" - val tesAzureDbName = "tes_pasgjf" - val cromwellOnAzureDatabases: List[WsmControlledDatabaseResource] = List( - WsmControlledDatabaseResource("cromwell", cromwellAzureDbName), - WsmControlledDatabaseResource("cbas", cbasAzureDbName), - WsmControlledDatabaseResource("tes", tesAzureDbName) - ) - - it should "build coa override values" in { - val params = buildHelmOverrideValuesParams(cromwellOnAzureDatabases) - val overrides = cromwellAppInstall.buildHelmOverrideValues(params) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe - "config.resourceGroup=mrg," + - "config.batchAccountKey=batchKey," + - "config.batchAccountName=batch," + - "config.batchNodesSubnetId=subnet1," + - s"config.drsUrl=${ConfigReader.appConfig.drs.url}," + - "config.landingZoneId=5c12f64b-f4ac-4be1-ae4a-4cace5de807d," + - "config.subscriptionId=sub," + - s"config.region=${azureRegion}," + - "config.applicationInsightsConnectionString=applicationInsightsConnectionString," + - s"config.azureEnvironment=${ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment}," + - s"config.azureManagementTokenScope=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getResourceManagerEndpoint}.default," + - s"config.batchAccountSuffix=${AzureEnvironmentConverter - .batchAccountSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "relay.path=https://relay.com/app," + - "persistence.storageResourceGroup=mrg," + - "persistence.storageAccount=storage," + - s"persistence.storageAccountSuffix=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix}," + - "persistence.blobContainer=sc-container," + - "persistence.leoAppInstanceName=app1," + - s"persistence.workspaceManager.url=${ConfigReader.appConfig.azure.wsm.uri.renderString}," + - s"persistence.workspaceManager.workspaceId=${workspaceId.value}," + - s"persistence.workspaceManager.containerResourceId=${storageContainer.resourceId.value.toString}," + - "identity.enabled=false," + - "workloadIdentity.enabled=true," + - "workloadIdentity.serviceAccountName=ksa-1," + - "identity.name=mi-1," + - "sam.url=https://sam.dsde-dev.broadinstitute.org/," + - "leonardo.url=https://leo-dummy-url.org," + - "cbas.enabled=true," + - "cromwell.enabled=true," + - "dockstore.baseUrl=https://staging.dockstore.org/," + - "fullnameOverride=coa-rel-1," + - "instrumentationEnabled=false," + - s"provenance.userAccessToken=${petUserInfo.accessToken.token}," + - "postgres.podLocalDatabaseEnabled=false," + - s"postgres.host=${lzResources.postgresServer.map(_.name).get}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "postgres.pgbouncer.enabled=true," + - "postgres.user=ksa-1," + - s"postgres.dbnames.cromwell=$cromwellAzureDbName," + - s"postgres.dbnames.cbas=$cbasAzureDbName," + - s"postgres.dbnames.tes=$tesAzureDbName" - } - - it should "build coa override values when pgbouncer is not enabled" in { - val params = BuildHelmOverrideValuesParams( - app, - workspaceId, - cloudContext, - billingProfileId, - lzResources.copy(postgresServer = Some(PostgresServer("postgres", false))), - Some(storageContainer), - Uri.unsafeFromString("https://relay.com/app"), - ServiceAccountName("ksa-1"), - ManagedIdentityName("mi-1"), - cromwellOnAzureDatabases, - aksInterpConfig - ) - - val overrides = cromwellAppInstall.buildHelmOverrideValues(params) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe - "config.resourceGroup=mrg," + - "config.batchAccountKey=batchKey," + - "config.batchAccountName=batch," + - "config.batchNodesSubnetId=subnet1," + - s"config.drsUrl=${ConfigReader.appConfig.drs.url}," + - "config.landingZoneId=5c12f64b-f4ac-4be1-ae4a-4cace5de807d," + - "config.subscriptionId=sub," + - s"config.region=${azureRegion}," + - "config.applicationInsightsConnectionString=applicationInsightsConnectionString," + - s"config.azureEnvironment=${ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment}," + - s"config.azureManagementTokenScope=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getResourceManagerEndpoint}.default," + - s"config.batchAccountSuffix=${AzureEnvironmentConverter - .batchAccountSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "relay.path=https://relay.com/app," + - "persistence.storageResourceGroup=mrg," + - "persistence.storageAccount=storage," + - s"persistence.storageAccountSuffix=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix}," + - "persistence.blobContainer=sc-container," + - "persistence.leoAppInstanceName=app1," + - s"persistence.workspaceManager.url=${ConfigReader.appConfig.azure.wsm.uri.renderString}," + - s"persistence.workspaceManager.workspaceId=${workspaceId.value}," + - s"persistence.workspaceManager.containerResourceId=${storageContainer.resourceId.value.toString}," + - "identity.enabled=false," + - "workloadIdentity.enabled=true," + - "workloadIdentity.serviceAccountName=ksa-1," + - "identity.name=mi-1," + - "sam.url=https://sam.dsde-dev.broadinstitute.org/," + - "leonardo.url=https://leo-dummy-url.org," + - "cbas.enabled=true," + - "cromwell.enabled=true," + - "dockstore.baseUrl=https://staging.dockstore.org/," + - "fullnameOverride=coa-rel-1," + - "instrumentationEnabled=false," + - s"provenance.userAccessToken=${petUserInfo.accessToken.token}," + - "postgres.podLocalDatabaseEnabled=false," + - s"postgres.host=${lzResources.postgresServer.map(_.name).get}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "postgres.pgbouncer.enabled=false," + - "postgres.user=ksa-1," + - s"postgres.dbnames.cromwell=$cromwellAzureDbName," + - s"postgres.dbnames.cbas=$cbasAzureDbName," + - s"postgres.dbnames.tes=$tesAzureDbName" - } - - it should "fail if there is no storage container" in { - val params = buildHelmOverrideValuesParams(cromwellOnAzureDatabases).copy(storageContainer = None) - val overrides = cromwellAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail if there is no postgres server" in { - val params = buildHelmOverrideValuesParams(cromwellOnAzureDatabases) - .copy(landingZoneResources = landingZoneResources.copy(postgresServer = None)) - val overrides = cromwellAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail if there are no databases" in { - val params = buildHelmOverrideValuesParams(List.empty) - val overrides = cromwellAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstallSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstallSpec.scala deleted file mode 100644 index 59df8bffd1..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/CromwellRunnerAppInstallSpec.scala +++ /dev/null @@ -1,137 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import cats.effect.IO -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{azureRegion, landingZoneResources, petUserInfo} -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.AzureEnvironmentConverter -import org.broadinstitute.dsde.workbench.leonardo.{BillingProfileId, WsmControlledDatabaseResource} -import org.broadinstitute.dsde.workbench.leonardo.config.Config.samConfig -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException - -import java.util.UUID - -class CromwellRunnerAppInstallSpec extends BaseAppInstallSpec { - - val cromwellRunnerAppInstall = new CromwellRunnerAppInstall[IO]( - ConfigReader.appConfig.azure.cromwellRunnerAppConfig, - ConfigReader.appConfig.drs, - samConfig, - mockSamDAO, - mockCromwellDAO, - mockAzureBatchService, - mockAzureApplicationInsightsService, - mockBpmClientProvider, - mockSamAuthProvider - ) - - val cromwellAzureDbName = "cromwell_wgsdoi" - val tesAzureDbName = "tes_oiwjnz" - val cromwellMetadataAzureDbName = "cromwellmetadata_tyuiwk" - val cromwellRunnerAzureDatabases: List[WsmControlledDatabaseResource] = List( - WsmControlledDatabaseResource("cromwell", cromwellAzureDbName), - WsmControlledDatabaseResource("tes", tesAzureDbName), - WsmControlledDatabaseResource("cromwellmetadata", cromwellMetadataAzureDbName) - ) - - val expectedOverrides = "config.resourceGroup=mrg," + - "config.batchAccountKey=batchKey," + - "config.batchAccountName=batch," + - "config.batchNodesSubnetId=subnet1," + - s"config.drsUrl=${ConfigReader.appConfig.drs.url}," + - "config.landingZoneId=5c12f64b-f4ac-4be1-ae4a-4cace5de807d," + - "config.subscriptionId=sub," + - s"config.region=${azureRegion}," + - "config.applicationInsightsConnectionString=applicationInsightsConnectionString," + - s"config.azureEnvironment=${ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment}," + - s"config.azureManagementTokenScope=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getResourceManagerEndpoint}.default," + - s"config.batchAccountSuffix=${AzureEnvironmentConverter - .batchAccountSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "relay.path=https://relay.com/app," + - "persistence.storageAccount=storage," + - s"persistence.storageAccountSuffix=${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix}," + - "persistence.blobContainer=sc-container," + - "persistence.leoAppInstanceName=app1," + - s"persistence.workspaceManager.url=${ConfigReader.appConfig.azure.wsm.uri.renderString}," + - s"persistence.workspaceManager.workspaceId=${workspaceId.value}," + - s"persistence.workspaceManager.containerResourceId=${storageContainer.resourceId.value.toString}," + - "workloadIdentity.serviceAccountName=ksa-1," + - "identity.name=mi-1," + - "cromwell.enabled=true," + - "fullnameOverride=cra-rel-1," + - "instrumentationEnabled=false," + - s"provenance.userAccessToken=${petUserInfo.accessToken.token}," + - "postgres.podLocalDatabaseEnabled=false," + - s"postgres.host=${lzResources.postgresServer.map(_.name).get}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "postgres.pgbouncer.enabled=true," + - "postgres.user=ksa-1," + - s"postgres.dbnames.cromwell=$cromwellAzureDbName," + - s"postgres.dbnames.tes=$tesAzureDbName," + - s"postgres.dbnames.cromwellMetadata=$cromwellMetadataAzureDbName," + - s"ecm.baseUri=https://externalcreds.dsde-dev.broadinstitute.org," + - s"sam.baseUri=https://sam.test.org:443," + - s"sam.acrPullActionIdentityResourceId=spend-profile," + - "bard.bardUrl=https://terra-bard-dev.appspot.com," + - "bard.enabled=false" - - it should "build cromwell-runner override values" in { - val params = buildHelmOverrideValuesParams(cromwellRunnerAzureDatabases) - - val overrides = cromwellRunnerAppInstall.buildHelmOverrideValues(params) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe expectedOverrides - } - - it should "build cromwell-runner override values for a limited bp" in { - val params = buildHelmOverrideValuesParams(cromwellRunnerAzureDatabases) - val bpid = UUID.randomUUID().toString - val updatedParams = params.copy(billingProfileId = BillingProfileId(bpid)) - - val overrides = cromwellRunnerAppInstall.buildHelmOverrideValues(updatedParams) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe expectedOverrides.replace( - "spend-profile", - bpid - ) + s",config.concurrentJobLimit=100" - } - - it should "fail if there is no storage container" in { - val params = buildHelmOverrideValuesParams(cromwellRunnerAzureDatabases).copy(storageContainer = None) - val overrides = cromwellRunnerAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail if there is no postgres server" in { - val params = buildHelmOverrideValuesParams(cromwellRunnerAzureDatabases) - .copy(landingZoneResources = landingZoneResources.copy(postgresServer = None)) - val overrides = cromwellRunnerAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail if there are no databases" in { - val params = buildHelmOverrideValuesParams(List.empty) - val overrides = cromwellRunnerAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "find the first instance of each database type" in { - cromwellRunnerAppInstall.toCromwellRunnerAppDatabaseNames( - List( - WsmControlledDatabaseResource("cromwellmetadata", cromwellMetadataAzureDbName), - WsmControlledDatabaseResource("cromwell", cromwellAzureDbName), - WsmControlledDatabaseResource("tes", tesAzureDbName) - ) // put cromwellmetadata first to ensure it doesn't get confused with cromwell - ) should be(Some(CromwellRunnerAppDatabaseNames(cromwellAzureDbName, tesAzureDbName, cromwellMetadataAzureDbName))) - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstallSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstallSpec.scala deleted file mode 100644 index e0b0dd34c1..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/HailBatchAppInstallSpec.scala +++ /dev/null @@ -1,49 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import cats.effect.IO -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.AzureEnvironmentConverter -import org.broadinstitute.dsde.workbench.leonardo.dao.HailBatchDAO -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.when - -class HailBatchAppInstallSpec extends BaseAppInstallSpec { - - val mockHailBatchDAO = setUpMockHailBatchDAO - val hailBatchAppInstall = new HailBatchAppInstall[IO]( - ConfigReader.appConfig.azure.hailBatchAppConfig, - mockHailBatchDAO - ) - - it should "build hail batch override values" in { - val params = buildHelmOverrideValuesParams(List.empty) - - val overrides = hailBatchAppInstall.buildHelmOverrideValues(params) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe - "persistence.storageAccount=storage," + - "persistence.blobContainer=sc-container," + - s"persistence.workspaceManager.url=${ConfigReader.appConfig.azure.wsm.uri.renderString}," + - s"persistence.workspaceManager.workspaceId=${workspaceId.value}," + - s"persistence.workspaceManager.containerResourceId=${storageContainer.resourceId.value.toString}," + - s"persistence.workspaceManager.storageContainerUrl=https://${lzResources.storageAccountName.value}.blob${AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix}/${storageContainer.name.value}," + - "persistence.leoAppName=app1," + - "workloadIdentity.serviceAccountName=ksa-1," + - s"relay.domain=relay.com," + - "relay.subpath=/app" - } - - private def setUpMockHailBatchDAO: HailBatchDAO[IO] = { - val batch = mock[HailBatchDAO[IO]] - when { - batch.getStatus(any, any)(any) - } thenReturn IO.pure(true) - when { - batch.getDriverStatus(any, any)(any) - } thenReturn IO.pure(true) - batch - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstallSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstallSpec.scala deleted file mode 100644 index 990995c8ac..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WdsAppInstallSpec.scala +++ /dev/null @@ -1,118 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import cats.effect.IO -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{azureRegion, landingZoneResources, petUserInfo} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.AzureEnvironmentConverter -import org.broadinstitute.dsde.workbench.leonardo.{WorkspaceId, WsmControlledDatabaseResource} -import org.broadinstitute.dsde.workbench.leonardo.dao.WdsDAO -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.when - -import java.util.UUID - -class WdsAppInstallSpec extends BaseAppInstallSpec { - val mockWdsDAO = setUpMockWdsDAO - - val wdsAppInstall = new WdsAppInstall[IO]( - ConfigReader.appConfig.azure.wdsAppConfig, - ConfigReader.appConfig.azure.tdr, - mockSamDAO, - mockWdsDAO, - mockAzureApplicationInsightsService, - mockSamAuthProvider - ) - - val wdsAzureDbName = "wds_rtyjga" - val wdsAzureDatabases: List[WsmControlledDatabaseResource] = List( - WsmControlledDatabaseResource("wds", wdsAzureDbName) - ) - - it should "build wds override values" in { - val params = buildHelmOverrideValuesParams(wdsAzureDatabases) - - val overrides = wdsAppInstall.buildHelmOverrideValues(params) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe - "wds.environment=dev," + - "wds.environmentBase=live," + - "config.resourceGroup=mrg," + - "config.applicationInsightsConnectionString=applicationInsightsConnectionString," + - "config.aks.vpaEnabled=false," + - "config.subscriptionId=sub," + - s"config.region=${azureRegion}," + - "general.leoAppInstanceName=app1," + - s"general.workspaceManager.workspaceId=${workspaceId.value}," + - "identity.enabled=false," + - "workloadIdentity.enabled=true," + - "workloadIdentity.serviceAccountName=ksa-1," + - "fullnameOverride=wds-rel-1," + - "instrumentationEnabled=false," + - s"provenance.userAccessToken=${petUserInfo.accessToken.token}," + - "provenance.sourceWorkspaceId=," + - s"postgres.host=${lzResources.postgresServer.map(_.name).get}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "postgres.pgbouncer.enabled=true," + - s"postgres.dbname=$wdsAzureDbName," + - "postgres.user=ksa-1" - } - - it should "build wds override values with a sourceWorkspaceId" in { - val sourceWorkspaceId = WorkspaceId(UUID.randomUUID()) - val params = buildHelmOverrideValuesParams(wdsAzureDatabases).copy( - app = app.copy(sourceWorkspaceId = Some(sourceWorkspaceId)) - ) - - val overrides = wdsAppInstall.buildHelmOverrideValues(params) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe - "wds.environment=dev," + - "wds.environmentBase=live," + - "config.resourceGroup=mrg," + - "config.applicationInsightsConnectionString=applicationInsightsConnectionString," + - "config.aks.vpaEnabled=false," + - "config.subscriptionId=sub," + - s"config.region=${azureRegion}," + - "general.leoAppInstanceName=app1," + - s"general.workspaceManager.workspaceId=${workspaceId.value}," + - "identity.enabled=false," + - "workloadIdentity.enabled=true," + - "workloadIdentity.serviceAccountName=ksa-1," + - "fullnameOverride=wds-rel-1," + - "instrumentationEnabled=false," + - s"provenance.userAccessToken=${petUserInfo.accessToken.token}," + - s"provenance.sourceWorkspaceId=${sourceWorkspaceId.value}," + - s"postgres.host=${lzResources.postgresServer.map(_.name).get}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "postgres.pgbouncer.enabled=true," + - s"postgres.dbname=$wdsAzureDbName," + - "postgres.user=ksa-1" - } - - it should "fail if there is no postgres server" in { - val params = buildHelmOverrideValuesParams(wdsAzureDatabases) - .copy(landingZoneResources = landingZoneResources.copy(postgresServer = None)) - val overrides = wdsAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail if there are no databases" in { - val params = buildHelmOverrideValuesParams(List.empty) - val overrides = wdsAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - private def setUpMockWdsDAO: WdsDAO[IO] = { - val wds = mock[WdsDAO[IO]] - when { - wds.getStatus(any, any)(any) - } thenReturn IO.pure(true) - wds - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstallSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstallSpec.scala deleted file mode 100644 index 20cc7a1c46..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/app/WorkflowsAppInstallSpec.scala +++ /dev/null @@ -1,87 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.app - -import cats.effect.IO -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{landingZoneResources, petUserInfo} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.WsmControlledDatabaseResource -import org.broadinstitute.dsde.workbench.leonardo.config.AzureEnvironmentConverter -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException - -class WorkflowsAppInstallSpec extends BaseAppInstallSpec { - - val workflowsAppInstall = new WorkflowsAppInstall[IO]( - ConfigReader.appConfig.azure.workflowsAppConfig, - ConfigReader.appConfig.drs, - mockSamDAO, - mockCromwellDAO, - mockCbasDAO, - mockAzureBatchService, - mockAzureApplicationInsightsService - ) - - val cbasAzureDbName = "cbas_wgsdoi" - val cromwellMetadataAzureDbName = "cromwellmetadata_tyuiwk" - val workflowsAzureDatabases: List[WsmControlledDatabaseResource] = List( - WsmControlledDatabaseResource("cbas", cbasAzureDbName), - WsmControlledDatabaseResource("cromwellmetadata", cromwellMetadataAzureDbName) - ) - - it should "build workflows app override values" in { - val params = buildHelmOverrideValuesParams(workflowsAzureDatabases) - - val overrides = workflowsAppInstall.buildHelmOverrideValues(params) - - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global).asString shouldBe - s"config.drsUrl=${ConfigReader.appConfig.drs.url}," + - "config.applicationInsightsConnectionString=applicationInsightsConnectionString," + - "relay.path=https://relay.com/app," + - "persistence.storageAccount=storage," + - "persistence.blobContainer=sc-container," + - "persistence.leoAppInstanceName=app1," + - s"persistence.workspaceManager.url=${ConfigReader.appConfig.azure.wsm.uri.renderString}," + - s"persistence.workspaceManager.workspaceId=${workspaceId.value}," + - "workloadIdentity.serviceAccountName=ksa-1," + - "sam.url=https://sam.dsde-dev.broadinstitute.org/," + - "leonardo.url=https://leo-dummy-url.org," + - "dockstore.baseUrl=https://staging.dockstore.org/," + - "fullnameOverride=wfa-rel-1," + - "instrumentationEnabled=false," + - s"provenance.userAccessToken=${petUserInfo.accessToken.token}," + - "postgres.podLocalDatabaseEnabled=false," + - s"postgres.host=${lzResources.postgresServer.map(_.name).get}.postgres${AzureEnvironmentConverter - .postgresSuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}," + - "postgres.pgbouncer.enabled=true," + - "postgres.user=ksa-1," + - s"postgres.dbnames.cromwellMetadata=$cromwellMetadataAzureDbName," + - s"postgres.dbnames.cbas=$cbasAzureDbName," + - s"ecm.baseUri=https://externalcreds.dsde-dev.broadinstitute.org," + - s"bard.baseUri=https://terra-bard-dev.appspot.com," + - s"bard.enabled=false" - } - - it should "fail if there is no storage container" in { - val params = buildHelmOverrideValuesParams(workflowsAzureDatabases).copy(storageContainer = None) - val overrides = workflowsAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail if there is no postgres server" in { - val params = buildHelmOverrideValuesParams(workflowsAzureDatabases) - .copy(landingZoneResources = landingZoneResources.copy(postgresServer = None)) - val overrides = workflowsAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } - - it should "fail if there are no databases" in { - val params = buildHelmOverrideValuesParams(List.empty) - val overrides = workflowsAppInstall.buildHelmOverrideValues(params) - assertThrows[AppCreationException] { - overrides.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/mocks.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/mocks.scala index 240176ccdc..ff734b7691 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/mocks.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/mocks.scala @@ -271,17 +271,6 @@ class MockGKEService extends GKEAlgebra[IO] { override def startAndPollApp(params: StartAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = IO.unit } -class MockAKSInterp extends AKSAlgebra[IO] { - - /** Creates an app and polls it for completion */ - override def createAndPollApp(params: CreateAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = IO.unit - - /** Updates an app and polls it for completion */ - override def updateAndPollApp(params: UpdateAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = IO.unit - - override def deleteApp(params: DeleteAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = IO.unit -} - class BaseMockSamService extends SamService[IO] { override def getPetServiceAccount(bearerToken: String, googleProject: GoogleProject)(implicit ev: Ask[IO, AppContext] diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala index 92786fab21..410f6e13c5 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala @@ -12,7 +12,6 @@ import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubCodec._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ CreateAppMessage, - CreateAppV2Message, CreateAzureRuntimeMessage, CreateRuntimeMessage } @@ -161,26 +160,4 @@ class LeoPubsubCodecSpec extends AnyFlatSpec with Matchers { res shouldBe Right(landingZoneResources) } - - it should "encode/decode CreateAppV2Message properly" in { - val originalMessage = - CreateAppV2Message( - AppId(1), - AppName("test"), - WorkspaceId(UUID.randomUUID()), - CloudContext.Azure( - AzureCloudContext( - TenantId("id"), - SubscriptionId("sub"), - ManagedResourceGroupName("rg-name") - ) - ), - BillingProfileId("spend-profile"), - None - ) - - val res = decode[CreateAppV2Message](originalMessage.asJson.printWith(Printer.noSpaces)) - - res shouldBe Right(originalMessage) - } } diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala index 54a032c040..13749072b8 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala @@ -23,26 +23,11 @@ import org.broadinstitute.dsde.workbench.google.mock._ import org.broadinstitute.dsde.workbench.google2.KubernetesModels.PodStatus import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceAccountName import org.broadinstitute.dsde.workbench.google2.mock.{MockKubernetesService => _, _} -import org.broadinstitute.dsde.workbench.google2.{ - DiskName, - GKEModels, - GoogleDiskService, - GoogleStorageService, - KubernetesModels, - MachineTypeName, - RegionName, - ZoneName -} +import org.broadinstitute.dsde.workbench.google2.{DiskName, GKEModels, GoogleDiskService, GoogleStorageService, KubernetesModels, MachineTypeName, RegionName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo.AppRestore.GalaxyRestore import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor.Task import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{ - makeApp, - makeAzureCluster, - makeKubeCluster, - makeNodepool, - makeService -} +import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{makeApp, makeKubeCluster, makeNodepool, makeService} import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.BootSource import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext import org.broadinstitute.dsde.workbench.leonardo.config.{ApplicationConfig, Config} @@ -52,7 +37,7 @@ import org.broadinstitute.dsde.workbench.leonardo.http._ import org.broadinstitute.dsde.workbench.leonardo.model.LeoAuthProvider import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage._ import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError.ClusterInvalidState -import org.broadinstitute.dsde.workbench.leonardo.util.{AzurePubsubHandlerInterp, _} +import org.broadinstitute.dsde.workbench.leonardo.util._ import org.broadinstitute.dsde.workbench.model.google.{GcsBucketName, GoogleProject} import org.broadinstitute.dsde.workbench.model.{IP, TraceId, WorkbenchEmail} import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics @@ -65,8 +50,8 @@ import org.scalatest.BeforeAndAfterEach import org.scalatest.concurrent._ import org.scalatest.flatspec.AnyFlatSpecLike import org.scalatest.matchers.should.Matchers -import org.scalatestplus.mockito.MockitoSugar import org.scalatest.prop.TableDrivenPropertyChecks._ +import org.scalatestplus.mockito.MockitoSugar import scalacache.caffeine.CaffeineCache import java.net.URL @@ -2193,315 +2178,6 @@ class LeoPubsubMessageSubscriberSpec res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "save an error and transition app to error when a fatal error occurs in azure updateAndPollApp" in isolatedDbTest { - val errors = Table( - "exception", - TestExceptionIndexTuple(HelmException("test helm exception"), 1), - TestExceptionIndexTuple(AppUpdatePollingException("test polling exception", None), 2) - ) - forAll(errors) { (tuple: TestExceptionIndexTuple) => - val exception = tuple.exception - val index = tuple.index - val queue = makeTaskQueue() - val mockAckConsumer = mock[AckHandler] - - val mockAksInterp = new MockAKSInterp { - override def updateAndPollApp(params: UpdateAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = - IO.raiseError(exception) - } - - val leoSubscriber = makeLeoSubscriber( - azureInterp = makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm, mockAksInterp = mockAksInterp), - asyncTaskQueue = queue - ) - - val savedCluster1 = makeAzureCluster(index).save() - val savedNodepool1 = makeNodepool(index, savedCluster1.id).save() - val savedApp1 = makeApp(index, savedNodepool1.id, appType = AppType.Cromwell).save() - val jobId = UpdateAppJobId(UUID.randomUUID()) - val msg = - UpdateAppMessage(jobId, - savedApp1.id, - savedApp1.appName, - savedCluster1.cloudContext, - savedApp1.workspaceId, - None, - None - ) - - val startTime = Instant.now() - - val res = - for { - _ <- updateAppLogQuery.save(jobId, savedApp1.id, startTime).transaction - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, None, instantTimestamp, mockAckConsumer)) - - assertions = for { - getAppOpt <- KubernetesServiceDbQueries - .getActiveFullAppByName(savedCluster1.cloudContext, savedApp1.appName) - .transaction - getApp = getAppOpt.get - - getUpdateLogOpt <- updateAppLogQuery.get(savedApp1.id, jobId).transaction - getUpdateLog = getUpdateLogOpt.get - - appErrorList <- appErrorQuery.get(getApp.app.id).transaction - } yield { - getApp.app.errors.size shouldBe 1 - getApp.app.errors.map(_.action) should contain(ErrorAction.UpdateApp) - getApp.app.errors.map(_.source) should contain(ErrorSource.App) - getApp.app.errors.head.errorMessage should include(exception.getMessage) - getApp.app.status shouldBe AppStatus.Error - - appErrorList.size shouldBe 1 - getUpdateLog.errorId.isDefined shouldBe true - getUpdateLog.status shouldBe UpdateAppJobStatus.Error - getUpdateLog.endTime.isDefined shouldBe true - getUpdateLog.startTime.toEpochMilli shouldBe startTime.toEpochMilli - // We cant verify before/after because tests don't use the same application ctx that the app does - getUpdateLog.endTime.get.toEpochMilli == getUpdateLog.startTime.toEpochMilli shouldBe false - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - verify(mockAckConsumer, times(1)).ack() - } - } - - it should "save an error and transition app to running when a non-fatal error occurs in azure updateAndPollApp" in isolatedDbTest { - val exception = new RuntimeException("random test exception") - val queue = makeTaskQueue() - val mockAckConsumer = mock[AckHandler] - - val mockAksInterp = new MockAKSInterp { - override def updateAndPollApp(params: UpdateAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = - IO.raiseError(exception) - } - - val leoSubscriber = makeLeoSubscriber( - azureInterp = makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm, mockAksInterp = mockAksInterp), - asyncTaskQueue = queue - ) - - val savedCluster1 = makeAzureCluster(1).save() - val savedNodepool1 = makeNodepool(1, savedCluster1.id).save() - val savedApp1 = makeApp(1, savedNodepool1.id, appType = AppType.Cromwell).save() - val jobId = UpdateAppJobId(UUID.randomUUID()) - val msg = - UpdateAppMessage(jobId, - savedApp1.id, - savedApp1.appName, - savedCluster1.cloudContext, - savedApp1.workspaceId, - None, - None - ) - - val startTime = Instant.now() - - val res = - for { - _ <- updateAppLogQuery.save(jobId, savedApp1.id, startTime).transaction - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, None, instantTimestamp, mockAckConsumer)) - - assertions = for { - getAppOpt <- KubernetesServiceDbQueries - .getActiveFullAppByName(savedCluster1.cloudContext, savedApp1.appName) - .transaction - getApp = getAppOpt.get - getUpdateLogOpt <- updateAppLogQuery.get(savedApp1.id, jobId).transaction - getUpdateLog = getUpdateLogOpt.get - - appErrorList <- appErrorQuery.get(getApp.app.id).transaction - } yield { - getApp.app.errors.size shouldBe 1 - getApp.app.errors.map(_.action) should contain(ErrorAction.UpdateApp) - getApp.app.errors.map(_.source) should contain(ErrorSource.App) - getApp.app.errors.head.errorMessage should include("test") - getApp.app.status shouldBe AppStatus.Running - - appErrorList.size shouldBe 1 - getUpdateLog.errorId.isDefined shouldBe true - getUpdateLog.status shouldBe UpdateAppJobStatus.Error - getUpdateLog.endTime.isDefined shouldBe true - getUpdateLog.startTime.toEpochMilli shouldBe startTime.toEpochMilli - // We cant verify before/after because tests don't use the same application ctx that the app does - getUpdateLog.endTime.get.toEpochMilli == getUpdateLog.startTime.toEpochMilli shouldBe false - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions, 20) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - verify(mockAckConsumer, times(1)).ack() - } - - it should "properly update app log after successful update" in isolatedDbTest { - val queue = makeTaskQueue() - val mockAckConsumer = mock[AckHandler] - - val mockAksInterp = new MockAKSInterp {} - - val leoSubscriber = makeLeoSubscriber( - azureInterp = makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm, mockAksInterp = mockAksInterp), - asyncTaskQueue = queue - ) - - val savedCluster1 = makeAzureCluster(1).save() - val savedNodepool1 = makeNodepool(1, savedCluster1.id).save() - val savedApp1 = makeApp(1, savedNodepool1.id, appType = AppType.Cromwell).save() - val jobId = UpdateAppJobId(UUID.randomUUID()) - val msg = - UpdateAppMessage(jobId, - savedApp1.id, - savedApp1.appName, - savedCluster1.cloudContext, - savedApp1.workspaceId, - None, - None - ) - - val startTime = Instant.now() - - val res = - for { - _ <- updateAppLogQuery.save(jobId, savedApp1.id, startTime).transaction - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, None, instantTimestamp, mockAckConsumer)) - - assertions = for { - getAppOpt <- KubernetesServiceDbQueries - .getActiveFullAppByName(savedCluster1.cloudContext, savedApp1.appName) - .transaction - getApp = getAppOpt.get - getUpdateLogOpt <- updateAppLogQuery.get(savedApp1.id, jobId).transaction - getUpdateLog = getUpdateLogOpt.get - - appErrorList <- appErrorQuery.get(getApp.app.id).transaction - } yield { - getApp.app.errors.size shouldBe 0 - getApp.app.status shouldBe savedApp1.status - - appErrorList.size shouldBe 0 - getUpdateLog.errorId.isDefined shouldBe false - getUpdateLog.status shouldBe UpdateAppJobStatus.Success - getUpdateLog.endTime.isDefined shouldBe true - getUpdateLog.startTime.toEpochMilli shouldBe startTime.toEpochMilli - // We cant verify before/after because tests don't use the same application ctx that the app does - getUpdateLog.endTime.get.toEpochMilli == getUpdateLog.startTime.toEpochMilli shouldBe false - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions, maxRetry = 20) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - verify(mockAckConsumer, times(1)).ack() - } - - it should "save an error and transition app to error when an error occurs in azure deleteApp" in isolatedDbTest { - val exception = new RuntimeException("random test exception") - val queue = makeTaskQueue() - val mockAckConsumer = mock[AckHandler] - - val mockAksInterp = new MockAKSInterp { - override def deleteApp(params: DeleteAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = - IO.raiseError(exception) - } - - val leoSubscriber = makeLeoSubscriber( - azureInterp = makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm, mockAksInterp = mockAksInterp), - asyncTaskQueue = queue - ) - - val savedCluster1 = makeAzureCluster(1).save() - val savedNodepool1 = makeNodepool(1, savedCluster1.id).save() - val savedApp1 = makeApp(1, savedNodepool1.id, appType = AppType.Cromwell).save() - val msg = - DeleteAppV2Message(savedApp1.id, - savedApp1.appName, - savedApp1.workspaceId.get, - savedCluster1.cloudContext, - None, - billingProfileId, - None - ) - - val res = - for { - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, None, instantTimestamp, mockAckConsumer)) - - assertions = for { - getAppOpt <- KubernetesServiceDbQueries - .getActiveFullAppByName(savedCluster1.cloudContext, savedApp1.appName) - .transaction - getApp = getAppOpt.get - } yield { - getApp.app.errors.size shouldBe 1 - getApp.app.errors.map(_.action) should contain(ErrorAction.DeleteApp) - getApp.app.errors.map(_.source) should contain(ErrorSource.App) - getApp.app.errors.head.errorMessage should include("test") - getApp.app.status shouldBe AppStatus.Error - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - verify(mockAckConsumer, times(1)).ack() - } - - it should "save an error and transition app to error when an error occurs in azure createApp" in isolatedDbTest { - val exception = new RuntimeException("random test exception") - val queue = makeTaskQueue() - val mockAckConsumer = mock[AckHandler] - - val mockAksInterp = new MockAKSInterp { - override def createAndPollApp(params: CreateAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = - IO.raiseError(exception) - } - - val leoSubscriber = makeLeoSubscriber( - azureInterp = makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm, mockAksInterp = mockAksInterp), - asyncTaskQueue = queue - ) - - val savedCluster1 = makeAzureCluster(1).save() - val savedNodepool1 = makeNodepool(1, savedCluster1.id).save() - val savedApp1 = makeApp(1, savedNodepool1.id, appType = AppType.Cromwell).save() - val msg = - CreateAppV2Message(savedApp1.id, - savedApp1.appName, - savedApp1.workspaceId.get, - savedCluster1.cloudContext, - billingProfileId, - None - ) - - val res = - for { - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, None, instantTimestamp, mockAckConsumer)) - - assertions = for { - getAppOpt <- KubernetesServiceDbQueries - .getActiveFullAppByName(savedCluster1.cloudContext, savedApp1.appName) - .transaction - getApp = getAppOpt.get - } yield { - getApp.app.errors.size shouldBe 1 - getApp.app.errors.map(_.action) should contain(ErrorAction.CreateApp) - getApp.app.errors.map(_.source) should contain(ErrorSource.App) - getApp.app.errors.head.errorMessage should include("test") - getApp.app.status shouldBe AppStatus.Error - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - verify(mockAckConsumer, times(1)).ack() - } - it should "create a metric for a successful and failed condition" in isolatedDbTest { val savedCluster1 = makeKubeCluster(1).save() val savedNodepool1 = makeNodepool(1, savedCluster1.id).save() @@ -2632,8 +2308,7 @@ class LeoPubsubMessageSubscriberSpec relayService: AzureRelayService[IO] = FakeAzureRelayService, wsmDAO: MockWsmDAO = new MockWsmDAO, azureVmService: AzureVmService[IO] = FakeAzureVmService, - mockWsmClient: WsmApiClientProvider[IO] = mockWsm, - mockAksInterp: AKSAlgebra[IO] = new MockAKSInterp() + mockWsmClient: WsmApiClientProvider[IO] = mockWsm ): AzurePubsubHandlerAlgebra[IO] = new AzurePubsubHandlerInterp[IO]( ConfigReader.appConfig.azure.pubsubHandler, @@ -2653,7 +2328,6 @@ class LeoPubsubMessageSubscriberSpec new MockJupyterDAO(), relayService, azureVmService, - mockAksInterp, refererConfig, mockWsmClient ) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala index e2fe40b6f6..f667095b69 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala @@ -15,9 +15,7 @@ import org.broadinstitute.dsde.workbench.leonardo.monitor.ClusterNodepoolAction. } import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ CreateAppMessage, - CreateAppV2Message, - DeleteAppMessage, - DeleteAppV2Message + DeleteAppMessage } import org.broadinstitute.dsde.workbench.leonardo.{ AppMachineType, @@ -147,27 +145,6 @@ class MonitorAtBootSpec extends AnyFlatSpec with TestComponent with LeonardoTest res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "recover AppStatus.Provisioning properly with cluster and nodepool creation in Azure" in isolatedDbTest { - val res = for { - queue <- Queue.bounded[IO, LeoPubsubMessage](10) - monitorAtBoot = createMonitorAtBoot(queue) - cluster <- IO(makeAzureCluster(1).copy(status = KubernetesClusterStatus.Provisioning).save()) - nodepool <- IO(makeNodepool(2, cluster.id).copy(status = NodepoolStatus.Provisioning).save()) - disk <- makePersistentDisk(None).copy(status = DiskStatus.Creating).save() - app = makeApp(1, nodepool.id).copy(status = AppStatus.Provisioning) - appWithDisk = LeoLenses.appToDisk.set(Some(disk))(app) - savedApp <- IO(appWithDisk.save()) - _ <- monitorAtBoot.process.take(1).compile.drain - msg <- queue.tryTake - } yield { - msg.isDefined shouldBe true - val createMsg = msg.get.asInstanceOf[CreateAppV2Message] - createMsg.cloudContext shouldBe cluster.cloudContext - createMsg.appId shouldBe savedApp.id - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "recover AppStatus.Provisioning properly with nodepool creation in GCP" in isolatedDbTest { val res = for { queue <- Queue.bounded[IO, LeoPubsubMessage](10) @@ -200,27 +177,6 @@ class MonitorAtBootSpec extends AnyFlatSpec with TestComponent with LeonardoTest res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "recover AppStatus.Provisioning properly with nodepool creation in Azure" in isolatedDbTest { - val res = for { - queue <- Queue.bounded[IO, LeoPubsubMessage](10) - monitorAtBoot = createMonitorAtBoot(queue) - cluster <- IO(makeAzureCluster(1).copy(status = KubernetesClusterStatus.Running).save()) - nodepool <- IO(makeNodepool(2, cluster.id).copy(status = NodepoolStatus.Provisioning).save()) - disk <- makePersistentDisk(None).copy(status = DiskStatus.Creating).save() - app = makeApp(1, nodepool.id).copy(status = AppStatus.Provisioning) - appWithDisk = LeoLenses.appToDisk.set(Some(disk))(app) - savedApp <- IO(appWithDisk.save()) - _ <- monitorAtBoot.process.take(1).compile.drain - msg <- queue.tryTake - } yield { - msg.isDefined shouldBe true - val createMsg = msg.get.asInstanceOf[CreateAppV2Message] - createMsg.cloudContext shouldBe cluster.cloudContext - createMsg.appId shouldBe savedApp.id - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "recover AppStatus.Provisioning properly in GCP" in isolatedDbTest { val res = for { queue <- Queue.bounded[IO, LeoPubsubMessage](10) @@ -253,27 +209,6 @@ class MonitorAtBootSpec extends AnyFlatSpec with TestComponent with LeonardoTest res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "recover AppStatus.Provisioning properly in Azure" in isolatedDbTest { - val res = for { - queue <- Queue.bounded[IO, LeoPubsubMessage](10) - monitorAtBoot = createMonitorAtBoot(queue) - cluster <- IO(makeAzureCluster(1).copy(status = KubernetesClusterStatus.Running).save()) - nodepool <- IO(makeNodepool(2, cluster.id).copy(status = NodepoolStatus.Running).save()) - disk <- makePersistentDisk(None).copy(status = DiskStatus.Creating).save() - app = makeApp(1, nodepool.id).copy(status = AppStatus.Provisioning) - appWithDisk = LeoLenses.appToDisk.set(Some(disk))(app) - savedApp <- IO(appWithDisk.save()) - _ <- monitorAtBoot.process.take(1).compile.drain - msg <- queue.tryTake - } yield { - msg.isDefined shouldBe true - val createMsg = msg.get.asInstanceOf[CreateAppV2Message] - createMsg.cloudContext shouldBe cluster.cloudContext - createMsg.appId shouldBe savedApp.id - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "recover AppStatus.Deleting properly in GCP" in isolatedDbTest { val res = for { queue <- Queue.bounded[IO, LeoPubsubMessage](10) @@ -299,27 +234,6 @@ class MonitorAtBootSpec extends AnyFlatSpec with TestComponent with LeonardoTest res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "recover AppStatus.Deleting properly in Azure" in isolatedDbTest { - val res = for { - queue <- Queue.bounded[IO, LeoPubsubMessage](10) - monitorAtBoot = createMonitorAtBoot(queue) - cluster <- IO(makeAzureCluster(1).copy(status = KubernetesClusterStatus.Running).save()) - nodepool <- IO(makeNodepool(2, cluster.id).copy(status = NodepoolStatus.Deleting).save()) - disk <- makePersistentDisk(None).copy(status = DiskStatus.Ready).save() - app = makeApp(1, nodepool.id).copy(status = AppStatus.Deleting) - appWithDisk = LeoLenses.appToDisk.set(Some(disk))(app) - savedApp <- IO(appWithDisk.save()) - _ <- monitorAtBoot.process.take(1).compile.drain - msg <- queue.tryTake - } yield { - msg.isDefined shouldBe true - val createMsg = msg.get.asInstanceOf[DeleteAppV2Message] - createMsg.cloudContext shouldBe cluster.cloudContext - createMsg.appId shouldBe savedApp.id - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "ignore non-monitored apps in GCP" in isolatedDbTest { val res = for { queue <- Queue.bounded[IO, LeoPubsubMessage](10) @@ -336,22 +250,6 @@ class MonitorAtBootSpec extends AnyFlatSpec with TestComponent with LeonardoTest res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "ignore non-monitored apps in Azure" in isolatedDbTest { - val res = for { - queue <- Queue.bounded[IO, LeoPubsubMessage](10) - monitorAtBoot = createMonitorAtBoot(queue) - cluster <- IO(makeAzureCluster(1).copy(status = KubernetesClusterStatus.Running).save()) - nodepool <- IO(makeNodepool(2, cluster.id).copy(status = NodepoolStatus.Running).save()) - disk <- makePersistentDisk(None).copy(status = DiskStatus.Ready).save() - app = makeApp(1, nodepool.id).copy(status = AppStatus.Running) - appWithDisk = LeoLenses.appToDisk.set(Some(disk))(app) - _ <- IO(appWithDisk.save()) - _ <- monitorAtBoot.process.take(1).compile.drain - msg <- queue.tryTake - } yield msg shouldBe None - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - def createMonitorAtBoot( queue: Queue[IO, LeoPubsubMessage] = Queue.bounded[IO, LeoPubsubMessage](10).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreterSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreterSpec.scala deleted file mode 100644 index 4c1aa0c785..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AKSInterpreterSpec.scala +++ /dev/null @@ -1,1404 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package util - -import bio.terra.workspace.api.{ControlledAzureResourceApi, ResourceApi, WorkspaceApi} -import bio.terra.workspace.model.{DeleteControlledAzureResourceRequest, _} -import cats.data.Kleisli -import cats.effect.IO -import cats.mtl.Ask -import com.azure.resourcemanager.containerservice.models.KubernetesCluster -import io.kubernetes.client.openapi.apis.CoreV1Api -import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.google2.KubernetesModels.PodStatus -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.{NamespaceName, ServiceAccountName} -import org.broadinstitute.dsde.workbench.google2.{KubernetesModels, NetworkName, SubnetworkName} -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{makeApp, makeKubeCluster, makeNodepool} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.app.Database.ControlledDatabase -import org.broadinstitute.dsde.workbench.leonardo.app.{AppInstall, WorkflowsAppInstall} -import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider -import org.broadinstitute.dsde.workbench.leonardo.config.Config.appMonitorConfig -import org.broadinstitute.dsde.workbench.leonardo.config.SamConfig -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http.{dbioToIO, ConfigReader} -import org.broadinstitute.dsde.workbench.model.WorkbenchEmail -import org.broadinstitute.dsp.mocks.MockHelm -import org.broadinstitute.dsp._ -import org.http4s.headers.Authorization -import org.http4s.{AuthScheme, Credentials} -import org.mockito.ArgumentMatchers.{any, eq => mockitoEq} -import org.mockito.Mockito._ -import org.mockito.{ArgumentCaptor, ArgumentMatchers} -import org.scalatest.Succeeded -import org.scalatest.flatspec.AnyFlatSpecLike -import org.scalatestplus.mockito.MockitoSugar - -import java.net.URL -import java.nio.file.Files -import java.util.{Base64, UUID} -import scala.collection.mutable -import scala.concurrent.ExecutionContext.Implicits.global -import scala.jdk.CollectionConverters._ - -class AKSInterpreterSpec extends AnyFlatSpecLike with TestComponent with LeonardoTestSuite with MockitoSugar { - - val config = AKSInterpreterConfig( - SamConfig("https://sam.dsde-dev.broadinstitute.org/"), - appMonitorConfig, - ConfigReader.appConfig.azure.wsm, - new URL("https://leo-dummy-url.org"), - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.listenerImage, - ConfigReader.appConfig.azure.listenerChartConfig - ) - - val mockSamDAO = setUpMockSamDAO - val mockWsmDAO = new MockWsmDAO - val mockAzureContainerService = setUpMockAzureContainerService - val mockAzureRelayService = setUpMockAzureRelayService - val mockKube = setUpMockKube - val (mockWsm, mockControlledResourceApi, mockResourceApi, mockWorkspaceApi) = setUpMockWsmApiClientProvider() - val mockSamAuthProvider = setUpMockSamAuthProvider - - implicit val appTypeToAppInstall: AppType => AppInstall[IO] = { - case AppType.WorkflowsApp => setUpMockWorkflowAppInstall() - case _ => setUpMockAppInstall() - } - def newAksInterp(configuration: AKSInterpreterConfig = config, - mockWsm: WsmApiClientProvider[IO] = mockWsm, - helmClient: MockHelm = new MockHelm - ) = - new AKSInterpreter[IO]( - configuration, - helmClient, - mockAzureContainerService, - mockAzureRelayService, - mockSamDAO, - mockWsmDAO, - mockKube, - mockWsm, - mockWsmDAO, - mockSamAuthProvider, - MockSamService - ) - - val aksInterp = newAksInterp(config) - - val cloudContext = AzureCloudContext( - TenantId("tenant"), - SubscriptionId("sub"), - ManagedResourceGroupName("mrg") - ) - - val lzResources = LandingZoneResources( - UUID.fromString("5c12f64b-f4ac-4be1-ae4a-4cace5de807d"), - AKSCluster("cluster", Map.empty[String, Boolean]), - BatchAccountName("batch"), - RelayNamespace("relay"), - StorageAccountName("storage"), - NetworkName("network"), - SubnetworkName("subnet1"), - SubnetworkName("subnet2"), - azureRegion, - ApplicationInsightsName("lzappinsights"), - Some(PostgresServer("postgres", false)) - ) - - val storageContainer = StorageContainerResponse( - ContainerName("sc-container"), - WsmControlledResourceId(UUID.randomUUID) - ) - - "AKSInterpreter" should "get a helm auth context" in { - val res = for { - authContext <- aksInterp.getHelmAuthContext(lzResources.aksCluster.asClusterName, - cloudContext, - NamespaceName("ns") - ) - } yield { - authContext.namespace.asString shouldBe "ns" - authContext.kubeApiServer.asString shouldBe "server" - authContext.kubeToken.asString shouldBe "token" - Files.exists(authContext.caCertFile.path) shouldBe true - Files.readAllLines(authContext.caCertFile.path).asScala.mkString shouldBe "cert" - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // create and poll each app type - for ( - appType <- List(AppType.Wds, AppType.Cromwell, AppType.HailBatch, AppType.WorkflowsApp, AppType.CromwellRunnerApp) - ) - it should s"create and poll ${appType} app" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - namespace = NamespaceName(s"$appType-ns-1") - app = makeApp(1, nodepool.id).copy( - appType = appType, - appResources = AppResources( - namespace = namespace, - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ), - googleServiceAccount = WorkbenchEmail( - "/subscriptions/sub/resourcegroups/mrg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/mi-1" - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - appName = saveApp.appName - - params = CreateAKSAppParams(appId, appName, workspaceId, cloudContext, billingProfileId) - _ <- aksInterp.createAndPollApp(params) - - app <- KubernetesServiceDbQueries - .getActiveFullAppByName(CloudContext.Azure(params.cloudContext), appName) - .transaction - } yield { - app shouldBe defined - app.get.app.status shouldBe AppStatus.Running - app.get.cluster.asyncFields shouldBe defined - - // verify that cloning instructions for Azure K8s namespace is COPY_NOTHING for all apps - val createNamespaceCaptor = - ArgumentCaptor.forClass(classOf[CreateControlledAzureKubernetesNamespaceRequestBody]) - verify(mockControlledResourceApi, atLeastOnce()).createAzureKubernetesNamespace(createNamespaceCaptor.capture(), - any() - ) - assert(createNamespaceCaptor.getValue.isInstanceOf[CreateControlledAzureKubernetesNamespaceRequestBody]) - val createNamespaceCaptorValues = - createNamespaceCaptor.getValue.asInstanceOf[CreateControlledAzureKubernetesNamespaceRequestBody] - createNamespaceCaptorValues.getCommon.getName shouldBe namespace.value - createNamespaceCaptorValues.getCommon.getCloningInstructions shouldBe CloningInstructionsEnum.NOTHING - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // update and poll each app type - for ( - appType <- List(AppType.Wds, AppType.Cromwell, AppType.HailBatch, AppType.WorkflowsApp, AppType.CromwellRunnerApp) - ) - it should s"update and poll ${appType} app" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = appType, - status = AppStatus.Updating, - chart = Chart(ChartName("myapp"), ChartVersion("0.0.1")), - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appName = saveApp.appName - appId = saveApp.id - - namespaceId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(namespaceId), - WsmResourceType.AzureKubernetesNamespace, - AppControlledResourceStatus.Created - ) - .transaction - - _ <- aksInterp.updateAndPollApp( - UpdateAKSAppParams(appId, appName, ChartVersion("0.0.2"), Some(workspaceIdForUpdating), cloudContext) - ) - app <- KubernetesServiceDbQueries - .getActiveFullAppByName(CloudContext.Azure(cloudContext), appName) - .transaction - } yield { - app shouldBe defined - app.get.app.status shouldBe AppStatus.Running - app.get.app.chart shouldBe Chart(ChartName("myapp"), ChartVersion("0.0.2")) - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should s"emit an exception if the app is not alive prior to upgrading" in isolatedDbTest { - implicit val appTypeToAppInstall: AppType => AppInstall[IO] = { - case AppType.WorkflowsApp => setUpMockWorkflowAppInstall(false) - case _ => setUpMockAppInstall(false) - } - val aksInterp = new AKSInterpreter[IO]( - config, - new MockHelm, - mockAzureContainerService, - mockAzureRelayService, - mockSamDAO, - mockWsmDAO, - mockKube, - mockWsm, - mockWsmDAO, - mockSamAuthProvider, - MockSamService - ) - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = AppType.Cromwell, - status = AppStatus.Running, - chart = Chart(ChartName("myapp"), ChartVersion("0.0.1")), - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appName = saveApp.appName - appId = saveApp.id - - namespaceId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(namespaceId), - WsmResourceType.AzureKubernetesNamespace, - AppControlledResourceStatus.Created - ) - .transaction - - _ <- aksInterp - .updateAndPollApp( - UpdateAKSAppParams(appId, appName, ChartVersion("0.0.2"), Some(workspaceIdForUpdating), cloudContext) - ) - - } yield () - val either = res.attempt.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - either.isLeft shouldBe true - either match { - case Left(value) => - value.getMessage should include("was not alive") - value.getClass shouldBe AppUpdatePollingException("message", None).getClass - case _ => fail() - } - } - - // Note that the app will eventually transition to error or running status, but `Running` occurs on success and `Error` occurs in `LeoPubsubMessageSubscriber` (the latter being applicable to this test). - // This test ensures that the underlying `AKSInterpreter` correctly transitions app to `Upgrading` and doesn't transition it to `Running` if an error occurs - it should s"exit with app in Updating status if error occurs in helm client" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = AppType.Cromwell, - status = AppStatus.Running, - chart = Chart(ChartName("myapp"), ChartVersion("0.0.1")), - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appName = saveApp.appName - appId = saveApp.id - - namespaceId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(namespaceId), - WsmResourceType.AzureKubernetesNamespace, - AppControlledResourceStatus.Created - ) - .transaction - - mockHelm = new MockHelm { - override def upgradeChart(release: Release, - chartName: ChartName, - chartVersion: ChartVersion, - values: Values - ): Kleisli[IO, AuthContext, Unit] = Kleisli.liftF(IO.raiseError(HelmException("test exception"))) - } - - aksInterp = newAksInterp(helmClient = mockHelm) - - // Throw away the error here, we aren't trying to verify that the exception we are mocking above is being thrown - _ <- aksInterp - .updateAndPollApp( - UpdateAKSAppParams(appId, appName, ChartVersion("0.0.2"), Some(workspaceIdForUpdating), cloudContext) - ) - .handleErrorWith(_ => IO.unit) - - app <- KubernetesServiceDbQueries - .getActiveFullAppByName(CloudContext.Azure(cloudContext), appName) - .transaction - } yield { - app shouldBe defined - app.get.app.status shouldBe AppStatus.Updating - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // delete each app type - for ( - appType <- List(AppType.Wds, AppType.Cromwell, AppType.HailBatch, AppType.WorkflowsApp, AppType.CromwellRunnerApp) - ) - it should s"delete and poll $appType app" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = appType, - status = AppStatus.Running, - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appName = saveApp.appName - - _ <- aksInterp.deleteApp(DeleteAKSAppParams(appName, workspaceId, cloudContext, billingProfileId)) - app <- KubernetesServiceDbQueries - .getActiveFullAppByName(CloudContext.Azure(cloudContext), appName) - .transaction - } yield app shouldBe None - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "retrieve a WSM controlled identity if it exists in workspace" in isolatedDbTest { - val res = for { - retrievedIdentity <- aksInterp.retrieveWsmManagedIdentity(mockResourceApi, - AppType.WorkflowsApp, - workspaceId.value - ) - } yield { - retrievedIdentity.get.wsmResourceName shouldBe "idworkflows_app" - retrievedIdentity.get.managedIdentityName shouldBe "abcxyz" - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "not retrieve a WSM controlled identity if it doesn't exist in workspace" in isolatedDbTest { - val res = for { - retrievedIdentity <- aksInterp.retrieveWsmManagedIdentity(mockResourceApi, - AppType.CromwellRunnerApp, - workspaceId.value - ) - } yield retrievedIdentity shouldBe None - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "retrieve a WSM database if it exists in workspace" in isolatedDbTest { - val res = for { - retrievedDatabases <- aksInterp.retrieveWsmDatabases(mockResourceApi, Set("cromwellmetadata"), workspaceId.value) - } yield { - retrievedDatabases.head.wsmDatabaseName shouldBe "cromwellmetadata" - retrievedDatabases.head.azureDatabaseName shouldBe "cromwellmetadata_abcxyz" - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not retrieve a WSM database if it doesn't exist in workspace" in isolatedDbTest { - val res = for { - retrievedDatabases <- aksInterp.retrieveWsmDatabases(mockResourceApi, Set("cbas"), workspaceId.value) - } yield retrievedDatabases shouldBe empty - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "retrieve a WSM namespace if it exists in workspace" in isolatedDbTest { - val res = for { - retrievedNamespaces <- aksInterp.retrieveWsmNamespace(mockResourceApi, "ns-name", workspaceId.value) - } yield retrievedNamespaces.map(ns => ns.name.value) shouldBe Some("ns-name") - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not retrieve a WSM namespace if doesn't exist in workspace" in isolatedDbTest { - val res = for { - retrievedNamespaces <- aksInterp.retrieveWsmNamespace(mockResourceApi, "something-else", workspaceId.value) - } yield retrievedNamespaces shouldBe None - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // create wsm identity for shared apps - for (appType <- List(AppType.Wds, AppType.WorkflowsApp)) - it should s"create a WSM controlled identity for $appType app" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id, appAccessScope = AppAccessScope.WorkspaceShared).copy( - appType = appType, - status = AppStatus.Running, - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - - createdIdentity <- aksInterp.createWsmIdentityResource(saveApp, "ns", workspaceId) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - } yield { - createdIdentity.getAzureManagedIdentity.getMetadata.getName shouldBe s"id${appType.toString.toLowerCase}" - controlledResources.size shouldBe 1 - controlledResources.head.resourceId.value shouldBe createdIdentity.getResourceId - controlledResources.head.resourceType shouldBe WsmResourceType.AzureManagedIdentity - controlledResources.head.status shouldBe AppControlledResourceStatus.Created - controlledResources.head.appId shouldBe appId.id - - // verify that cloning instructions for Azure managed identity is: - // - COPY_NOTHING for WDS app - // - COPY_RESOURCE for Workflows app - val expectedCloningInstructions = - if (appType == AppType.WorkflowsApp) CloningInstructionsEnum.RESOURCE else CloningInstructionsEnum.NOTHING - createdIdentity.getAzureManagedIdentity.getMetadata.getCloningInstructions shouldBe expectedCloningInstructions - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // fetch wsm database or create if it doesn't exists - it should "retrieve cbas database and create cromwellmetadata database for WORKFLOWS app" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id, appAccessScope = AppAccessScope.WorkspaceShared).copy( - appType = AppType.WorkflowsApp, - status = AppStatus.Running, - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - - controlledDatabases <- aksInterp.createOrFetchWsmDatabaseResources(saveApp, - saveApp.appType, - workspaceIdForCloning, - app.appResources.namespace.value, - Option("idworkflows_app"), - lzResources, - mockResourceApi - ) - - _ <- aksInterp.createMissingAppControlledResources(saveApp, - saveApp.appType, - workspaceIdForCloning, - lzResources, - mockResourceApi - ) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - } yield { - controlledDatabases.size shouldBe 2 - controlledDatabases.head.wsmDatabaseName shouldBe "cbas" - controlledDatabases.head.azureDatabaseName shouldBe "cbas_cloned_db_abcxyz" - controlledDatabases(1).wsmDatabaseName shouldBe "cromwellmetadata" - controlledDatabases(1).azureDatabaseName shouldBe "cromwellmetadata_ns" - - controlledResources.size shouldBe 2 - controlledResources.head.resourceType shouldBe WsmResourceType.AzureDatabase - controlledResources.head.status shouldBe AppControlledResourceStatus.Created - controlledResources.head.appId shouldBe appId.id - controlledResources(1).resourceType shouldBe WsmResourceType.AzureDatabase - controlledResources(1).status shouldBe AppControlledResourceStatus.Created - controlledResources(1).appId shouldBe appId.id - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // create wsm database - it should "create a WSM controlled database" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id, appAccessScope = AppAccessScope.WorkspaceShared).copy( - appType = AppType.Wds, - status = AppStatus.Running, - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - owner = "idwds" - - controlledDatabases <- aksInterp.createOrFetchWsmDatabaseResources(saveApp, - saveApp.appType, - workspaceIdForAppCreation, - app.appResources.namespace.value, - Some(owner), - lzResources, - mockResourceApi - ) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - } yield { - controlledDatabases.size shouldBe 1 - // for all apps (except Workflows app) "db1" is the returned controlled database in mock data - controlledDatabases.head.wsmDatabaseName shouldBe "db1" - controlledDatabases.head.azureDatabaseName shouldBe "db1_ns" - - controlledResources.size shouldBe 1 - controlledResources.head.resourceType shouldBe WsmResourceType.AzureDatabase - controlledResources.head.status shouldBe AppControlledResourceStatus.Created - controlledResources.head.appId shouldBe appId.id - - // verify that cloning instructions for Azure database is set to COPY_NOTHING - val createDbCaptor = ArgumentCaptor.forClass(classOf[CreateControlledAzureDatabaseRequestBody]) - verify(mockControlledResourceApi, atLeastOnce()).createAzureDatabase(createDbCaptor.capture(), any()) - assert(createDbCaptor.getValue.isInstanceOf[CreateControlledAzureDatabaseRequestBody]) - val createDbCaptorValues = createDbCaptor.getValue.asInstanceOf[CreateControlledAzureDatabaseRequestBody] - createDbCaptorValues.getCommon.getName shouldBe "db1" - createDbCaptorValues.getCommon.getCloningInstructions shouldBe CloningInstructionsEnum.NOTHING - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // create wsm databases for Workflows app - it should "create WSM controlled databases for Workflows app" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id, appAccessScope = AppAccessScope.WorkspaceShared).copy( - appType = AppType.WorkflowsApp, - status = AppStatus.Running, - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - - controlledDatabases <- aksInterp.createOrFetchWsmDatabaseResources(saveApp, - saveApp.appType, - workspaceIdForAppCreation, - app.appResources.namespace.value, - Option("idworkflows_app"), - lzResources, - mockResourceApi - ) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - } yield { - controlledDatabases.size shouldBe 2 - controlledDatabases.head.wsmDatabaseName shouldBe "cbas" - controlledDatabases.head.azureDatabaseName shouldBe "cbas_ns" - controlledDatabases(1).wsmDatabaseName shouldBe "cromwellmetadata" - controlledDatabases(1).azureDatabaseName shouldBe "cromwellmetadata_ns" - - controlledResources.size shouldBe 2 - controlledResources.head.resourceType shouldBe WsmResourceType.AzureDatabase - controlledResources.head.status shouldBe AppControlledResourceStatus.Created - controlledResources.head.appId shouldBe appId.id - controlledResources(1).resourceType shouldBe WsmResourceType.AzureDatabase - controlledResources(1).status shouldBe AppControlledResourceStatus.Created - controlledResources(1).appId shouldBe appId.id - - val createDbCaptor = ArgumentCaptor.forClass(classOf[CreateControlledAzureDatabaseRequestBody]) - verify(mockControlledResourceApi, atLeastOnce()).createAzureDatabase(createDbCaptor.capture(), any()) - // there are multiple invocations of "createAzureDatabase" in the mock API before this test is run. - // We extract the last 2 invocations which reflect the calls made as part of this test. - // Call for "cbas" db creation will be second last in the list - val cbasDbCreationCallIndex = createDbCaptor.getAllValues.size() - 2 - assert( - createDbCaptor.getAllValues - .get(cbasDbCreationCallIndex) - .isInstanceOf[CreateControlledAzureDatabaseRequestBody] - ) - // Call for "crowmellmetadata" db creation will be last in the list - assert(createDbCaptor.getValue.isInstanceOf[CreateControlledAzureDatabaseRequestBody]) - - // verify that cloning instructions for "cbas" Azure database is set to COPY_RESOURCE - val cbasDbCaptorValues = createDbCaptor.getAllValues - .get(cbasDbCreationCallIndex) - .asInstanceOf[CreateControlledAzureDatabaseRequestBody] - cbasDbCaptorValues.getCommon.getName shouldBe "cbas" - cbasDbCaptorValues.getCommon.getCloningInstructions shouldBe CloningInstructionsEnum.RESOURCE - - // verify that cloning instructions for "cromwellmetadata" Azure database is set to COPY_NOTHING - val cromwellMetadataDbCaptorValues = - createDbCaptor.getValue.asInstanceOf[CreateControlledAzureDatabaseRequestBody] - cromwellMetadataDbCaptorValues.getCommon.getName shouldBe "cromwellmetadata" - cromwellMetadataDbCaptorValues.getCommon.getCloningInstructions shouldBe CloningInstructionsEnum.NOTHING - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - // create wsm k8s namespace - it should "create a WSM controlled namespace" in isolatedDbTest { - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id, appAccessScope = AppAccessScope.WorkspaceShared).copy( - appType = AppType.Cromwell, - status = AppStatus.Running, - appResources = AppResources( - NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - databases = List("db1", "db2") - identity = "id" - - createdNamespace <- aksInterp.createWsmKubernetesNamespaceResource(saveApp, - workspaceId, - "ns", - databases, - Some(identity) - ) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - } yield { - createdNamespace.name.value should startWith("ns") - controlledResources.size shouldBe 1 - controlledResources.head.resourceId shouldBe createdNamespace.wsmResourceId - controlledResources.head.resourceType shouldBe WsmResourceType.AzureKubernetesNamespace - controlledResources.head.status shouldBe AppControlledResourceStatus.Created - controlledResources.head.appId shouldBe appId.id - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not create a WSM managed identity for a private app" in isolatedDbTest { - val (mockWsm, mockControlledResourceApi, _, _) = setUpMockWsmApiClientProvider() - val aksInterp = newAksInterp(config, mockWsm = mockWsm) - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = AppType.Cromwell, - status = AppStatus.Running, - appResources = AppResources( - NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - appName = saveApp.appName - - params = CreateAKSAppParams(appId, appName, workspaceId, cloudContext, billingProfileId) - _ <- aksInterp.createAndPollApp(params) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - } yield { - controlledResources.size shouldBe 2 - verify(mockControlledResourceApi, never()).createAzureManagedIdentity(any(), any()) - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not create a WSM controlled namespace if one already exists" in isolatedDbTest { - val (mockWsm, mockControlledResourceApi, _, _) = setUpMockWsmApiClientProvider() - val aksInterp = newAksInterp(config, mockWsm = mockWsm) - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = AppType.Cromwell, - status = AppStatus.Running, - appResources = AppResources( - NamespaceName("ns-name"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - databases = List("db1", "db2") - identity = "id" - - createdNamespace <- aksInterp.createWsmKubernetesNamespaceResource(saveApp, - workspaceId, - "ns-name", - databases, - Some(identity) - ) - - params = CreateAKSAppParams(appId, saveApp.appName, workspaceId, cloudContext, billingProfileId) - _ <- aksInterp.createAndPollApp(params) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - namespaceRecord = controlledResources.filter(r => r.resourceType == WsmResourceType.AzureKubernetesNamespace).head - } yield { - verify(mockControlledResourceApi, times(1)) - .createAzureKubernetesNamespace(any[CreateControlledAzureKubernetesNamespaceRequestBody], any[UUID]) - controlledResources.size shouldBe 2 - namespaceRecord.resourceId shouldBe createdNamespace.wsmResourceId - namespaceRecord.status shouldBe AppControlledResourceStatus.Created - namespaceRecord.appId shouldBe appId.id - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not create a WSM managed identity if one already exists" in isolatedDbTest { - val (mockWsm, mockControlledResourceApi, _, _) = setUpMockWsmApiClientProvider() - val aksInterp = newAksInterp(config, mockWsm = mockWsm) - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = AppType.WorkflowsApp, - status = AppStatus.Running, - appResources = AppResources( - NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - appId = saveApp.id - - _ <- aksInterp.createAzureManagedIdentity(saveApp, "ns-1", workspaceId) - - params = CreateAKSAppParams(appId, saveApp.appName, workspaceId, cloudContext, billingProfileId) - _ <- aksInterp.createAndPollApp(params) - - controlledResources <- appControlledResourceQuery - .getAllForAppByStatus(appId.id, AppControlledResourceStatus.Created) - .transaction - idRecord = controlledResources.filter(r => r.resourceType == WsmResourceType.AzureManagedIdentity).head - } yield { - verify(mockControlledResourceApi, times(1)) - .createAzureManagedIdentity(any[CreateControlledAzureManagedIdentityRequestBody], any[UUID]) - controlledResources.size shouldBe 4 - idRecord.status shouldBe AppControlledResourceStatus.Created - idRecord.appId shouldBe appId.id - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete app WSM resources" in isolatedDbTest { - val (mockWsm, mockControlledResourceApi, _, _) = setUpMockWsmApiClientProvider() - val aksInterp = newAksInterp(config, mockWsm = mockWsm) - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = AppType.Cromwell, - status = AppStatus.Running, - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - databaseId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(databaseId), - WsmResourceType.AzureDatabase, - AppControlledResourceStatus.Created - ) - .transaction - namespaceId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(namespaceId), - WsmResourceType.AzureKubernetesNamespace, - AppControlledResourceStatus.Created - ) - .transaction - identityId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(identityId), - WsmResourceType.AzureManagedIdentity, - AppControlledResourceStatus.Created - ) - .transaction - - params = DeleteAKSAppParams(saveApp.appName, workspaceId, cloudContext, billingProfileId) - _ <- aksInterp.deleteApp(params) - - deletedControlledResources <- appControlledResourceQuery - .getAllForApp(appId) - .transaction - } yield { - deletedControlledResources.length shouldBe 3 - deletedControlledResources.map(_.status).distinct shouldBe List(AppControlledResourceStatus.Deleted) - verify(mockControlledResourceApi, times(1)).deleteAzureDatabaseAsync(any, - mockitoEq(workspaceId.value), - mockitoEq(databaseId) - ) - verify(mockControlledResourceApi, times(1)).getDeleteAzureDatabaseResult(mockitoEq(workspaceId.value), any) - verify(mockControlledResourceApi, times(1)).deleteAzureKubernetesNamespace(any, - mockitoEq(workspaceId.value), - mockitoEq(namespaceId) - ) - verify(mockControlledResourceApi, times(1)).getDeleteAzureKubernetesNamespaceResult(mockitoEq(workspaceId.value), - any - ) - verify(mockControlledResourceApi, times(1)).deleteAzureManagedIdentity(mockitoEq(workspaceId.value), - mockitoEq(identityId) - ) - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle deleting WSM resources that don't exist" in isolatedDbTest { - val (mockWsm, mockControlledResourceApi, _, _) = setUpMockWsmApiClientProvider(false, false, false) - val aksInterp = newAksInterp(config, mockWsm = mockWsm) - val res = for { - cluster <- IO(makeKubeCluster(1).copy(cloudContext = CloudContext.Azure(cloudContext)).save()) - nodepool <- IO(makeNodepool(1, cluster.id).save()) - app = makeApp(1, nodepool.id).copy( - appType = AppType.Cromwell, - status = AppStatus.Running, - appResources = AppResources( - namespace = NamespaceName("ns-1"), - disk = None, - services = List.empty, - kubernetesServiceAccountName = Some(ServiceAccountName("ksa-1")) - ) - ) - saveApp <- IO(app.save()) - - appId = saveApp.id - databaseId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(databaseId), - WsmResourceType.AzureDatabase, - AppControlledResourceStatus.Created - ) - .transaction - namespaceId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(namespaceId), - WsmResourceType.AzureKubernetesNamespace, - AppControlledResourceStatus.Created - ) - .transaction - identityId = UUID.randomUUID() - _ <- appControlledResourceQuery - .insert(appId.id, - WsmControlledResourceId(identityId), - WsmResourceType.AzureManagedIdentity, - AppControlledResourceStatus.Created - ) - .transaction - - params = DeleteAKSAppParams(saveApp.appName, workspaceId, cloudContext, billingProfileId) - _ <- aksInterp.deleteApp(params) - - deletedControlledResources <- appControlledResourceQuery - .getAllForApp(appId) - .transaction - } yield { - deletedControlledResources.length shouldBe 3 - deletedControlledResources.map(_.status).distinct shouldBe List(AppControlledResourceStatus.Deleted) - verify(mockControlledResourceApi, times(1)).deleteAzureDatabaseAsync(any, - mockitoEq(workspaceId.value), - mockitoEq(databaseId) - ) - verify(mockControlledResourceApi, times(0)).getDeleteAzureDatabaseResult(mockitoEq(workspaceId.value), any) - verify(mockControlledResourceApi, times(1)).deleteAzureKubernetesNamespace(any, - mockitoEq(workspaceId.value), - mockitoEq(namespaceId) - ) - verify(mockControlledResourceApi, times(0)).getDeleteAzureKubernetesNamespaceResult(mockitoEq(workspaceId.value), - any - ) - verify(mockControlledResourceApi, times(1)).deleteAzureManagedIdentity(mockitoEq(workspaceId.value), - mockitoEq(identityId) - ) - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - private def setUpMockAzureContainerService: AzureContainerService[IO] = { - val container = mock[AzureContainerService[IO]] - val cluster = mock[KubernetesCluster] - when { - cluster.nodeResourceGroup() - } thenReturn "node-rg" - when { - container.getCluster(any[String].asInstanceOf[AKSClusterName], any)(any) - } thenReturn IO.pure(cluster) - when { - container.getClusterCredentials(any[String].asInstanceOf[AKSClusterName], any)(any) - } thenReturn IO.pure( - AKSCredentials(AKSServer("server"), - AKSToken("token"), - AKSCertificate(Base64.getEncoder.encodeToString("cert".getBytes())) - ) - ) - container - } - - private def setUpMockAzureRelayService: AzureRelayService[IO] = { - val mockAzureRelayService = mock[AzureRelayService[IO]] - val primaryKey = PrimaryKey("testKey") - - when { - mockAzureRelayService.createRelayHybridConnection(any[String].asInstanceOf[RelayNamespace], - any[String].asInstanceOf[RelayHybridConnectionName], - any[String].asInstanceOf[AzureCloudContext] - )(any()) - } thenReturn IO.pure(primaryKey) - when { - mockAzureRelayService.getRelayHybridConnectionKey(any[String].asInstanceOf[RelayNamespace], - any[String].asInstanceOf[RelayHybridConnectionName], - any[String].asInstanceOf[AzureCloudContext] - )(any) - } thenReturn IO.pure(primaryKey) - when { - mockAzureRelayService.deleteRelayHybridConnection(any[String].asInstanceOf[RelayNamespace], - any[String].asInstanceOf[RelayHybridConnectionName], - any[String].asInstanceOf[AzureCloudContext] - )(any()) - } thenReturn IO.unit - mockAzureRelayService - } - - private def setUpMockKube(): KubernetesAlgebra[IO] = { - val coreV1Api = mock[CoreV1Api] - new KubernetesAlgebra[IO] { - override def createAzureClient(cloudContext: AzureCloudContext, clusterName: AKSClusterName)(implicit - ev: Ask[IO, AppContext] - ): IO[CoreV1Api] = IO.pure(coreV1Api) - override def listPodStatus(clusterId: CoreV1Api, namespace: KubernetesModels.KubernetesNamespace)(implicit - ev: Ask[IO, AppContext] - ): IO[List[PodStatus]] = IO.pure(List(PodStatus.Failed)) - override def createNamespace(client: CoreV1Api, namespace: KubernetesModels.KubernetesNamespace)(implicit - ev: Ask[IO, AppContext] - ): IO[Unit] = IO.unit - override def deleteNamespace(client: CoreV1Api, namespace: KubernetesModels.KubernetesNamespace)(implicit - ev: Ask[IO, AppContext] - ): IO[Unit] = IO.unit - override def namespaceExists(client: CoreV1Api, namespace: KubernetesModels.KubernetesNamespace)(implicit - ev: Ask[IO, AppContext] - ): IO[Boolean] = IO.pure(false) - } - } - - private def setUpMockSamDAO: SamDAO[IO] = { - val sam = mock[SamDAO[IO]] - when { - sam.getCachedArbitraryPetAccessToken(any)(any) - } thenReturn IO.pure(Some("token")) - when { - sam.getLeoAuthToken - } thenReturn IO.pure(Authorization(Credentials.Token(AuthScheme.Bearer, "leotoken"))) - sam - } - - private def setUpMockWsmApiClientProvider(databaseExists: Boolean = true, - namespaceExists: Boolean = true, - identityExists: Boolean = true - ): (WsmApiClientProvider[IO], ControlledAzureResourceApi, ResourceApi, WorkspaceApi) = { - val api = mock[ControlledAzureResourceApi] - val resourceApi = mock[ResourceApi] - val workspaceApi = mock[WorkspaceApi] - val dbsByJob = mutable.Map.empty[String, CreateControlledAzureDatabaseRequestBody] - val namespacesByJob = mutable.Map.empty[String, CreateControlledAzureKubernetesNamespaceRequestBody] - - // Create managed identity - when { - api.createAzureManagedIdentity(any, any) - } thenAnswer { invocation => - val requestBody = invocation.getArgument[CreateControlledAzureManagedIdentityRequestBody](0) - new CreatedControlledAzureManagedIdentity() - .resourceId(requestBody.getCommon.getResourceId) - .azureManagedIdentity( - new AzureManagedIdentityResource() - .metadata( - new ResourceMetadata() - .name(requestBody.getCommon.getName) - .cloningInstructions(requestBody.getCommon.getCloningInstructions) - ) - .attributes(new AzureManagedIdentityAttributes().managedIdentityName(requestBody.getCommon.getName)) - ) - } - - // delete managed identity - when { - api.deleteAzureManagedIdentity(any, any) - } thenAnswer { _ => - if (identityExists) - Succeeded - else throw new TestException() - } - - // Create database - when { - api.createAzureDatabase(any, any) - } thenAnswer { invocation => - val requestBody = invocation.getArgument[CreateControlledAzureDatabaseRequestBody](0) - val uuid = requestBody.getCommon.getResourceId - val jobId = requestBody.getJobControl.getId - dbsByJob += (jobId -> requestBody) - new CreatedControlledAzureDatabaseResult().resourceId(uuid) - } - - // Get create database job result - when { - api.getCreateAzureDatabaseResult(any, any) - } thenAnswer { invocation => - val jobId = invocation.getArgument[String](1) - val request = dbsByJob(jobId) - new CreatedControlledAzureDatabaseResult() - .resourceId(request.getCommon.getResourceId) - .azureDatabase( - new AzureDatabaseResource() - .metadata( - new ResourceMetadata() - .resourceId(request.getCommon.getResourceId) - .name(request.getCommon.getName) - ) - .attributes( - new AzureDatabaseAttributes() - .allowAccessForAllWorkspaceUsers(request.getAzureDatabase.isAllowAccessForAllWorkspaceUsers) - .databaseName(request.getAzureDatabase.getName) - .databaseOwner(request.getAzureDatabase.getOwner) - ) - ) - .jobReport(new JobReport().status(JobReport.StatusEnum.SUCCEEDED)) - } - - // delete database - when { - api.deleteAzureDatabaseAsync(any, any, any) - } thenAnswer { _ => - if (databaseExists) - new DeleteControlledAzureResourceResult() - .jobReport(new JobReport().status(JobReport.StatusEnum.SUCCEEDED)) - else throw new TestException() - } - - // get delete database job result - when { - api.getDeleteAzureDatabaseResult(any, any) - } thenAnswer { _ => - new DeleteControlledAzureResourceResult() - .jobReport(new JobReport().status(JobReport.StatusEnum.SUCCEEDED)) - } - - // Create Kubernetes Namespace - when { - api.createAzureKubernetesNamespace(any, any) - } thenAnswer { invocation => - val requestBody = invocation.getArgument[CreateControlledAzureKubernetesNamespaceRequestBody](0) - val uuid = requestBody.getCommon.getResourceId - val jobId = requestBody.getJobControl.getId - namespacesByJob += (jobId -> requestBody) - new CreatedControlledAzureKubernetesNamespaceResult().resourceId(uuid) - } - // Get create Kubernetes Namespace job result - when { - api.getCreateAzureKubernetesNamespaceResult(any, any) - } thenAnswer { invocation => - val jobId = invocation.getArgument[String](1) - val request = namespacesByJob(jobId) - new CreatedControlledAzureKubernetesNamespaceResult() - .resourceId(request.getCommon.getResourceId) - .azureKubernetesNamespace( - new AzureKubernetesNamespaceResource() - .metadata( - new ResourceMetadata().resourceId(request.getCommon.getResourceId).name(request.getCommon.getName) - ) - .attributes( - new AzureKubernetesNamespaceAttributes() - .kubernetesNamespace(request.getAzureKubernetesNamespace.getNamespacePrefix) - .databases(request.getAzureKubernetesNamespace.getDatabases) - .managedIdentity(request.getAzureKubernetesNamespace.getManagedIdentity) - .kubernetesServiceAccount( - Option(request.getAzureKubernetesNamespace.getManagedIdentity).map(_.toString).getOrElse("ksa-1") - ) - ) - ) - .jobReport( - new JobReport().status(JobReport.StatusEnum.SUCCEEDED) - ) - } - // Get Kubernetes Namespace - when { - api.getAzureKubernetesNamespace(any, any) - } thenAnswer { invocation => - val resourceId = invocation.getArgument[UUID](1) - new AzureKubernetesNamespaceResource() - .metadata( - new ResourceMetadata().resourceId(resourceId).name("namespace") - ) - .attributes( - new AzureKubernetesNamespaceAttributes() - .kubernetesNamespace("ns") - .databases(List("db1").asJava) - .managedIdentity("id1") - .kubernetesServiceAccount("ksa-1") - ) - } - // Delete Kubernetes Namespace - when { - api.deleteAzureKubernetesNamespace(any, any, any) - } thenAnswer { invocation => - val request = invocation.getArgument[DeleteControlledAzureResourceRequest](0) - if (namespaceExists) - new DeleteControlledAzureResourceResult().jobReport( - new JobReport().status(JobReport.StatusEnum.SUCCEEDED).id(request.getJobControl.getId) - ) - else throw new TestException() - } - // Get delete Kubernetes Namespace job - when { - api.getDeleteAzureKubernetesNamespaceResult(any, any) - } thenReturn { - new DeleteControlledAzureResourceResult().jobReport(new JobReport().status(JobReport.StatusEnum.SUCCEEDED)) - } - - // "ns-name" workspace database resource - - when { - resourceApi.getResourceByName( - workspaceId.value, - "cromwellmetadata" - ) - }.thenReturn( - new ResourceDescription() - .metadata(new ResourceMetadata().name("cromwellmetadata").resourceId(java.util.UUID.randomUUID())) - .resourceAttributes( - new ResourceAttributesUnion().azureDatabase( - new AzureDatabaseAttributes().databaseName("cromwellmetadata_abcxyz") - ) - ) - ) - - // workspace database resource for a workspace with cloned db - when { - resourceApi.getResourceByName( - workspaceIdForCloning.value, - "cbas" - ) - }.thenReturn { - new ResourceDescription() - .metadata(new ResourceMetadata().name("cbas").resourceId(java.util.UUID.randomUUID())) - .resourceAttributes( - new ResourceAttributesUnion().azureDatabase( - new AzureDatabaseAttributes().databaseName("cbas_cloned_db_abcxyz") - ) - ) - } - // workspace database resource for an updating app - when { - resourceApi.getResourceByName( - workspaceIdForUpdating.value, - "cbas" - ) - }.thenReturn { - new ResourceDescription() - .metadata(new ResourceMetadata().name("cbas").resourceId(cbasUuidForUpdateApp)) - .resourceAttributes( - new ResourceAttributesUnion().azureDatabase( - new AzureDatabaseAttributes().databaseName("cbas_db_abcxyz") - ) - ) - - } - - when { - resourceApi.getResourceByName( - workspaceIdForUpdating.value, - "cromwellmetadata" - ) - }.thenReturn { - new ResourceDescription() - .metadata( - new ResourceMetadata().name("cromwellmetadata").resourceId(cromwellmetadataUuidForUpdateApp) - ) - .resourceAttributes( - new ResourceAttributesUnion().azureDatabase( - new AzureDatabaseAttributes().databaseName("cromwellmetadata_abcxyz") - ) - ) - - } - - // getAzureDatabase for an updating app - when { - api.getAzureDatabase(ArgumentMatchers.eq(workspaceIdForUpdating.value), ArgumentMatchers.eq(cbasUuidForUpdateApp)) - } thenReturn { - new AzureDatabaseResource() - .metadata( - new ResourceMetadata() - .resourceId(cbasUuidForUpdateApp) - .name("cbas") - ) - .attributes( - new AzureDatabaseAttributes() - .allowAccessForAllWorkspaceUsers(true) - .databaseName("cbas_db_abcxyz") - ) - } - when { - api.getAzureDatabase(ArgumentMatchers.eq(workspaceIdForUpdating.value), - ArgumentMatchers.eq(cromwellmetadataUuidForUpdateApp) - ) - } thenReturn { - new AzureDatabaseResource() - .metadata( - new ResourceMetadata() - .resourceId(cromwellmetadataUuidForUpdateApp) - .name("cromwellmetadata") - ) - .attributes( - new AzureDatabaseAttributes() - .allowAccessForAllWorkspaceUsers(true) - .databaseName("cromwellmetadata_db_abcxyz") - ) - } - // workspace managed identity resource - when { - resourceApi.getResourceByName(workspaceId.value, s"idworkflows_app") - }.thenReturn { - new ResourceDescription() - .metadata(new ResourceMetadata().name(s"idworkflows_app").resourceId(java.util.UUID.randomUUID())) - .resourceAttributes( - new ResourceAttributesUnion().azureManagedIdentity( - new AzureManagedIdentityAttributes().managedIdentityName("abcxyz") - ) - ) - } - - // workspace namespace resource - when { - resourceApi.getResourceByName(workspaceId.value, s"ns-name-${workspaceId.value.toString}") - }.thenReturn { - new ResourceDescription() - .resourceAttributes( - new ResourceAttributesUnion().azureKubernetesNamespace( - new AzureKubernetesNamespaceAttributes().kubernetesNamespace("ns-name") - ) - ) - .metadata(new ResourceMetadata().name(s"ns-name-${workspaceId.value.toString}")) - } - when { - workspaceApi.getWorkspace(ArgumentMatchers.eq(workspaceId.value), any) - } thenReturn { - new bio.terra.workspace.model.WorkspaceDescription().createdDate(workspaceCreatedDate); - } - - when { - workspaceApi.getWorkspace(ArgumentMatchers.eq(workspaceIdForUpdating.value), any) - } thenReturn { - new bio.terra.workspace.model.WorkspaceDescription().createdDate(workspaceCreatedDate); - } - - val wsm = new MockWsmClientProvider(api, resourceApi, workspaceApi) - - (wsm, api, resourceApi, workspaceApi) - - } - - private def setUpMockAppInstall(checkStatus: Boolean = true): AppInstall[IO] = { - val appInstall = mock[AppInstall[IO]] - when { - appInstall.databases - } thenReturn List(ControlledDatabase("db1")) - when { - appInstall.buildHelmOverrideValues(any)(any) - } thenReturn IO.pure(Values("values")) - when { - appInstall.checkStatus(any, any)(any) - } thenReturn IO.pure(checkStatus) - appInstall - } - - private def setUpMockWorkflowAppInstall(checkStatus: Boolean = true): AppInstall[IO] = { - val mockWorkflowsAppInstall = mock[WorkflowsAppInstall[IO]] - - when(mockWorkflowsAppInstall.databases) thenReturn - List( - ControlledDatabase("cbas", cloningInstructions = CloningInstructionsEnum.RESOURCE), - ControlledDatabase("cromwellmetadata", allowAccessForAllWorkspaceUsers = true) - ) - when { - mockWorkflowsAppInstall.buildHelmOverrideValues(any)(any) - } thenReturn IO.pure(Values("values")) - when { - mockWorkflowsAppInstall.checkStatus(any, any)(any) - } thenReturn IO.pure(checkStatus) - - mockWorkflowsAppInstall - } - - private def setUpMockSamAuthProvider: SamAuthProvider[IO] = { - val mockSamAuth = mock[SamAuthProvider[IO]] - - when { - mockSamAuth.getLeoAuthToken - } thenReturn IO.pure(tokenValue) - mockSamAuth - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala index e45fed5257..8942f5d29d 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala @@ -18,7 +18,7 @@ import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.PrivateAzureStorageAccountSamResourceId import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext import org.broadinstitute.dsde.workbench.leonardo.config.ApplicationConfig -import org.broadinstitute.dsde.workbench.leonardo.dao.{WsmApiClientProvider, _} +import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.http.{ConfigReader, _} import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage._ @@ -26,7 +26,6 @@ import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageErr import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} import org.broadinstitute.dsde.workbench.util2.InstanceName -import org.broadinstitute.dsp.HelmException import org.http4s.headers.Authorization import org.mockito.ArgumentMatchers.{any, eq => mockitoEq} import org.mockito.Mockito.{spy, times, verify, when} @@ -1505,46 +1504,6 @@ class AzurePubsubHandlerSpec res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "handle AKS errors" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - - val failAksInterp = new MockAKSInterp { - override def createAndPollApp(params: CreateAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = - IO.raiseError(HelmException("something went wrong")) - - override def deleteApp(params: DeleteAKSAppParams)(implicit ev: Ask[IO, AppContext]): IO[Unit] = - IO.raiseError(HelmException("something went wrong")) - } - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, aksAlg = failAksInterp) - - val appId = AppId(42) - - val res = for { - ctx <- appContext.ask[AppContext] - result <- azureInterp - .createAndPollApp(appId, AppName("app"), WorkspaceId(UUID.randomUUID()), azureCloudContext, billingProfileId) - .attempt - } yield result shouldBe Left( - PubsubKubernetesError( - AppError( - s"Error creating Azure app with id ${appId.id} and cloudContext ${azureCloudContext.asString}: something went wrong", - ctx.now, - ErrorAction.CreateApp, - ErrorSource.App, - None, - Some(ctx.traceId) - ), - Some(appId), - false, - None, - None, - None - ) - ) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "delete azure disk properly" in isolatedDbTest { val queue = QueueFactory.asyncTaskQueue() @@ -1649,7 +1608,6 @@ class AzurePubsubHandlerSpec wsmDAO: WsmDao[IO] = new MockWsmDAO, welderDao: WelderDAO[IO] = new MockWelderDAO(), azureVmService: AzureVmService[IO] = FakeAzureVmService, - aksAlg: AKSAlgebra[IO] = new MockAKSInterp, wsmClient: WsmApiClientProvider[IO] = mockWsm, samDAO: SamDAO[IO] = new MockSamDAO() ): AzurePubsubHandlerAlgebra[IO] = @@ -1671,7 +1629,6 @@ class AzurePubsubHandlerSpec new MockJupyterDAO(), relayService, azureVmService, - aksAlg, refererConfig, wsmClient ) From b37b8ebf151207295d2dc0f0ef96b0251bdfe7e6 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 3 Jul 2025 16:09:00 -0400 Subject: [PATCH 10/43] Remove azure-only AppTypes and associated config --- .../workbench/leonardo/kubernetesModels.scala | 22 +- http/src/main/resources/leo.conf | 34 - http/src/main/resources/reference.conf | 189 +----- .../leonardo/config/KubernetesAppConfig.scala | 104 --- .../leonardo/http/ConfigReader.scala | 9 +- .../http/service/LeoAppServiceInterp.scala | 3 +- .../leonardo/util/BuildHelmChartValues.scala | 11 +- .../leonardo/util/GKEInterpreter.scala | 2 - .../leonardo/http/ConfigReaderSpec.scala | 158 +---- .../monitor/LeoMetricsMonitorSpec.scala | 628 ++++++++---------- .../util/BuildHelmChartValuesSpec.scala | 2 +- .../leonardo/provider/AppStateManager.scala | 2 +- 12 files changed, 290 insertions(+), 874 deletions(-) diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/kubernetesModels.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/kubernetesModels.scala index 7a21f5dcdf..0765b75e94 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/kubernetesModels.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/kubernetesModels.scala @@ -349,20 +349,6 @@ object AppType { case object Cromwell extends AppType { override def toString: String = "CROMWELL" } - case object WorkflowsApp extends AppType { - override def toString: String = "WORKFLOWS_APP" - } - case object CromwellRunnerApp extends AppType { - override def toString: String = "CROMWELL_RUNNER_APP" - } - - case object Wds extends AppType { - override def toString: String = "WDS" - } - - case object HailBatch extends AppType { - override def toString: String = "HAIL_BATCH" - } // See more context in https://docs.google.com/document/d/1RaQRMqAx7ymoygP6f7QVdBbZC-iD9oY_XLNMe_oz_cs/edit case object Allowed extends AppType { @@ -383,10 +369,10 @@ object AppType { */ def appTypeToFormattedByType(appType: AppType): FormattedBy = appType match { - case Galaxy => FormattedBy.Galaxy - case Custom => FormattedBy.Custom - case Allowed => FormattedBy.Allowed - case Cromwell | Wds | HailBatch | WorkflowsApp | CromwellRunnerApp => FormattedBy.Cromwell + case Galaxy => FormattedBy.Galaxy + case Custom => FormattedBy.Custom + case Allowed => FormattedBy.Allowed + case Cromwell => FormattedBy.Cromwell } } diff --git a/http/src/main/resources/leo.conf b/http/src/main/resources/leo.conf index b585337cfe..fce8712a83 100644 --- a/http/src/main/resources/leo.conf +++ b/http/src/main/resources/leo.conf @@ -191,40 +191,6 @@ azure { } } - hail-batch-app-config { - enabled = ${?HAIL_BATCH_APP_ENABLED} - } - - coa-app-config { - instrumentation-enabled = ${?COA_INSTRUMENTATION_ENABLED} - database-enabled = ${?COA_DATABASE_ENABLED} - dockstore-base-url = ${?DOCKSTORE_BASE_URL} - } - - workflows-app-config { - instrumentation-enabled = ${?WORKFLOWS_APP_INSTRUMENTATION_ENABLED} - enabled = ${?WORKFLOWS_APP_ENABLED} - dockstore-base-url = ${?DOCKSTORE_BASE_URL} - ecm-base-uri = ${?ECM_URL} - bard-base-uri = ${?BARD_URL} - bard-enabled = ${?BARD_ENABLED} - } - - cromwell-runner-app-config { - instrumentation-enabled = ${?CROMWELL_RUNNER_APP_INSTRUMENTATION_ENABLED} - enabled = ${?CROMWELL_RUNNER_APP_ENABLED} - ecm-base-uri = ${?ECM_URL} - bard-base-uri = ${?BARD_URL} - bard-enabled = ${?BARD_ENABLED} - } - - wds-app-config { - instrumentation-enabled = ${?WDS_INSTRUMENTATION_ENABLED} - database-enabled = ${?WDS_DATABASE_ENABLED} - environment = ${?APP_ENVIRONMENT} - environment-base = ${?APP_ENVIRONMENT_BASE} - } - wsm { uri = ${?WSM_URL} } diff --git a/http/src/main/resources/reference.conf b/http/src/main/resources/reference.conf index c4571650e4..f2ffcb8233 100644 --- a/http/src/main/resources/reference.conf +++ b/http/src/main/resources/reference.conf @@ -274,195 +274,8 @@ azure { managed-app-tenant-id = "" } - coa-app-config { - instrumentation-enabled = false - chart-name = "cromwell-helm/cromwell-on-azure" - chart-version = "0.2.523" - release-name-suffix = "coa-rls" - namespace-name-suffix = "coa-ns" - ksa-name = "coa-ksa" - dockstore-base-url = "https://staging.dockstore.org/" - # See https://github.com/broadinstitute/cromwhelm/blob/main/terra-batch-libchart/templates/_reverse-proxy.tpl#L81-L114 - # for list of services. - services = [ - { - name = "cbas" - kind = "ClusterIP" - }, - { - name = "cromwell" - kind = "ClusterIP" - } - ] - enabled = true - database-enabled = false - # App developers - Please keep the list of non-backward compatible versions in the list below - # All app versions excluded up until the switch to WSM-controlled KubernetesNamespace - # https://github.com/DataBiosphere/leonardo/pull/3666 - chart-versions-to-exclude-from-updates = [ - "0.2.341", - "0.2.338", - "0.2.334", - "0.2.332", - "0.2.328", - "0.2.291", - "0.2.277", - "0.2.276", - "0.2.268", - "0.2.265", - "0.2.263", - "0.2.251", - "0.2.242", - "0.2.239", - "0.2.237", - "0.2.232", - "0.2.231", - "0.2.229", - "0.2.225", - "0.2.223", - "0.2.220", - "0.2.219", - "0.2.218", - "0.2.217", - "0.2.216", - "0.2.215", - "0.2.213", - "0.2.212", - "0.2.211", - "0.2.210", - "0.2.209", - "0.2.204", - "0.2.201", - "0.2.199", - "0.2.197", - "0.2.195", - "0.2.192", - "0.2.191", - "0.2.187", - "0.2.184", - "0.2.179", - "0.2.160", - "0.2.159", - "0.2.148", - "0.2.39" - ] - } - - workflows-app-config { - instrumentation-enabled = false - chart-name = "terra-helm/workflows-app" - chart-version = "0.286.0" - release-name-suffix = "wfa-rls" - namespace-name-suffix = "wfa-ns" - ksa-name = "wfa-ksa" - dockstore-base-url = "https://staging.dockstore.org/" - services = [ - { - name = "cbas" - kind = "ClusterIP" - }, - { - name = "cromwell-reader" - kind = "ClusterIP" - path = "/cromwell" - } - ] - enabled = true - chart-versions-to-exclude-from-updates = [] - ecm-base-uri = "https://externalcreds.dsde-dev.broadinstitute.org" - bard-base-uri = "https://terra-bard-dev.appspot.com" - bard-enabled = false - } - - cromwell-runner-app-config { - instrumentation-enabled = false - chart-name = "terra-helm/cromwell-runner-app" - chart-version = "0.198.0" - release-name-suffix = "cra-rls" - namespace-name-suffix = "cra-ns" - ksa-name = "cra-ksa" - # See https://github.com/broadinstitute/cromwhelm/blob/main/terra-batch-libchart/templates/_reverse-proxy.tpl#L81-L114 - # for list of services. - services = [ - { - name = "cromwell-runner" - kind = "ClusterIP" - path = "/cromwell" - } - ] - enabled = true - chart-versions-to-exclude-from-updates = [] - ecm-base-uri = "https://externalcreds.dsde-dev.broadinstitute.org" - bard-base-uri = "https://terra-bard-dev.appspot.com" - bard-enabled = false - } - - wds-app-config { - environment = "dev" - environment-base = "live" - instrumentation-enabled = false - chart-name = "terra-helm/wds" - chart-version = "0.94.0" - release-name-suffix = "wds-rls" - namespace-name-suffix = "wds-ns" - ksa-name = "wds-ksa" - services = [ - { - name = "wds" - kind = "ClusterIP" - path = "/" - } - ] - enabled = true - database-enabled = false - # App developers - Please keep the list of non-backward compatible versions in the list below - # All app versions excluded up until the switch to WSM-controlled KubernetesNamespace - # https://github.com/DataBiosphere/leonardo/pull/3666 - chart-versions-to-exclude-from-updates = [ - "0.3.0", - "0.7.0", - "0.13.0", - "0.16.0", - "0.17.0", - "0.19.0", - "0.20.0", - "0.21.0", - "0.22.0", - "0.24.0", - "0.26.0", - "0.27.0", - "0.28.0", - "0.31.0", - "0.38.0", - "0.39.0", - "0.41.0", - "0.42.0", - "0.43.0" - ] - } - - hail-batch-app-config { - chart-name = "/leonardo/hail-batch-terra-azure" - chart-version = "0.2.0" - release-name-suffix = "hail-rls" - namespace-name-suffix = "hail-ns" - ksa-name = "hail-ksa" - services = [ - { - name = "batch" - kind = "ClusterIP" - } - ] - enabled = true - # App developers - Please keep the list of non-backward compatible versions in the list below - chart-versions-to-exclude-from-updates = [] - } - # App types which are allowed to launch with WORKSPACE_SHARED access scope. - allowed-shared-apps = [ - "WDS", - "WORKFLOWS_APP" - ] + allowed-shared-apps = [] listener-chart-config { chart-name = "terra-helm/listener" diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala index 070a300bd3..9a46cfe94d 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala @@ -3,11 +3,8 @@ package org.broadinstitute.dsde.workbench.leonardo.config import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceAccountName import org.broadinstitute.dsde.workbench.leonardo.AppType._ import org.broadinstitute.dsde.workbench.leonardo._ -import org.broadinstitute.dsde.workbench.leonardo.http.ConfigReader import org.broadinstitute.dsp.{ChartName, ChartVersion} -import java.net.URL - sealed trait KubernetesAppConfig extends Product with Serializable { def chartName: ChartName @@ -42,11 +39,6 @@ object KubernetesAppConfig { case (Custom, CloudProvider.Gcp) => Some(Config.gkeCustomAppConfig) case (Cromwell, CloudProvider.Gcp) => Some(Config.gkeCromwellAppConfig) case (AppType.Allowed, CloudProvider.Gcp) => Some(Config.gkeAllowedAppConfig) - case (Cromwell, CloudProvider.Azure) => Some(ConfigReader.appConfig.azure.coaAppConfig) - case (WorkflowsApp, CloudProvider.Azure) => Some(ConfigReader.appConfig.azure.workflowsAppConfig) - case (CromwellRunnerApp, CloudProvider.Azure) => Some(ConfigReader.appConfig.azure.cromwellRunnerAppConfig) - case (Wds, CloudProvider.Azure) => Some(ConfigReader.appConfig.azure.wdsAppConfig) - case (HailBatch, CloudProvider.Azure) => Some(ConfigReader.appConfig.azure.hailBatchAppConfig) case _ => None } } @@ -107,102 +99,6 @@ final case class CustomAppConfig(chartName: ChartName, val appType: AppType = AppType.Custom } -final case class CoaAppConfig(chartName: ChartName, - chartVersion: ChartVersion, - releaseNameSuffix: ReleaseNameSuffix, - namespaceNameSuffix: NamespaceNameSuffix, - ksaName: KsaName, - services: List[ServiceConfig], - instrumentationEnabled: Boolean, - enabled: Boolean, - dockstoreBaseUrl: URL, - databaseEnabled: Boolean, - chartVersionsToExcludeFromUpdates: List[ChartVersion] -) extends KubernetesAppConfig { - override val kubernetesServices: List[KubernetesService] = services.map(s => KubernetesService(ServiceId(-1), s)) - - override val serviceAccountName = ServiceAccountName(ksaName.value) - - val cloudProvider: CloudProvider = CloudProvider.Azure - val appType: AppType = AppType.Cromwell -} - -final case class WorkflowsAppConfig(chartName: ChartName, - chartVersion: ChartVersion, - releaseNameSuffix: ReleaseNameSuffix, - namespaceNameSuffix: NamespaceNameSuffix, - ksaName: KsaName, - services: List[ServiceConfig], - instrumentationEnabled: Boolean, - enabled: Boolean, - dockstoreBaseUrl: URL, - chartVersionsToExcludeFromUpdates: List[ChartVersion], - ecmBaseUri: URL, - bardBaseUri: URL, - bardEnabled: Boolean -) extends KubernetesAppConfig { - override lazy val kubernetesServices: List[KubernetesService] = services.map(s => KubernetesService(ServiceId(-1), s)) - override val serviceAccountName = ServiceAccountName(ksaName.value) - - val cloudProvider: CloudProvider = CloudProvider.Azure - val appType: AppType = AppType.WorkflowsApp -} - -final case class CromwellRunnerAppConfig(chartName: ChartName, - chartVersion: ChartVersion, - releaseNameSuffix: ReleaseNameSuffix, - namespaceNameSuffix: NamespaceNameSuffix, - ksaName: KsaName, - services: List[ServiceConfig], - instrumentationEnabled: Boolean, - enabled: Boolean, - chartVersionsToExcludeFromUpdates: List[ChartVersion], - ecmBaseUri: URL, - bardBaseUri: URL, - bardEnabled: Boolean -) extends KubernetesAppConfig { - override lazy val kubernetesServices: List[KubernetesService] = services.map(s => KubernetesService(ServiceId(-1), s)) - override val serviceAccountName = ServiceAccountName(ksaName.value) - val cloudProvider: CloudProvider = CloudProvider.Azure - val appType: AppType = AppType.CromwellRunnerApp -} - -final case class WdsAppConfig(chartName: ChartName, - chartVersion: ChartVersion, - releaseNameSuffix: ReleaseNameSuffix, - namespaceNameSuffix: NamespaceNameSuffix, - ksaName: KsaName, - services: List[ServiceConfig], - instrumentationEnabled: Boolean, - enabled: Boolean, - databaseEnabled: Boolean, - environment: String, - environmentBase: String, - chartVersionsToExcludeFromUpdates: List[ChartVersion] -) extends KubernetesAppConfig { - override lazy val kubernetesServices: List[KubernetesService] = services.map(s => KubernetesService(ServiceId(-1), s)) - override val serviceAccountName = ServiceAccountName(ksaName.value) - - val cloudProvider: CloudProvider = CloudProvider.Azure - val appType: AppType = AppType.Wds -} - -final case class HailBatchAppConfig(chartName: ChartName, - chartVersion: ChartVersion, - releaseNameSuffix: ReleaseNameSuffix, - namespaceNameSuffix: NamespaceNameSuffix, - ksaName: KsaName, - services: List[ServiceConfig], - enabled: Boolean, - chartVersionsToExcludeFromUpdates: List[ChartVersion] -) extends KubernetesAppConfig { - override val kubernetesServices: List[KubernetesService] = services.map(s => KubernetesService(ServiceId(-1), s)) - override val serviceAccountName = ServiceAccountName(ksaName.value) - - val cloudProvider: CloudProvider = CloudProvider.Azure - val appType: AppType = AppType.HailBatch -} - final case class ContainerRegistryUsername(asString: String) extends AnyVal final case class ContainerRegistryPassword(asString: String) extends AnyVal final case class ContainerRegistryCredentials(username: ContainerRegistryUsername, password: ContainerRegistryPassword) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala index 7551e68c16..c3efbf6071 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala @@ -1,16 +1,16 @@ package org.broadinstitute.dsde.workbench.leonardo package http +import _root_.pureconfig.generic.auto._ import org.broadinstitute.dsde.workbench.azure.AzureAppRegistrationConfig import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName +import org.broadinstitute.dsde.workbench.leonardo.ConfigImplicits._ import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetricsMonitorConfig import org.broadinstitute.dsde.workbench.leonardo.util.{AzurePubsubHandlerConfig, TerraAppSetupChartConfig} import org.broadinstitute.dsp.{ChartName, ChartVersion} import org.http4s.Uri import pureconfig.ConfigSource -import _root_.pureconfig.generic.auto._ -import ConfigImplicits._ object ConfigReader { lazy val appConfig = @@ -23,11 +23,6 @@ final case class AzureConfig( wsm: HttpWsmDaoConfig, bpm: BpmConfig, appRegistration: AzureAppRegistrationConfig, - coaAppConfig: CoaAppConfig, - cromwellRunnerAppConfig: CromwellRunnerAppConfig, - workflowsAppConfig: WorkflowsAppConfig, - wdsAppConfig: WdsAppConfig, - hailBatchAppConfig: HailBatchAppConfig, allowedSharedApps: List[AppType], tdr: TdrConfig, listenerChartConfig: ListenerChartConfig, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala index 7acf468b11..3a8477afcb 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala @@ -80,8 +80,7 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, enableIntraNodeVisibility = req.labels.get(AOU_UI_LABEL).exists(x => x == "true") _ <- req.appType match { - case AppType.Galaxy | AppType.HailBatch | AppType.Wds | AppType.Cromwell | AppType.WorkflowsApp | - AppType.CromwellRunnerApp => + case AppType.Galaxy | AppType.Cromwell => F.unit case AppType.Allowed => req.allowedChartName match { diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala index 96b4faef9b..dfdbcef13b 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala @@ -257,17 +257,12 @@ private[leonardo] object BuildHelmChartValues { ): Values = { val relayTargetHost = appType match { case AppType.Cromwell => s"http://coa-${release.asString}-reverse-proxy-service:8000/" - case AppType.CromwellRunnerApp => s"http://cra-${release.asString}-reverse-proxy-service:8000/" - case AppType.Wds => s"http://wds-${release.asString}-wds-svc:8080" - case AppType.HailBatch => "http://batch:8080" - case AppType.WorkflowsApp => s"http://wfa-${release.asString}-reverse-proxy-service:8000/" case _ => "unknown" } - // Hail batch serves requests on /{appName}/batch and uses relative redirects, - // so requires that we don't strip the entity path. For other app types we do - // strip the entity path. - val removeEntityPathFromHttpUrl = appType != AppType.HailBatch + // Some apps may serve requests on endpoints like /{appName}/batch and use relative redirects, + // requiring that we don't strip the entity path. For all current app types we do strip the entity path. + val removeEntityPathFromHttpUrl = true // validHosts can have a different number of hosts, this pre-processes the list as separate chart values val validHostValues = validHosts.zipWithIndex.map { case (elem, idx) => diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/GKEInterpreter.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/GKEInterpreter.scala index 1adc0f50f1..eae0b15710 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/GKEInterpreter.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/GKEInterpreter.scala @@ -1213,8 +1213,6 @@ class GKEInterpreter[F[_]]( config.monitorConfig.startApp.interval ).interruptAfter(config.monitorConfig.startApp.interruptAfter).compile.lastOrError } yield last.isDone - case AppType.Wds | AppType.HailBatch | AppType.WorkflowsApp | AppType.CromwellRunnerApp => - F.raiseError(AppCreationException(s"App type ${dbApp.app.appType} not supported on GCP")) } _ <- diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala index 8a34bba89e..decd78df31 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala @@ -2,18 +2,16 @@ package org.broadinstitute.dsde.workbench.leonardo package http import com.azure.core.management.AzureEnvironment -import org.broadinstitute.dsde.workbench.azure.{AzureAppRegistrationConfig, AzureServiceBusPublisherConfig, AzureServiceBusSubscriberConfig, ClientId, ClientSecret, ManagedAppTenantId} -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName +import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.ZoneName import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoMetricsMonitorConfig, PollMonitorConfig} -import org.broadinstitute.dsde.workbench.leonardo.util.{AzurePubsubHandlerConfig, AzureRuntimeDefaults, CustomScriptExtensionConfig, TerraAppSetupChartConfig, VMCredential} +import org.broadinstitute.dsde.workbench.leonardo.util._ import org.broadinstitute.dsp._ import org.http4s.Uri import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import java.net.URL import scala.concurrent.duration._ class ConfigReaderSpec extends AnyFlatSpec with Matchers { @@ -76,157 +74,7 @@ class ConfigReaderSpec extends AnyFlatSpec with Matchers { HttpWsmDaoConfig(Uri.unsafeFromString("https://localhost:8000")), BpmConfig(Uri.unsafeFromString("https://localhost:8000")), AzureAppRegistrationConfig(ClientId(""), ClientSecret(""), ManagedAppTenantId("")), - CoaAppConfig( - ChartName("cromwell-helm/cromwell-on-azure"), - ChartVersion("0.2.523"), - ReleaseNameSuffix("coa-rls"), - NamespaceNameSuffix("coa-ns"), - KsaName("coa-ksa"), - List( - ServiceConfig(ServiceName("cbas"), KubernetesServiceKindName("ClusterIP")), - ServiceConfig(ServiceName("cromwell"), KubernetesServiceKindName("ClusterIP")) - ), - instrumentationEnabled = false, - enabled = true, - dockstoreBaseUrl = new URL("https://staging.dockstore.org/"), - databaseEnabled = false, - chartVersionsToExcludeFromUpdates = List( - ChartVersion("0.2.341"), - ChartVersion("0.2.338"), - ChartVersion("0.2.334"), - ChartVersion("0.2.332"), - ChartVersion("0.2.328"), - ChartVersion("0.2.291"), - ChartVersion("0.2.277"), - ChartVersion("0.2.276"), - ChartVersion("0.2.268"), - ChartVersion("0.2.265"), - ChartVersion("0.2.263"), - ChartVersion("0.2.251"), - ChartVersion("0.2.242"), - ChartVersion("0.2.239"), - ChartVersion("0.2.237"), - ChartVersion("0.2.232"), - ChartVersion("0.2.231"), - ChartVersion("0.2.229"), - ChartVersion("0.2.225"), - ChartVersion("0.2.223"), - ChartVersion("0.2.220"), - ChartVersion("0.2.219"), - ChartVersion("0.2.218"), - ChartVersion("0.2.217"), - ChartVersion("0.2.216"), - ChartVersion("0.2.215"), - ChartVersion("0.2.213"), - ChartVersion("0.2.212"), - ChartVersion("0.2.211"), - ChartVersion("0.2.210"), - ChartVersion("0.2.209"), - ChartVersion("0.2.204"), - ChartVersion("0.2.201"), - ChartVersion("0.2.199"), - ChartVersion("0.2.197"), - ChartVersion("0.2.195"), - ChartVersion("0.2.192"), - ChartVersion("0.2.191"), - ChartVersion("0.2.187"), - ChartVersion("0.2.184"), - ChartVersion("0.2.179"), - ChartVersion("0.2.160"), - ChartVersion("0.2.159"), - ChartVersion("0.2.148"), - ChartVersion("0.2.39") - ) - ), - CromwellRunnerAppConfig( - ChartName("terra-helm/cromwell-runner-app"), - ChartVersion("0.198.0"), - ReleaseNameSuffix("cra-rls"), - NamespaceNameSuffix("cra-ns"), - KsaName("cra-ksa"), - List( - ServiceConfig(ServiceName("cromwell-runner"), - KubernetesServiceKindName("ClusterIP"), - Some(ServicePath("/cromwell")) - ) - ), - instrumentationEnabled = false, - enabled = true, - chartVersionsToExcludeFromUpdates = List.empty, - ecmBaseUri = new URL("https://externalcreds.dsde-dev.broadinstitute.org"), - bardBaseUri = new URL("https://terra-bard-dev.appspot.com"), - bardEnabled = false - ), - WorkflowsAppConfig( - ChartName("terra-helm/workflows-app"), - ChartVersion("0.286.0"), - ReleaseNameSuffix("wfa-rls"), - NamespaceNameSuffix("wfa-ns"), - KsaName("wfa-ksa"), - List( - ServiceConfig(ServiceName("cbas"), KubernetesServiceKindName("ClusterIP")), - ServiceConfig(ServiceName("cromwell-reader"), - KubernetesServiceKindName("ClusterIP"), - Some(ServicePath("/cromwell")) - ) - ), - instrumentationEnabled = false, - enabled = true, - dockstoreBaseUrl = new URL("https://staging.dockstore.org/"), - chartVersionsToExcludeFromUpdates = List.empty, - ecmBaseUri = new URL("https://externalcreds.dsde-dev.broadinstitute.org"), - bardBaseUri = new URL("https://terra-bard-dev.appspot.com"), - bardEnabled = false - ), - WdsAppConfig( - ChartName("terra-helm/wds"), - ChartVersion("0.94.0"), - ReleaseNameSuffix("wds-rls"), - NamespaceNameSuffix("wds-ns"), - KsaName("wds-ksa"), - List( - ServiceConfig(ServiceName("wds"), KubernetesServiceKindName("ClusterIP"), Some(ServicePath("/"))) - ), - instrumentationEnabled = false, - enabled = true, - databaseEnabled = false, - environment = "dev", - environmentBase = "live", - chartVersionsToExcludeFromUpdates = List( - ChartVersion("0.3.0"), - ChartVersion("0.7.0"), - ChartVersion("0.13.0"), - ChartVersion("0.16.0"), - ChartVersion("0.17.0"), - ChartVersion("0.19.0"), - ChartVersion("0.20.0"), - ChartVersion("0.21.0"), - ChartVersion("0.22.0"), - ChartVersion("0.24.0"), - ChartVersion("0.26.0"), - ChartVersion("0.27.0"), - ChartVersion("0.28.0"), - ChartVersion("0.31.0"), - ChartVersion("0.38.0"), - ChartVersion("0.39.0"), - ChartVersion("0.41.0"), - ChartVersion("0.42.0"), - ChartVersion("0.43.0") - ) - ), - HailBatchAppConfig( - ChartName("/leonardo/hail-batch-terra-azure"), - ChartVersion("0.2.0"), - ReleaseNameSuffix("hail-rls"), - NamespaceNameSuffix("hail-ns"), - KsaName("hail-ksa"), - List( - ServiceConfig(ServiceName("batch"), KubernetesServiceKindName("ClusterIP")) - ), - false, - chartVersionsToExcludeFromUpdates = List() - ), - List(AppType.Wds, AppType.WorkflowsApp), + List(), TdrConfig("https://jade.datarepo-dev.broadinstitute.org"), ListenerChartConfig(ChartName("terra-helm/listener"), ChartVersion("0.3.0")), AzureHostingModeConfig( diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index 496c13cf57..28ff75a446 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -106,18 +106,7 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test "LeoMetricsMonitor" should "count apps by status" in { val test = leoMetricsMonitor.countAppsByDbStatus(allApps) // 10 apps - test.size shouldBe 10 - // Cromwell on Azure - test.get( - AppStatusMetric(CloudProvider.Azure, - AppType.Cromwell, - AppStatus.Running, - RuntimeUI.Terra, - Some(azureContext), - cromwellOnAzureChart, - true - ) - ) shouldBe Some(1) + test.size shouldBe 5 // Cromwell on GCP on Terra test.get( AppStatusMetric(CloudProvider.Gcp, @@ -145,50 +134,6 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test test.get( AppStatusMetric(CloudProvider.Gcp, AppType.Allowed, AppStatus.Running, RuntimeUI.AoU, None, rstudioChart, true) ) shouldBe Some(1) - // Hail Batch on Azure - test.get( - AppStatusMetric(CloudProvider.Azure, - AppType.HailBatch, - AppStatus.Running, - RuntimeUI.Terra, - Some(azureContext), - hailBatchChart, - true - ) - ) shouldBe Some(1) - // WDS on Azure - test.get( - AppStatusMetric(CloudProvider.Azure, - AppType.Wds, - AppStatus.Running, - RuntimeUI.Terra, - Some(azureContext2), - wdsChart, - true - ) - ) shouldBe Some(1) - // Workflows App on Azure - test.get( - AppStatusMetric(CloudProvider.Azure, - AppType.WorkflowsApp, - AppStatus.Running, - RuntimeUI.Terra, - Some(azureContext2), - workflowsAppChart, - true - ) - ) shouldBe Some(1) - // Cromwell Runner App on Azure - test.get( - AppStatusMetric(CloudProvider.Azure, - AppType.CromwellRunnerApp, - AppStatus.Running, - RuntimeUI.Terra, - Some(azureContext2), - cromwellRunnerAppChart, - true - ) - ) shouldBe Some(1) } it should "count runtimes by status" in { @@ -237,106 +182,106 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test ) shouldBe Some(1) } - it should "health check apps" in { - val test = - leoMetricsMonitor - .countAppsByHealth(List(cromwellAppAzure, galaxyAppGcp, workflowsApp, cromwellRunnerApp)) - .unsafeRunSync()(IORuntime.global) - // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy - test.size shouldBe 12 - List("cromwell", "cbas").foreach { s => - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.Cromwell, - ServiceName(s), - RuntimeUI.Terra, - Some(azureContext), - s != "cbas", - cromwellOnAzureChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.Cromwell, - ServiceName(s), - RuntimeUI.Terra, - Some(azureContext), - s == "cbas", - cromwellOnAzureChart, - true - ) - ) shouldBe Some(0) - } - test.get( - AppHealthMetric(CloudProvider.Gcp, - AppType.Galaxy, - ServiceName("galaxy"), - RuntimeUI.Terra, - None, - true, - galaxyChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Gcp, - AppType.Galaxy, - ServiceName("galaxy"), - RuntimeUI.Terra, - None, - false, - galaxyChart, - true - ) - ) shouldBe Some(0) - List("cromwell-reader", "cbas").foreach { s => - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.WorkflowsApp, - ServiceName(s), - RuntimeUI.Terra, - Some(azureContext2), - s != "cbas", - workflowsAppChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.WorkflowsApp, - ServiceName(s), - RuntimeUI.Terra, - Some(azureContext2), - s == "cbas", - workflowsAppChart, - true - ) - ) shouldBe Some(0) - } - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.CromwellRunnerApp, - ServiceName("cromwell-runner"), - RuntimeUI.Terra, - Some(azureContext2), - true, - cromwellRunnerAppChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.CromwellRunnerApp, - ServiceName("cromwell-runner"), - RuntimeUI.Terra, - Some(azureContext2), - false, - cromwellRunnerAppChart, - true - ) - ) shouldBe Some(0) - } +// it should "health check apps" in { +// val test = +// leoMetricsMonitor +// .countAppsByHealth(List(galaxyAppGcp)) +// .unsafeRunSync()(IORuntime.global) +// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy +// test.size shouldBe 12 +// List("cromwell", "cbas").foreach { s => +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.Cromwell, +// ServiceName(s), +// RuntimeUI.Terra, +// Some(azureContext), +// s != "cbas", +// cromwellOnAzureChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.Cromwell, +// ServiceName(s), +// RuntimeUI.Terra, +// Some(azureContext), +// s == "cbas", +// cromwellOnAzureChart, +// true +// ) +// ) shouldBe Some(0) +// } +// test.get( +// AppHealthMetric(CloudProvider.Gcp, +// AppType.Galaxy, +// ServiceName("galaxy"), +// RuntimeUI.Terra, +// None, +// true, +// galaxyChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Gcp, +// AppType.Galaxy, +// ServiceName("galaxy"), +// RuntimeUI.Terra, +// None, +// false, +// galaxyChart, +// true +// ) +// ) shouldBe Some(0) +// List("cromwell-reader", "cbas").foreach { s => +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.WorkflowsApp, +// ServiceName(s), +// RuntimeUI.Terra, +// Some(azureContext2), +// s != "cbas", +// workflowsAppChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.WorkflowsApp, +// ServiceName(s), +// RuntimeUI.Terra, +// Some(azureContext2), +// s == "cbas", +// workflowsAppChart, +// true +// ) +// ) shouldBe Some(0) +// } +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.CromwellRunnerApp, +// ServiceName("cromwell-runner"), +// RuntimeUI.Terra, +// Some(azureContext2), +// true, +// cromwellRunnerAppChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.CromwellRunnerApp, +// ServiceName("cromwell-runner"), +// RuntimeUI.Terra, +// Some(azureContext2), +// false, +// cromwellRunnerAppChart, +// true +// ) +// ) shouldBe Some(0) +// } it should "health check runtimes" in { val test = leoMetricsMonitor.countRuntimesByHealth(List(jupyterAzure, rstudioGcp)).unsafeRunSync()(IORuntime.global) @@ -362,178 +307,178 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test } } - it should "not include AzureCloudContext if disabled" in { - val config = LeoMetricsMonitorConfig(true, 1 minute, false) - val azureDisabledMetricsMonitor = new LeoMetricsMonitor[IO]( - config, - appDAO, - wdsDAO, - cbasDAO, - cromwellDAO, - hailBatchDAO, - relayListenerDAO, - samDAO, - kube, - containerService - ) - val test = - azureDisabledMetricsMonitor - .countAppsByHealth(List(cromwellAppAzure, galaxyAppGcp, workflowsApp, cromwellRunnerApp)) - .unsafeRunSync()(IORuntime.global) - // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy - test.size shouldBe 12 - List("cromwell", "cbas").foreach { s => - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.Cromwell, - ServiceName(s), - RuntimeUI.Terra, - None, - s != "cbas", - cromwellOnAzureChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.Cromwell, - ServiceName(s), - RuntimeUI.Terra, - None, - s == "cbas", - cromwellOnAzureChart, - true - ) - ) shouldBe Some(0) - } - test.get( - AppHealthMetric(CloudProvider.Gcp, - AppType.Galaxy, - ServiceName("galaxy"), - RuntimeUI.Terra, - None, - true, - galaxyChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Gcp, - AppType.Galaxy, - ServiceName("galaxy"), - RuntimeUI.Terra, - None, - false, - galaxyChart, - true - ) - ) shouldBe Some(0) - List("cromwell-reader", "cbas").foreach { s => - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.WorkflowsApp, - ServiceName(s), - RuntimeUI.Terra, - None, - s != "cbas", - workflowsAppChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.WorkflowsApp, - ServiceName(s), - RuntimeUI.Terra, - None, - s == "cbas", - workflowsAppChart, - true - ) - ) shouldBe Some(0) - } - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.CromwellRunnerApp, - ServiceName("cromwell-runner"), - RuntimeUI.Terra, - None, - true, - cromwellRunnerAppChart, - true - ) - ) shouldBe Some(1) - test.get( - AppHealthMetric(CloudProvider.Azure, - AppType.CromwellRunnerApp, - ServiceName("cromwell-runner"), - RuntimeUI.Terra, - None, - false, - cromwellRunnerAppChart, - true - ) - ) shouldBe Some(0) - } - - it should "record nodepool size" in { - val test = leoMetricsMonitor.getNodepoolSize(List(wdsAppAzure, hailBatchAppAzure)).unsafeRunSync()(IORuntime.global) - test.size shouldBe 4 - test.get(NodepoolSizeMetric(azureContext, "pool1")) shouldBe Some(10) - test.get(NodepoolSizeMetric(azureContext, "pool2")) shouldBe Some(1) - test.get(NodepoolSizeMetric(azureContext2, "pool1")) shouldBe Some(10) - test.get(NodepoolSizeMetric(azureContext2, "pool2")) shouldBe Some(1) - } - - it should "record app k8s metrics" in { - val chart = Chart.fromString("wds-0.0.1").get - val test = leoMetricsMonitor.getAppK8sResources(List(wdsAppAzure)).unsafeRunSync()(IORuntime.global) - test.size shouldBe 4 - test.get( - AppResourcesMetric(CloudProvider.Azure, - AppType.Wds, - ServiceName("wds"), - RuntimeUI.Terra, - Some(azureContext2), - "request", - "cpu", - chart - ) - ) shouldBe Some(1) - test.get( - AppResourcesMetric(CloudProvider.Azure, - AppType.Wds, - ServiceName("wds"), - RuntimeUI.Terra, - Some(azureContext2), - "request", - "memory", - chart - ) - ) shouldBe Some(1073741824d) - test.get( - AppResourcesMetric(CloudProvider.Azure, - AppType.Wds, - ServiceName("wds"), - RuntimeUI.Terra, - Some(azureContext2), - "limit", - "cpu", - chart - ) - ) shouldBe Some(2) - test.get( - AppResourcesMetric(CloudProvider.Azure, - AppType.Wds, - ServiceName("wds"), - RuntimeUI.Terra, - Some(azureContext2), - "limit", - "memory", - chart - ) - ) shouldBe Some(2147483648d) - } +// it should "not include AzureCloudContext if disabled" in { +// val config = LeoMetricsMonitorConfig(true, 1 minute, false) +// val azureDisabledMetricsMonitor = new LeoMetricsMonitor[IO]( +// config, +// appDAO, +// wdsDAO, +// cbasDAO, +// cromwellDAO, +// hailBatchDAO, +// relayListenerDAO, +// samDAO, +// kube, +// containerService +// ) +// val test = +// azureDisabledMetricsMonitor +// .countAppsByHealth(List(cromwellAppAzure, galaxyAppGcp, workflowsApp, cromwellRunnerApp)) +// .unsafeRunSync()(IORuntime.global) +// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy +// test.size shouldBe 12 +// List("cromwell", "cbas").foreach { s => +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.Cromwell, +// ServiceName(s), +// RuntimeUI.Terra, +// None, +// s != "cbas", +// cromwellOnAzureChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.Cromwell, +// ServiceName(s), +// RuntimeUI.Terra, +// None, +// s == "cbas", +// cromwellOnAzureChart, +// true +// ) +// ) shouldBe Some(0) +// } +// test.get( +// AppHealthMetric(CloudProvider.Gcp, +// AppType.Galaxy, +// ServiceName("galaxy"), +// RuntimeUI.Terra, +// None, +// true, +// galaxyChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Gcp, +// AppType.Galaxy, +// ServiceName("galaxy"), +// RuntimeUI.Terra, +// None, +// false, +// galaxyChart, +// true +// ) +// ) shouldBe Some(0) +// List("cromwell-reader", "cbas").foreach { s => +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.WorkflowsApp, +// ServiceName(s), +// RuntimeUI.Terra, +// None, +// s != "cbas", +// workflowsAppChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.WorkflowsApp, +// ServiceName(s), +// RuntimeUI.Terra, +// None, +// s == "cbas", +// workflowsAppChart, +// true +// ) +// ) shouldBe Some(0) +// } +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.CromwellRunnerApp, +// ServiceName("cromwell-runner"), +// RuntimeUI.Terra, +// None, +// true, +// cromwellRunnerAppChart, +// true +// ) +// ) shouldBe Some(1) +// test.get( +// AppHealthMetric(CloudProvider.Azure, +// AppType.CromwellRunnerApp, +// ServiceName("cromwell-runner"), +// RuntimeUI.Terra, +// None, +// false, +// cromwellRunnerAppChart, +// true +// ) +// ) shouldBe Some(0) +// } + +// it should "record nodepool size" in { +// val test = leoMetricsMonitor.getNodepoolSize(List(wdsAppAzure, hailBatchAppAzure)).unsafeRunSync()(IORuntime.global) +// test.size shouldBe 4 +// test.get(NodepoolSizeMetric(azureContext, "pool1")) shouldBe Some(10) +// test.get(NodepoolSizeMetric(azureContext, "pool2")) shouldBe Some(1) +// test.get(NodepoolSizeMetric(azureContext2, "pool1")) shouldBe Some(10) +// test.get(NodepoolSizeMetric(azureContext2, "pool2")) shouldBe Some(1) +// } + +// it should "record app k8s metrics" in { +// val chart = Chart.fromString("wds-0.0.1").get +// val test = leoMetricsMonitor.getAppK8sResources(List(wdsAppAzure)).unsafeRunSync()(IORuntime.global) +// test.size shouldBe 4 +// test.get( +// AppResourcesMetric(CloudProvider.Azure, +// AppType.Wds, +// ServiceName("wds"), +// RuntimeUI.Terra, +// Some(azureContext2), +// "request", +// "cpu", +// chart +// ) +// ) shouldBe Some(1) +// test.get( +// AppResourcesMetric(CloudProvider.Azure, +// AppType.Wds, +// ServiceName("wds"), +// RuntimeUI.Terra, +// Some(azureContext2), +// "request", +// "memory", +// chart +// ) +// ) shouldBe Some(1073741824d) +// test.get( +// AppResourcesMetric(CloudProvider.Azure, +// AppType.Wds, +// ServiceName("wds"), +// RuntimeUI.Terra, +// Some(azureContext2), +// "limit", +// "cpu", +// chart +// ) +// ) shouldBe Some(2) +// test.get( +// AppResourcesMetric(CloudProvider.Azure, +// AppType.Wds, +// ServiceName("wds"), +// RuntimeUI.Terra, +// Some(azureContext2), +// "limit", +// "memory", +// chart +// ) +// ) shouldBe Some(2147483648d) +// } // Data generators @@ -573,9 +518,6 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test def genService(name: String): KubernetesService = KubernetesService(ServiceId(-1), ServiceConfig(ServiceName(name), KubernetesServiceKindName("ClusterIP"))) - private def cromwellAppAzure: KubernetesCluster = - genApp(true, AppType.Cromwell, cromwellOnAzureChart, false, true, false) - .copy(cloudContext = CloudContext.Azure(azureContext)) private def cromwellAppGcp: KubernetesCluster = genApp(false, AppType.Cromwell, cromwellChart, false, true, false) private def galaxyAppGcp: KubernetesCluster = @@ -586,41 +528,19 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test genApp(false, AppType.Cromwell, cromwellChart, true, true, false) private def rstudioAppGcpAou: KubernetesCluster = genApp(false, AppType.Allowed, rstudioChart, true, false, false) - private def hailBatchAppAzure: KubernetesCluster = - genApp(true, AppType.HailBatch, hailBatchChart, false, false, false) - .copy(cloudContext = CloudContext.Azure(azureContext)) - private def wdsAppAzure: KubernetesCluster = - genApp(true, AppType.Wds, wdsChart, false, false, false) - .copy(cloudContext = CloudContext.Azure(azureContext2)) - private def workflowsApp: KubernetesCluster = - genApp(true, AppType.WorkflowsApp, workflowsAppChart, false, false, true) - .copy(cloudContext = CloudContext.Azure(azureContext2)) - private def cromwellRunnerApp: KubernetesCluster = - genApp(true, AppType.CromwellRunnerApp, cromwellRunnerAppChart, false, false, false, true) - .copy(cloudContext = CloudContext.Azure(azureContext2)) private def cromwellChart = Chart.fromString("cromwell-0.0.1").get - private def cromwellOnAzureChart = Chart.fromString("cromwell-on-azure-0.0.1").get private def galaxyChart = Chart.fromString("galaxy-0.0.1").get private def customChart = Chart.fromString("custom-0.0.1").get private def rstudioChart = Chart.fromString("rstudio-0.0.1").get - private def hailBatchChart = Chart.fromString("hail-batch-0.1.0").get - private def wdsChart = Chart.fromString("wds-0.0.1").get - private def workflowsAppChart = Chart.fromString("workflows-app-0.0.1").get - private def cromwellRunnerAppChart = Chart.fromString("cromwell-runner-app-0.0.1").get private def allApps = List( - cromwellAppAzure, cromwellAppGcp, galaxyAppGcp, customAppGcp, cromwellAppGcpAou, - rstudioAppGcpAou, - hailBatchAppAzure, - wdsAppAzure, - workflowsApp, - cromwellRunnerApp + rstudioAppGcpAou ) private def genRuntime(isJupyter: Boolean, isAou: Boolean, isGcp: Boolean): RuntimeMetrics = diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala index 19c2849fec..ca4bcbacba 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala @@ -551,7 +551,7 @@ class BuildHelmChartValuesSpec extends AnyFlatSpecLike with LeonardoTestSuite { RelayNamespace("relay-ns"), RelayHybridConnectionName("hc-name"), PrimaryKey("hc-name"), - AppType.Wds, + AppType.Cromwell, workspaceId, AppName("app1"), Set("example.com", "foo.com", "bar.org"), diff --git a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/AppStateManager.scala b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/AppStateManager.scala index eebbbc2143..b4ffa70541 100644 --- a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/AppStateManager.scala +++ b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/AppStateManager.scala @@ -54,7 +54,7 @@ object AppStateManager { Some(DiskName("exampleDiskName")), Map.empty[String, String], AuditInfo(WorkbenchEmail(""), Instant.now(), None, Instant.now()), - AppType.CromwellRunnerApp, + AppType.Cromwell, ChartName(""), None, Map.empty[String, String], From 65cba353aa2d247ce467532b9dca99a1aca8de22 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 3 Jul 2025 16:31:56 -0400 Subject: [PATCH 11/43] Remove app DAOs --- .../dsde/workbench/leonardo/dao/CbasDAO.scala | 12 --- .../workbench/leonardo/dao/CromwellDAO.scala | 12 --- .../workbench/leonardo/dao/HailBatchDAO.scala | 25 ------- .../workbench/leonardo/dao/HttpCbasDAO.scala | 66 ----------------- .../leonardo/dao/HttpCromwellDAO.scala | 73 ------------------- .../leonardo/dao/HttpHailBatchDAO.scala | 46 ------------ .../workbench/leonardo/dao/HttpWdsDAO.scala | 66 ----------------- .../dsde/workbench/leonardo/dao/WdsDAO.scala | 12 --- .../http/AppDependenciesBuilder.scala | 6 +- .../http/BaselineDependenciesBuilder.scala | 19 ----- .../leonardo/monitor/LeoMetricsMonitor.scala | 9 --- .../leonardo/dao/HttpCbasDAOSpec.scala | 72 ------------------ .../leonardo/dao/HttpCromwellDAOSpec.scala | 72 ------------------ .../leonardo/dao/HttpHailBatchDAOSpec.scala | 54 -------------- .../leonardo/dao/HttpWdsDAOSpec.scala | 52 ------------- .../http/GcpDependenciesBuilderSpec.scala | 4 - .../monitor/LeoMetricsMonitorSpec.scala | 44 ----------- 17 files changed, 1 insertion(+), 643 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CbasDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CromwellDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HailBatchDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WdsDAO.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAOSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAOSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAOSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAOSpec.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CbasDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CbasDAO.scala deleted file mode 100644 index 63806b9310..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CbasDAO.scala +++ /dev/null @@ -1,12 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.http4s.Uri -import org.http4s.headers.Authorization - -trait CbasDAO[F[_]] { - def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CromwellDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CromwellDAO.scala deleted file mode 100644 index 358b0668e2..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/CromwellDAO.scala +++ /dev/null @@ -1,12 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.http4s.Uri -import org.http4s.headers.Authorization - -trait CromwellDAO[F[_]] { - def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HailBatchDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HailBatchDAO.scala deleted file mode 100644 index 221aad1dcc..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HailBatchDAO.scala +++ /dev/null @@ -1,25 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.http4s.Uri -import org.http4s.headers.Authorization - -/** - * Client for Hail Batch app. - * The Hail Batch app exposes 2 services: batch and batch-driver. They are both fronted - * by a reverse proxy. The batch container serves the UI and user requests; the batch-driver - * is responsible for provisioning and monitoring compute nodes. - */ -trait HailBatchDAO[F[_]] { - - /** Status of the batch container. */ - def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] - - /** Status of the batch-driver container. */ - def getDriverStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAO.scala deleted file mode 100644 index 5b17871f45..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAO.scala +++ /dev/null @@ -1,66 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import akka.http.scaladsl.model.StatusCode -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import io.circe._ -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.broadinstitute.dsde.workbench.leonardo.model.LeoException -import org.broadinstitute.dsde.workbench.model.TraceId -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s._ -import org.http4s.circe.CirceEntityCodec.circeEntityDecoder -import org.http4s.client.Client -import org.http4s.client.dsl.Http4sClientDsl -import org.http4s.headers.Authorization -import org.typelevel.log4cats.StructuredLogger - -import scala.util.control.NoStackTrace - -object HttpCbasDAO { - implicit val statusDecoder: Decoder[CbasStatusCheckResponse] = Decoder.instance { d => - for { - ok <- d.downField("ok").as[Boolean] - } yield CbasStatusCheckResponse(ok) - } -} - -class HttpCbasDAO[F[_]](httpClient: Client[F])(implicit - logger: StructuredLogger[F], - F: Async[F], - metrics: OpenTelemetryMetrics[F] -) extends CbasDAO[F] - with Http4sClientDsl[F] { - import HttpCbasDAO._ - - override def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - for { - _ <- metrics.incrementCounter("cbas/status") - cbasStatusUri = baseUri / "cbas" / "status" - res <- httpClient.expectOr[CbasStatusCheckResponse]( - Request[F]( - method = Method.GET, - uri = cbasStatusUri, - headers = Headers(authHeader) - ) - )(onError) - } yield res.ok - - private def onError(response: Response[F])(implicit ev: Ask[F, AppContext]): F[Throwable] = - for { - ctx <- ev.ask - body <- response.bodyText.compile.foldMonoid - _ <- logger.error(ctx.loggingCtx)(s"Failed to get status from CBAS. Body: $body") - _ <- metrics.incrementCounter("cbas/errorResponse") - } yield CbasStatusCheckException(ctx.traceId, body, response.status.code) -} - -// API response models -final case class CbasStatusCheckResponse(ok: Boolean) extends AnyVal - -final case class CbasStatusCheckException(traceId: TraceId, msg: String, code: StatusCode) - extends LeoException(message = s"CBAS error: $msg", statusCode = code, traceId = Some(traceId)) - with NoStackTrace diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAO.scala deleted file mode 100644 index 85b5314e89..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAO.scala +++ /dev/null @@ -1,73 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import akka.http.scaladsl.model.StatusCode -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import io.circe._ -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.broadinstitute.dsde.workbench.leonardo.model.LeoException -import org.broadinstitute.dsde.workbench.model.TraceId -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s._ -import org.http4s.client.Client -import org.http4s.client.dsl.Http4sClientDsl -import org.http4s.headers.Authorization -import org.typelevel.log4cats.StructuredLogger - -import scala.util.control.NoStackTrace - -object HttpCromwellDAO { - implicit val statusDecoder: Decoder[CromwellStatusCheckResponse] = Decoder.instance { d => - for { - ok <- d.downField("ok").as[Boolean] - } yield CromwellStatusCheckResponse(ok) - } -} - -class HttpCromwellDAO[F[_]](httpClient: Client[F])(implicit - logger: StructuredLogger[F], - F: Async[F], - metrics: OpenTelemetryMetrics[F] -) extends CromwellDAO[F] - with Http4sClientDsl[F] { - - override def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - for { - _ <- metrics.incrementCounter("cromwell/status") - cromwellStatusUri = baseUri / "cromwell" / "engine" / "v1" / "status" - // Note: cromwell-as-an-app /status returns '{}' in the response body for some reason. - // For now just check the HTTP status code instead of parsing the response body. - res <- httpClient.status( - Request[F]( - method = Method.GET, - uri = cromwellStatusUri, - headers = Headers(authHeader) - ) - ) -// res <- httpClient.expectOr[CromwellStatusCheckResponse]( -// Request[F]( -// method = Method.GET, -// uri = cromwellStatusUri, -// headers = Headers(authHeader) -// ) -// )(onError) - } yield res.isSuccess - - private def onError(response: Response[F])(implicit ev: Ask[F, AppContext]): F[Throwable] = - for { - ctx <- ev.ask - body <- response.bodyText.compile.foldMonoid - _ <- logger.error(ctx.loggingCtx)(s"Failed to get status from Cromwell. Body: $body") - _ <- metrics.incrementCounter("cromwell/errorResponse") - } yield CromwellStatusCheckException(ctx.traceId, body, response.status.code) -} - -// API response models -final case class CromwellStatusCheckResponse(ok: Boolean) extends AnyVal - -final case class CromwellStatusCheckException(traceId: TraceId, msg: String, code: StatusCode) - extends LeoException(message = s"Cromwell error: $msg", statusCode = code, traceId = Some(traceId)) - with NoStackTrace diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAO.scala deleted file mode 100644 index 5b674b498e..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAO.scala +++ /dev/null @@ -1,46 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s._ -import org.http4s.client.Client -import org.http4s.client.dsl.Http4sClientDsl -import org.http4s.headers.Authorization - -/** - * HTTP client for Hail Batch app. - * The Hail Batch app exposes 2 services: batch and batch-driver. They are both fronted - * by a reverse proxy. The batch container serves the UI and user requests; the batch-driver - * is responsible for provisioning and monitoring compute nodes. - */ -class HttpHailBatchDAO[F[_]](httpClient: Client[F])(implicit - F: Async[F], - metrics: OpenTelemetryMetrics[F] -) extends HailBatchDAO[F] - with Http4sClientDsl[F] { - - /** Status of the batch container. */ - override def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = getStatusInternal(baseUri / "batch" / "batches", authHeader) - - /** Status of the batch-driver container. */ - override def getDriverStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - getStatusInternal(baseUri / "batch-driver", authHeader) - - private def getStatusInternal(uri: Uri, authHeader: Authorization): F[Boolean] = for { - _ <- metrics.incrementCounter("hail/status") - res <- httpClient.status( - Request[F]( - method = Method.GET, - uri = uri, - headers = Headers(authHeader) - ) - ) - } yield res.isSuccess -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAO.scala deleted file mode 100644 index 41920a60be..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAO.scala +++ /dev/null @@ -1,66 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import akka.http.scaladsl.model.StatusCode -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import io.circe._ -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.broadinstitute.dsde.workbench.leonardo.model.LeoException -import org.broadinstitute.dsde.workbench.model.TraceId -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s._ -import org.http4s.circe.CirceEntityCodec.circeEntityDecoder -import org.http4s.client.Client -import org.http4s.client.dsl.Http4sClientDsl -import org.http4s.headers.Authorization -import org.typelevel.log4cats.StructuredLogger - -import scala.util.control.NoStackTrace - -object HttpWdsDAO { - implicit val statusDecoder: Decoder[WdsStatusCheckResponse] = Decoder.instance { d => - for { - ok <- d.downField("status").as[String] - } yield WdsStatusCheckResponse(ok) - } -} - -class HttpWdsDAO[F[_]](httpClient: Client[F])(implicit - logger: StructuredLogger[F], - F: Async[F], - metrics: OpenTelemetryMetrics[F] -) extends WdsDAO[F] - with Http4sClientDsl[F] { - import HttpWdsDAO._ - - override def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - for { - _ <- metrics.incrementCounter("wds/status") - wdsStatusUri = baseUri / "status" - res <- httpClient.expectOr[WdsStatusCheckResponse]( - Request[F]( - method = Method.GET, - uri = wdsStatusUri, - headers = Headers(authHeader) - ) - )(onError) - } yield res.status.equalsIgnoreCase("up") - - private def onError(response: Response[F])(implicit ev: Ask[F, AppContext]): F[Throwable] = - for { - ctx <- ev.ask - body <- response.bodyText.compile.foldMonoid - _ <- logger.error(ctx.loggingCtx)(s"Failed to get status from WDS. Body: $body") - _ <- metrics.incrementCounter("wds/errorResponse") - } yield WdsStatusCheckException(ctx.traceId, body, response.status.code) -} - -// API response models -final case class WdsStatusCheckResponse(status: String) extends AnyVal - -final case class WdsStatusCheckException(traceId: TraceId, msg: String, code: StatusCode) - extends LeoException(message = s"WDS error: $msg", statusCode = code, traceId = Some(traceId)) - with NoStackTrace diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WdsDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WdsDAO.scala deleted file mode 100644 index c9d5d6694c..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WdsDAO.scala +++ /dev/null @@ -1,12 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.http4s.Uri -import org.http4s.headers.Authorization - -trait WdsDAO[F[_]] { - def getStatus(baseUri: Uri, authHeader: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 95d36c7910..92263e3c17 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -128,7 +128,7 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu // LeoMetricsMonitor collects metrics from both runtimes and apps. // - clusterToolToToolDao provides jupyter/rstudio/welder DAOs for runtime status checking. - // - appDAO, wdsDAO, cbasDAO, cromwellDAO are for status checking apps. + // - appDAO is for status checking apps. implicit val clusterToolToToolDao = ToolDAO.clusterToolToToolDao(baselineDependencies.jupyterDAO, baselineDependencies.welderDAO, @@ -141,10 +141,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu val metricsMonitor = new LeoMetricsMonitor( ConfigReader.appConfig.metrics, baselineDependencies.appDAO, - baselineDependencies.wdsDAO, - baselineDependencies.cbasDAO, - baselineDependencies.cromwellDAO, - baselineDependencies.hailBatchDAO, baselineDependencies.listenerDAO, baselineDependencies.samDAO, kubeAlg, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index c1fa1a9fdb..bee9240448 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -132,17 +132,6 @@ class BaselineDependenciesBuilder { cloudAuthTokenProvider ) ) - cromwellDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_cromwell_client"), false).map( - client => new HttpCromwellDAO[F](client) - ) - cbasDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_cbas_client"), false).map(client => - new HttpCbasDAO[F](client) - ) - wdsDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_wds_client"), false).map(client => - new HttpWdsDAO[F](client) - ) - hailBatchDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_hail_batch_client"), false) - .map(client => new HttpHailBatchDAO[F](client)) listenerDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_listener_client"), false).map( client => new HttpListenerDAO[F](client) ) @@ -307,10 +296,6 @@ class BaselineDependenciesBuilder { samResourceCache, oidcConfig, appDAO, - wdsDao, - cbasDao, - cromwellDao, - hailBatchDao, listenerDao, wsmClientProvider, bpmClientProvider, @@ -443,10 +428,6 @@ final case class BaselineDependencies[F[_]]( samResourceCache: scalacache.Cache[F, SamResourceCacheKey, (Option[String], Option[AppAccessScope])], openIDConnectConfiguration: OpenIDConnectConfiguration, appDAO: AppDAO[F], - wdsDAO: WdsDAO[F], - cbasDAO: CbasDAO[F], - cromwellDAO: CromwellDAO[F], - hailBatchDAO: HailBatchDAO[F], listenerDAO: ListenerDAO[F], wsmClientProvider: HttpWsmClientProvider[F], bpmClientProvider: HttpBpmClientProvider[F], diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala index 4152764d8a..abc9c3e05b 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala @@ -29,10 +29,6 @@ import scala.jdk.CollectionConverters._ /** Collects metrics about active Leo runtimes and apps. */ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, appDAO: AppDAO[F], - wdsDAO: WdsDAO[F], - cbasDAO: CbasDAO[F], - cromwellDAO: CromwellDAO[F], - hailBatchDAO: HailBatchDAO[F], listenerDAO: ListenerDAO[F], samDAO: SamDAO[F], kubeAlg: KubernetesAlgebra[F], @@ -187,11 +183,6 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, relayPath = Uri .unsafeFromString(baseUri.asString) / s"${app.appName.value}-${app.workspaceId.map(_.value.toString).getOrElse("")}" isUp <- serviceName match { - case ServiceName("cbas") => cbasDAO.getStatus(relayPath, authHeader).handleError(_ => false) - case ServiceName("cromwell") | ServiceName("cromwell-reader") | ServiceName("cromwell-runner") => - cromwellDAO.getStatus(relayPath, authHeader).handleError(_ => false) - case ServiceName("wds") => wdsDAO.getStatus(relayPath, authHeader).handleError(_ => false) - case ServiceName("batch") => hailBatchDAO.getStatus(relayPath, authHeader).handleError(_ => false) case s if s == ConfigReader.appConfig.azure.listenerChartConfig.service.config.name => listenerDAO.getStatus(relayPath).handleError(_ => false) case s => diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAOSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAOSpec.scala deleted file mode 100644 index ae49dabd01..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCbasDAOSpec.scala +++ /dev/null @@ -1,72 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import io.circe.parser._ -import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestSuite -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.dao.HttpCbasDAO.statusDecoder -import org.http4s._ -import org.http4s.circe.CirceEntityEncoder._ -import org.http4s.client.Client -import org.http4s.headers.Authorization -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class HttpCbasDAOSpec extends AnyFlatSpec with Matchers with LeonardoTestSuite { - "HttpCbasDAO" should "decode cbas status endpoint response successfully" in { - val response = - """ - |{ - | "ok": true - |} - """.stripMargin - - val res = for { - json <- parse(response) - resp <- json.as[CbasStatusCheckResponse] - } yield resp - - res shouldBe (Right(CbasStatusCheckResponse(true))) - } - - "HttpCbasDAO.getStatus" should "return false if status is not ok" in { - val authHeader = Authorization(Credentials.Token(AuthScheme.Bearer, "token")) - - val response = - """ - |{ - | "ok": false - |} - """.stripMargin - - val okCbas = Client.fromHttpApp[IO]( - HttpApp(_ => IO.fromEither(parse(response)).flatMap(r => IO(Response(status = Status.Ok).withEntity(r)))) - ) - - val cbasDAO = new HttpCbasDAO(okCbas) - val res = cbasDAO.getStatus(Uri.unsafeFromString("https://test.com/cbas/status"), authHeader) - val status = res.unsafeRunSync() - status shouldBe false - } - - "HttpCbasDAO.getStatus" should "return true if status is ok" in { - val authHeader = Authorization(Credentials.Token(AuthScheme.Bearer, "token")) - - val response = - """ - |{ - | "ok": true - |} - """.stripMargin - - val okCbas = Client.fromHttpApp[IO]( - HttpApp(_ => IO.fromEither(parse(response)).flatMap(r => IO(Response(status = Status.Ok).withEntity(r)))) - ) - - val cbasDAO = new HttpCbasDAO(okCbas) - val res = cbasDAO.getStatus(Uri.unsafeFromString("https://test.com/cbas/status"), authHeader) - val status = res.unsafeRunSync() - status shouldBe true - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAOSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAOSpec.scala deleted file mode 100644 index b034bb8beb..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpCromwellDAOSpec.scala +++ /dev/null @@ -1,72 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package dao - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import io.circe.parser._ -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.dao.HttpCromwellDAO.statusDecoder -import org.http4s._ -import org.http4s.circe.CirceEntityEncoder._ -import org.http4s.client.Client -import org.http4s.headers.Authorization -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class HttpCromwellDAOSpec extends AnyFlatSpec with Matchers with LeonardoTestSuite { - "HttpCromwellDAO" should "decode cromwell status endpoint response successfully" in { - val response = - """ - |{ - | "ok": true - |} - """.stripMargin - - val res = for { - json <- parse(response) - resp <- json.as[CromwellStatusCheckResponse] - } yield resp - - res shouldBe (Right(CromwellStatusCheckResponse(true))) - } - - "HttpCromwellDAO.getStatus" should "return false if status is not ok" in { - val authHeader = Authorization(Credentials.Token(AuthScheme.Bearer, "token")) - - val response = - """ - |{ - | "ok": false - |} - """.stripMargin - - val okCrom = Client.fromHttpApp[IO]( - HttpApp(_ => IO.fromEither(parse(response)).flatMap(r => IO(Response(status = Status.BadGateway).withEntity(r)))) - ) - - val cromwellDAO = new HttpCromwellDAO(okCrom) - val res = cromwellDAO.getStatus(Uri.unsafeFromString("https://test.com/cromwell/status"), authHeader) - val status = res.unsafeRunSync() - status shouldBe false - } - - "HttpCromwellDAO.getStatus" should "return true if status is ok" in { - val authHeader = Authorization(Credentials.Token(AuthScheme.Bearer, "token")) - - val response = - """ - |{ - | "ok": true - |} - """.stripMargin - - val okCrom = Client.fromHttpApp[IO]( - HttpApp(_ => IO.fromEither(parse(response)).flatMap(r => IO(Response(status = Status.Ok).withEntity(r)))) - ) - - val cromwellDAO = new HttpCromwellDAO(okCrom) - val res = cromwellDAO.getStatus(Uri.unsafeFromString("https://test.com/cromwell/status"), authHeader) - val status = res.unsafeRunSync() - status shouldBe true - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAOSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAOSpec.scala deleted file mode 100644 index 717f4e8d59..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpHailBatchDAOSpec.scala +++ /dev/null @@ -1,54 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestSuite -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.http4s._ -import org.http4s.circe.CirceEntityEncoder._ -import org.http4s.client.Client -import org.http4s.headers.Authorization -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class HttpHailBatchDAOSpec extends AnyFlatSpec with Matchers with LeonardoTestSuite { - "HttpHailBatchDAO.getStatus" should "return true if status is ok" in { - val hailBatchDAO = new HttpHailBatchDAO(okHail) - val res = hailBatchDAO.getStatus(Uri.unsafeFromString("https://test.com/hail/status"), authHeader) - res.unsafeRunSync() shouldBe true - } - - it should "return false if status is not ok" in { - val hailBatchDAO = new HttpHailBatchDAO(notOkHail) - val res = hailBatchDAO.getStatus(Uri.unsafeFromString("https://test.com/hail/status"), authHeader) - res.unsafeRunSync() shouldBe false - } - - "HttpHailBatchDAO.getDriverStatus" should "return true if status is ok" in { - val hailBatchDAO = new HttpHailBatchDAO(okHail) - val res = hailBatchDAO.getDriverStatus(Uri.unsafeFromString("https://test.com/hail/status"), authHeader) - res.unsafeRunSync() shouldBe true - } - - it should "return false if status is not ok" in { - val hailBatchDAO = new HttpHailBatchDAO(notOkHail) - val res = hailBatchDAO.getDriverStatus(Uri.unsafeFromString("https://test.com/hail/status"), authHeader) - res.unsafeRunSync() shouldBe false - } - - private def okHail: Client[IO] = { - val response = "huzzah" - Client.fromHttpApp[IO]( - HttpApp(_ => IO(Response(status = Status.Ok).withEntity(response))) - ) - } - - private def notOkHail: Client[IO] = { - val response = "alas" - Client.fromHttpApp[IO]( - HttpApp(_ => IO(Response(status = Status.InternalServerError).withEntity(response))) - ) - } - - private def authHeader = Authorization(Credentials.Token(AuthScheme.Bearer, "token")) -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAOSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAOSpec.scala deleted file mode 100644 index 63c2f72138..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWdsDAOSpec.scala +++ /dev/null @@ -1,52 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import io.circe.parser._ -import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestSuite -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.dao.HttpWdsDAO.statusDecoder -import org.http4s._ -import org.http4s.circe.CirceEntityEncoder._ -import org.http4s.client.Client -import org.http4s.headers.Authorization -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class HttpWdsDAOSpec extends AnyFlatSpec with Matchers with LeonardoTestSuite { - "HttpWdsDAO" should "decode wds status endpoint response successfully" in { - val response = - """ - |{ - | "status": "UP" - |} - """.stripMargin - - val res = for { - json <- parse(response) - resp <- json.as[WdsStatusCheckResponse] - } yield resp - - res shouldBe (Right(WdsStatusCheckResponse("UP"))) - } - - "HttpWdsDAO.getStatus" should "return false if status is not ok" in { - val authHeader = Authorization(Credentials.Token(AuthScheme.Bearer, "token")) - - val response = - """ - |{ - | "status": "DOWN" - |} - """.stripMargin - - val okWds = Client.fromHttpApp[IO]( - HttpApp(_ => IO.fromEither(parse(response)).flatMap(r => IO(Response(status = Status.Ok).withEntity(r)))) - ) - - val wdsDAO = new HttpWdsDAO(okWds) - val res = wdsDAO.getStatus(Uri.unsafeFromString("https://test.com/wds/status"), authHeader) - val status = res.unsafeRunSync() - status shouldBe false - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index e439a83e5d..fbe4f0a0a1 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -135,10 +135,6 @@ class GcpDependenciesBuilderSpec mock[Cache[IO, SamResourceCacheKey, (Option[String], Option[AppAccessScope])]], mock[OpenIDConnectConfiguration], mock[AppDAO[IO]], - mock[WdsDAO[IO]], - mock[CbasDAO[IO]], - mock[CromwellDAO[IO]], - mock[HailBatchDAO[IO]], mock[ListenerDAO[IO]], mock[HttpWsmClientProvider[IO]], mock[HttpBpmClientProvider[IO]], diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index 28ff75a446..f59c0af1ec 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -73,14 +73,10 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test // Mocks val appDAO = setUpMockAppDAO - val wdsDAO = setUpMockWdsDAO - val cbasDAO = setUpMockCbasDAO - val cromwellDAO = setUpMockCromwellDAO val samDAO = setUpMockSamDAO val jupyterDAO = setUpMockJupyterDAO val rstudioDAO = setUpMockRStudioDAO val welderDAO = setUpMockWelderDAO - val hailBatchDAO = setUpMockHailBatchDAO val relayListenerDAO = setUpMockRelayListenerDAO val kube = setUpMockKubeDAO val containerService = setUpMockAzureContainerService @@ -93,10 +89,6 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test val leoMetricsMonitor = new LeoMetricsMonitor[IO]( config, appDAO, - wdsDAO, - cbasDAO, - cromwellDAO, - hailBatchDAO, relayListenerDAO, samDAO, kube, @@ -583,31 +575,6 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test sam } - private def setUpMockCromwellDAO: CromwellDAO[IO] = { - val cromwell = mock[CromwellDAO[IO]] - when { - cromwell.getStatus(any, any)(any) - } thenReturn IO.pure(true) - cromwell - } - - // CBAS is down - private def setUpMockCbasDAO: CbasDAO[IO] = { - val cbas = mock[CbasDAO[IO]] - when { - cbas.getStatus(any, any)(any) - } thenReturn IO.pure(false) - cbas - } - - private def setUpMockWdsDAO: WdsDAO[IO] = { - val wds = mock[WdsDAO[IO]] - when { - wds.getStatus(any, any)(any) - } thenReturn IO.pure(true) - wds - } - private def setUpMockAppDAO: AppDAO[IO] = { val app = mock[AppDAO[IO]] when { @@ -641,17 +608,6 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test welder } - private def setUpMockHailBatchDAO: HailBatchDAO[IO] = { - val batch = mock[HailBatchDAO[IO]] - when { - batch.getStatus(any, any)(any) - } thenReturn IO.pure(true) - when { - batch.getDriverStatus(any, any)(any) - } thenReturn IO.pure(true) - batch - } - private def setUpMockRelayListenerDAO: ListenerDAO[IO] = { val listener = mock[ListenerDAO[IO]] when { From 03f731ecd03ac543159225d49013253e953e95e9 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 14:14:39 -0400 Subject: [PATCH 12/43] Remove AzurePubSubHandler (code that manages disks and VMs) --- .../http/AppDependenciesBuilder.scala | 16 - .../monitor/LeoPubsubMessageSubscriber.scala | 202 +- .../leonardo/util/AzurePubsubHandler.scala | 1309 ------------- .../util/AzurePubsubHandlerAlgebra.scala | 93 +- .../LeoPubsubMessageSubscriberSpec.scala | 269 +-- .../util/AzurePubsubHandlerSpec.scala | 1636 ----------------- 6 files changed, 68 insertions(+), 3457 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 92263e3c17..074d0c4266 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -147,27 +147,11 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu baselineDependencies.azureContainerService ) - val azureAlg = new AzurePubsubHandlerInterp[IO]( - ConfigReader.appConfig.azure.pubsubHandler, - applicationConfig, - contentSecurityPolicy, - baselineDependencies.asyncTasksQueue, - baselineDependencies.wsmDAO, - baselineDependencies.samDAO, - baselineDependencies.welderDAO, - baselineDependencies.jupyterDAO, - baselineDependencies.azureRelay, - baselineDependencies.azureVmService, - refererConfig, - baselineDependencies.wsmClientProvider - ) - val pubsubSubscriber = new LeoPubsubMessageSubscriber[IO]( leoPubsubMessageSubscriberConfig, baselineDependencies.subscriber, baselineDependencies.asyncTasksQueue, baselineDependencies.authProvider, - azureAlg, baselineDependencies.operationFutureCache, cloudSpecificDependencies, baselineDependencies.samService diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala index 181f1fca97..124717d52e 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala @@ -57,7 +57,6 @@ import scala.jdk.CollectionConverters._ * @param subscriber * @param asyncTasks * @param authProvider - * @param azurePubsubHandler * @param operationFutureCache This is used to cancel long running java Futures for Google operations. Currently, we only cancel existing stopping runtime operation if a `deleteRuntime` * message is received * @tparam F @@ -67,7 +66,6 @@ class LeoPubsubMessageSubscriber[F[_]]( subscriber: CloudSubscriber[F, LeoPubsubMessage], asyncTasks: Queue[F, Task[F]], authProvider: LeoAuthProvider[F], - azurePubsubHandler: AzurePubsubHandlerAlgebra[F], operationFutureCache: scalacache.Cache[F, Long, OperationFuture[Operation, Operation]], cloudSpecificDependenciesRegistry: ServicesRegistry, samService: SamService[F] @@ -110,25 +108,9 @@ class LeoPubsubMessageSubscriber[F[_]]( handleStartAppMessage(msg) case msg: UpdateAppMessage => handleUpdateAppMessage(msg) - case msg: CreateAzureRuntimeMessage => - azurePubsubHandler.createAndPollRuntime(msg).adaptError { case e => - PubsubHandleMessageError.AzureRuntimeCreationError( - msg.runtimeId, - msg.workspaceId, - e.getMessage, - msg.useExistingDisk - ) - } - case msg: DeleteAzureRuntimeMessage => - azurePubsubHandler.deleteAndPollRuntime(msg).adaptError { case e => - PubsubHandleMessageError.AzureRuntimeDeletionError( - msg.runtimeId, - msg.diskIdToDelete, - msg.workspaceId, - e.getMessage - ) - } - case msg: DeleteDiskV2Message => handleDeleteDiskV2Message(msg) + case _: CreateAzureRuntimeMessage => ??? + case _: DeleteAzureRuntimeMessage => ??? + case _: DeleteDiskV2Message => ??? } } yield resp @@ -188,10 +170,6 @@ class LeoPubsubMessageSubscriber[F[_]]( logger.error(ctx.loggingCtx, e)( s"Encountered an error for app ${ee.appId}, ${ee.getMessage}" ) >> handleKubernetesError(ee) - case ee: AzureRuntimeCreationError => - azurePubsubHandler.handleAzureRuntimeCreationError(ee, ctx.now) - case ee: AzureRuntimeDeletionError => - azurePubsubHandler.handleAzureRuntimeDeletionError(ee) case _ => logger.error(ctx.loggingCtx, ee)(s"Failed to process pubsub message.") } _ <- @@ -328,7 +306,7 @@ class LeoPubsubMessageSubscriber[F[_]]( ) googleProject <- F.fromOption( LeoLenses.cloudContextToGoogleProject.get(runtime.cloudContext), - AzureUnimplementedException("Azure runtime is not supported yet") + AzureUnimplementedException("Azure runtime is not supported") ) poll = op match { case Some(opFuture) => @@ -410,61 +388,45 @@ class LeoPubsubMessageSubscriber[F[_]]( _ <- logger.info( s"StopRuntimeMessage timing: Got the runtimeConfig, [runtime = ${runtime.runtimeName.asString}, traceId = ${ctx.traceId.asString},time = ${(now.toEpochMilli - ctx.now.toEpochMilli).toString}]" ) - _ <- runtime.cloudContext match { - case CloudContext.Gcp(_) => - for { - op <- runtimeConfig.cloudService.interpreter.stopRuntime( - StopRuntimeParams(RuntimeAndRuntimeConfig(runtime, runtimeConfig), ctx.now, isDataprocFullStop = true) - ) - poll = op match { - case Some(o) => - val monitorContext = MonitorContext(ctx.now, runtime.id, ctx.traceId, RuntimeStatus.Stopping) - for { - operation <- F.blocking(o.get()) - _ <- operationFutureCache.put(runtime.id)(o, None) - _ <- F.whenA(isSuccess(operation.getHttpErrorStatusCode))( - runtimeConfig.cloudService - .handlePollCheckCompletion(monitorContext, RuntimeAndRuntimeConfig(runtime, runtimeConfig)) - ) - } yield () - case None => - runtimeConfig.cloudService.process(runtime.id, RuntimeStatus.Stopping, None).compile.drain - } - now <- F.realTimeInstant - _ <- logger.info( - s"StopRuntimeMessage timing: Polling the stopRuntime, [runtime = ${runtime.runtimeName}, traceId = ${ctx.traceId.asString}, time = ${(now.toEpochMilli - ctx.now.toEpochMilli).toString}]" - ) - isAoU = runtime.labels.get(AOU_UI_LABEL).contains("true") - _ <- asyncTasks.offer( - Task( - ctx.traceId, - poll, - Some( - handleRuntimeMessageError( - msg.runtimeId, - ctx.now, - s"stopping runtime ${runtime.projectNameString}/${runtime.runtimeName.toString} failed" - ) - ), - ctx.now, - TaskMetricsTags("stopRuntime", None, Some(isAoU), CloudProvider.Gcp, Some(runtimeConfig.cloudService)) + _ <- for { + op <- runtimeConfig.cloudService.interpreter.stopRuntime( + StopRuntimeParams(RuntimeAndRuntimeConfig(runtime, runtimeConfig), ctx.now, isDataprocFullStop = true) + ) + poll = op match { + case Some(o) => + val monitorContext = MonitorContext(ctx.now, runtime.id, ctx.traceId, RuntimeStatus.Stopping) + for { + operation <- F.blocking(o.get()) + _ <- operationFutureCache.put(runtime.id)(o, None) + _ <- F.whenA(isSuccess(operation.getHttpErrorStatusCode))( + runtimeConfig.cloudService + .handlePollCheckCompletion(monitorContext, RuntimeAndRuntimeConfig(runtime, runtimeConfig)) ) - ) - } yield () - case CloudContext.Azure(azureContext) => - azurePubsubHandler - .stopAndMonitorRuntime(runtime, azureContext) - .handleErrorWith(e => - azurePubsubHandler.handleAzureRuntimeStopError( - AzureRuntimeStoppingError( - runtime.id, - s"stopping runtime ${runtime.projectNameString} failed. Cause: ${e.getMessage}", - ctx.traceId - ), - ctx.now + } yield () + case None => + runtimeConfig.cloudService.process(runtime.id, RuntimeStatus.Stopping, None).compile.drain + } + now <- F.realTimeInstant + _ <- logger.info( + s"StopRuntimeMessage timing: Polling the stopRuntime, [runtime = ${runtime.runtimeName}, traceId = ${ctx.traceId.asString}, time = ${(now.toEpochMilli - ctx.now.toEpochMilli).toString}]" + ) + isAoU = runtime.labels.get(AOU_UI_LABEL).contains("true") + _ <- asyncTasks.offer( + Task( + ctx.traceId, + poll, + Some( + handleRuntimeMessageError( + msg.runtimeId, + ctx.now, + s"stopping runtime ${runtime.projectNameString}/${runtime.runtimeName.toString} failed" ) - ) - } + ), + ctx.now, + TaskMetricsTags("stopRuntime", None, Some(isAoU), CloudProvider.Gcp, Some(runtimeConfig.cloudService)) + ) + ) + } yield () } yield () private[monitor] def handleStartRuntimeMessage(msg: StartRuntimeMessage)(implicit @@ -484,48 +446,31 @@ class LeoPubsubMessageSubscriber[F[_]]( PubsubHandleMessageError.ClusterInvalidState(msg.runtimeId, runtime.projectNameString, runtime, msg) ) else F.unit - _ <- runtime.cloudContext match { - case CloudContext.Gcp(_) => - for { - runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(runtime.runtimeConfigId).transaction - initBucket <- clusterQuery.getInitBucket(msg.runtimeId).transaction - bucketName <- F.fromOption( - initBucket.map(_.bucketName), - new RuntimeException(s"init bucket not found for ${runtime.projectNameString} in DB") - ) - _ <- runtimeConfig.cloudService.interpreter - .startRuntime(StartRuntimeParams(RuntimeAndRuntimeConfig(runtime, runtimeConfig), bucketName)) - isAoU = runtime.labels.get(AOU_UI_LABEL).contains("true") - _ <- asyncTasks.offer( - Task( - ctx.traceId, - runtimeConfig.cloudService.process(msg.runtimeId, RuntimeStatus.Starting, None).compile.drain, - Some( - handleRuntimeMessageError(msg.runtimeId, - ctx.now, - s"starting runtime ${runtime.projectNameString} failed" - ) - ), - ctx.now, - TaskMetricsTags("startRuntime", None, Some(isAoU), CloudProvider.Gcp, Some(runtimeConfig.cloudService)) - ) - ) - } yield () - case CloudContext.Azure(azureContext) => - azurePubsubHandler - .startAndMonitorRuntime(runtime, azureContext) - .handleErrorWith(e => - azurePubsubHandler.handleAzureRuntimeStartError( - AzureRuntimeStartingError( - runtime.id, - s"starting runtime ${runtime.projectNameString} failed. Cause: ${e.getMessage}", - ctx.traceId - ), - ctx.now + _ <- for { + runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(runtime.runtimeConfigId).transaction + initBucket <- clusterQuery.getInitBucket(msg.runtimeId).transaction + bucketName <- F.fromOption( + initBucket.map(_.bucketName), + new RuntimeException(s"init bucket not found for ${runtime.projectNameString} in DB") + ) + _ <- runtimeConfig.cloudService.interpreter + .startRuntime(StartRuntimeParams(RuntimeAndRuntimeConfig(runtime, runtimeConfig), bucketName)) + isAoU = runtime.labels.get(AOU_UI_LABEL).contains("true") + _ <- asyncTasks.offer( + Task( + ctx.traceId, + runtimeConfig.cloudService.process(msg.runtimeId, RuntimeStatus.Starting, None).compile.drain, + Some( + handleRuntimeMessageError(msg.runtimeId, + ctx.now, + s"starting runtime ${runtime.projectNameString} failed" ) - ) - } - + ), + ctx.now, + TaskMetricsTags("startRuntime", None, Some(isAoU), CloudProvider.Gcp, Some(runtimeConfig.cloudService)) + ) + ) + } yield () } yield () private[monitor] def handleUpdateRuntimeMessage(msg: UpdateRuntimeMessage)(implicit @@ -1770,23 +1715,6 @@ class LeoPubsubMessageSubscriber[F[_]]( } } yield res - private[monitor] def handleDeleteDiskV2Message( - msg: DeleteDiskV2Message - )(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - _ <- msg.cloudContext match { - case CloudContext.Azure(_) => - azurePubsubHandler.deleteDisk(msg).adaptError { case e => - PubsubHandleMessageError.DiskDeletionError( - msg.diskId, - msg.workspaceId, - e.getMessage - ) - } - case CloudContext.Gcp(_) => - deleteDisk(msg.diskId, false) - } - } yield () private def getGoogleDiskServiceFromRegistry(): GoogleDiskService[F] = { logger.info(s"Getting googleDiskService from registry") cloudSpecificDependenciesRegistry.lookup[GcpDependencies[F]].get.googleDiskService diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala deleted file mode 100644 index 174fd578cd..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandler.scala +++ /dev/null @@ -1,1309 +0,0 @@ -package org.broadinstitute.dsde.workbench -package leonardo -package util - -import bio.terra.workspace.api.ControlledAzureResourceApi -import bio.terra.workspace.model._ -import cats.Parallel -import cats.effect.Async -import cats.effect.std.Queue -import cats.mtl.Ask -import cats.syntax.all._ -import com.azure.resourcemanager.compute.models.{PowerState, VirtualMachine, VirtualMachineSizeTypes} -import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.google2.{RegionName, streamFUntilDone, streamUntilDoneOrTimeout} -import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor.{Task, TaskMetricsTags} -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.PrivateAzureStorageAccountSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.config.{ApplicationConfig, AzureEnvironmentConverter, ContentSecurityPolicyConfig, RefererConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http.{ConfigReader, ctxConversion, dbioToIO} -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAzureRuntimeMessage, DeleteAzureRuntimeMessage, DeleteDiskV2Message} -import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError -import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError._ -import org.broadinstitute.dsde.workbench.model.{IP, WorkbenchEmail} -import org.broadinstitute.dsde.workbench.util2.InstanceName -import org.typelevel.log4cats.StructuredLogger -import reactor.core.publisher.Mono - -import java.time.{Duration, Instant} -import java.util.UUID -import scala.concurrent.ExecutionContext -import scala.jdk.CollectionConverters._ - -class AzurePubsubHandlerInterp[F[_]: Parallel]( - config: AzurePubsubHandlerConfig, - applicationConfig: ApplicationConfig, - contentSecurityPolicyConfig: ContentSecurityPolicyConfig, - asyncTasks: Queue[F, Task[F]], - wsmDao: WsmDao[F], - samDAO: SamDAO[F], - welderDao: WelderDAO[F], - jupyterDAO: JupyterDAO[F], - azureRelay: AzureRelayService[F], - azureVmServiceInterp: AzureVmService[F], - refererConfig: RefererConfig, - wsmClientProvider: WsmApiClientProvider[F] -)(implicit val executionContext: ExecutionContext, dbRef: DbReference[F], logger: StructuredLogger[F], F: Async[F]) - extends AzurePubsubHandlerAlgebra[F] { - - // implicits necessary to poll on the status of external jobs - implicit private def isJupyterUpDoneCheckable: DoneCheckable[Boolean] = (v: Boolean) => v - - implicit private def wsmDeleteDoneControlledAzureResourceDoneCheckable - : DoneCheckable[DeleteControlledAzureResourceResult] = (v: DeleteControlledAzureResourceResult) => - v.getJobReport.getStatus.equals(JobReport.StatusEnum.SUCCEEDED) || v.getJobReport.getStatus - .equals(JobReport.StatusEnum.FAILED) - - implicit private def wsmCreateAzureResourceResultDoneCheckable: DoneCheckable[CreateControlledAzureResourceResult] = - (v: CreateControlledAzureResourceResult) => - v.getJobReport.getStatus.equals(JobReport.StatusEnum.SUCCEEDED) || v.getJobReport.getStatus - .equals(JobReport.StatusEnum.FAILED) - - implicit private def wsmCreateAzureVmResultDoneCheckable: DoneCheckable[CreatedControlledAzureVmResult] = - (v: CreatedControlledAzureVmResult) => - v.getJobReport.getStatus.equals(JobReport.StatusEnum.SUCCEEDED) || v.getJobReport.getStatus - .equals(JobReport.StatusEnum.FAILED) - - implicit private def vmStopDoneCheckable: DoneCheckable[Option[VirtualMachine]] = (v: Option[VirtualMachine]) => - v.get.powerState() == PowerState.DEALLOCATED - - override def createAndPollRuntime(msg: CreateAzureRuntimeMessage)(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - ctx <- ev.ask - _ <- logger.info(s"[AzurePubsubHandler/createAndPollRuntime] beginning for runtime ${msg.runtimeId}") - runtimeOpt <- clusterQuery.getClusterById(msg.runtimeId).transaction - runtime <- F.fromOption(runtimeOpt, PubsubHandleMessageError.ClusterNotFound(msg.runtimeId, msg)) - runtimeConfig <- RuntimeConfigQueries.getRuntimeConfig(runtime.runtimeConfigId).transaction - azureConfig <- runtimeConfig match { - case x: RuntimeConfig.AzureConfig => F.pure(x) - case x => - F.raiseError[RuntimeConfig.AzureConfig]( - new RuntimeException(s"this runtime doesn't have proper azure config $x") - ) - } - - // verify the cloud context - cloudContext = runtime.cloudContext match { - case _: CloudContext.Gcp => - throw PubsubHandleMessageError.ClusterError(runtime.id, - ctx.traceId, - "Azure runtime should not have GCP cloud context" - ) - case cc: CloudContext.Azure => cc - } - - // Query the Landing Zone service for the landing zone resources - leoAuth <- samDAO.getLeoAuthToken - landingZoneResources <- wsmDao.getLandingZoneResources(msg.billingProfileId, leoAuth) - - // Infer the runtime region from the Landing Zone - _ <- RuntimeConfigQueries - .updateRegion(runtime.runtimeConfigId, Some(RegionName(landingZoneResources.region.name()))) - .transaction - - _ <- logger.info( - s"[AzurePubsubHandler/createAndPollRuntime] getting workspace storage container from WSM for runtime ${msg.runtimeId}" - ) - // Get the optional storage container for the workspace - workspaceStorageContainerOpt <- wsmDao.getWorkspaceStorageContainer( - msg.workspaceId, - leoAuth - ) - - workspaceStorageContainer <- F.fromOption( - workspaceStorageContainerOpt, - AzureRuntimeCreationError( - runtime.id, - msg.workspaceId, - s"Storage container not found for workspace: ${msg.workspaceId.value}", - msg.useExistingDisk - ) - ) - - // send create disk message to WSM - createDiskResult <- createDiskForRuntime( - CreateAzureDiskParams(msg.workspaceId, runtime, msg.useExistingDisk, azureConfig) - ) - - // Get optional action managed identity from Sam for the private_azure_storage_account/read action. - // Identities must be passed to WSM for application-managed resources. - actionIdentityOpt <- samDAO.getAzureActionManagedIdentity( - leoAuth, - PrivateAzureStorageAccountSamResourceId(msg.billingProfileId.value), - PrivateAzureStorageAccountAction.Read - ) - - _ <- logger.info( - s"[AzurePubsubHandler/createAndPollRuntime] beginning to monitor runtime creation for runtime ${msg.runtimeId}" - ) - - // all other resources (hybrid connection, storage container, vm) - // are created within the async task - _ <- monitorCreateRuntime( - PollRuntimeParams( - msg.workspaceId, - runtime, - msg.useExistingDisk, - createDiskResult, - landingZoneResources, - azureConfig, - config.runtimeDefaults.image, - workspaceStorageContainer, - msg.workspaceName, - AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getStorageEndpointSuffix, - cloudContext, - List(actionIdentityOpt).flatten - ) - ) - } yield () - - private def setupCreateVmCreateMessage(params: PollRuntimeParams, - storageContainer: CreateStorageContainerResourcesResult, - hcPrimaryKey: PrimaryKey, - createVmJobId: WsmJobId, - hcName: RelayHybridConnectionName - ): CreateControlledAzureVmRequestBody = { - val samResourceId = WsmControlledResourceId(UUID.fromString(params.runtime.samResource.resourceId)) - - val wsStorageContainerUrl = - s"https://${params.landingZoneResources.storageAccountName.value}.${params.storageAccountUrlDomain}/${params.workspaceStorageContainer.name.value}" - - // Setup create VM message - val vmCommon = getCommonFieldsForWsmGeneratedClient( - ControlledResourceName(params.runtime.runtimeName.asString), - config.runtimeDefaults.vmControlledResourceDesc, - params.runtime.auditInfo.creator - ) - .resourceId(samResourceId.value) - - val arguments = List( - params.landingZoneResources.relayNamespace.value, - hcName.value, - "localhost", - hcPrimaryKey.value, - config.runtimeDefaults.listenerImage, - config.samUrl.renderString, - samResourceId.value.toString, - "csp.txt", - config.wsmUrl.renderString, - params.workspaceId.value.toString, - params.workspaceStorageContainer.resourceId.value.toString, - config.welderImage, - params.runtime.auditInfo.creator.value, - storageContainer.containerName.value, - storageContainer.resourceId.value.toString, - params.workspaceName, - wsStorageContainerUrl, - applicationConfig.leoUrlBase, - params.runtime.runtimeName.asString, - s"'${refererConfig.validHosts.mkString("','")}'", - AzureEnvironmentConverter.relaySuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment), - AzureEnvironmentConverter - .fromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment) - .getResourceManagerEndpoint() - ) - - val cmdToExecute = s"touch /var/log/azure_vm_init_script.log && chmod 400 /var/log/azure_vm_init_script.log &&" + - s"echo \"${contentSecurityPolicyConfig.asString}\" > csp.txt && bash azure_vm_init_script.sh ${arguments - .map(s => s"'$s'") - .mkString(" ")} > /var/log/azure_vm_init_script.log" - - val protectedSettings: List[AzureVmCustomScriptExtensionSetting] = List( - new AzureVmCustomScriptExtensionSetting() - .key("fileUris") - .value(config.runtimeDefaults.customScriptExtension.fileUris.asJava), - new AzureVmCustomScriptExtensionSetting() - .key("commandToExecute") - .value(cmdToExecute) - ) - - val customScriptExtension = new AzureVmCustomScriptExtension() - .name(config.runtimeDefaults.customScriptExtension.name) - .`type`(config.runtimeDefaults.customScriptExtension.`type`) - .publisher(config.runtimeDefaults.customScriptExtension.publisher) - .version(config.runtimeDefaults.customScriptExtension.version) - .minorVersionAutoUpgrade(config.runtimeDefaults.customScriptExtension.minorVersionAutoUpgrade) - .protectedSettings(protectedSettings.asJava) - - val vmPassword = AzurePubsubHandler.getAzureVMSecurePassword(applicationConfig.environment, - config.runtimeDefaults.vmCredential.password - ) - - val userAssignedIdentities = new AzureVmUserAssignedIdentities() - userAssignedIdentities.addAll(params.userAssignedIdentities.asJava) - - val creationParams = new AzureVmCreationParameters() - .customScriptExtension(customScriptExtension) - .diskId(params.createDiskResult.resourceId.value) - .vmImage(config.runtimeDefaults.image.toWsm()) - .vmSize(VirtualMachineSizeTypes.fromString(params.runtimeConfig.machineType.value).toString) - .name(params.runtime.runtimeName.asString) - .vmUser( - new AzureVmUser() - .name(config.runtimeDefaults.vmCredential.username) - .password(vmPassword) - ) - .userAssignedIdentities(userAssignedIdentities) - - new CreateControlledAzureVmRequestBody() - .azureVm(creationParams) - .common(vmCommon) - .jobControl(new JobControl().id(createVmJobId.value)) - } - - private def monitorStartRuntime(runtime: Runtime, startVmOp: Option[Mono[Void]])(implicit - ev: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- ev.ask - task = for { - _ <- startVmOp.traverse(startVmOp => F.blocking(startVmOp.block(Duration.ofMinutes(5)))) - isJupyterUp = jupyterDAO.isProxyAvailable(runtime.cloudContext, runtime.runtimeName) - _ <- streamUntilDoneOrTimeout( - isJupyterUp, - config.startStopVmPollConfig.maxAttempts, - config.startStopVmPollConfig.interval, - s"Jupyter was not running within ${config.createVmPollConfig.maxAttempts} attempts with ${config.createVmPollConfig.interval} delay" - ) - _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.Running, ctx.now).transaction - _ <- logger.info(ctx.loggingCtx)("runtime is ready") - } yield () - _ <- asyncTasks.offer( - Task( - ctx.traceId, - task, - Some(e => - handleAzureRuntimeStartError( - AzureRuntimeStartingError( - runtime.id, - s"Starting runtime ${runtime.projectNameString} failed. Cause: ${e.getMessage}", - ctx.traceId - ), - ctx.now - ) - ), - ctx.now, - TaskMetricsTags("startRuntimeV2", None, Some(false), CloudProvider.Azure) - ) - ) - } yield () - - override def startAndMonitorRuntime(runtime: Runtime, azureCloudContext: AzureCloudContext)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- ev.ask - vm <- azureVmServiceInterp.getAzureVm(InstanceName(runtime.runtimeName.asString), azureCloudContext) - - // if vm is stopping, stopped or deallocated --> send start message and monitor - // if vm is starting --> monitor - // if vm is running --> update DB - // else --> error - _ <- vm match { - case Some(vm) => - vm.powerState() match { - case PowerState.STOPPED | PowerState.DEALLOCATED | PowerState.STOPPING | PowerState.DEALLOCATING => - for { - startVmOpOpt <- azureVmServiceInterp.startAzureVm(InstanceName(runtime.runtimeName.asString), - azureCloudContext - ) - _ <- startVmOpOpt match { - case None => - F.raiseError[Unit]( - AzureRuntimeStartingError( - runtime.id, - s"Starting runtime ${runtime.id} request to Azure failed.", - ctx.traceId - ) - ) - case Some(startVmOp) => - monitorStartRuntime(runtime, Some(startVmOp)) - } - } yield () - case PowerState.STARTING => monitorStartRuntime(runtime, None) - case PowerState.RUNNING => - for { - _ <- logger.info(s"Runtime ${runtime.runtimeName.asString} already running, no-op for startRuntime") - _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.Running, ctx.now).transaction - } yield () - case _ => - F.raiseError( - AzureRuntimeStartingError( - runtime.id, - s"Runtime ${runtime.runtimeName.asString} cannot be started in a ${vm.powerState().toString} state, starting runtime request failed", - ctx.traceId - ) - ) - } - case None => - F.raiseError( - AzureRuntimeStartingError( - runtime.id, - s"Runtime ${runtime.runtimeName.asString} cannot be found in Azure, starting runtime request failed", - ctx.traceId - ) - ) - } - - } yield () - - private def monitorStopRuntime(runtime: Runtime, azureCloudContext: AzureCloudContext, monoOpt: Option[Mono[Void]])( - implicit ev: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- ev.ask - task = for { - _ <- monoOpt.traverse(mono => F.blocking(mono.block(Duration.ofMinutes(5)))) - vmStopped = azureVmServiceInterp.getAzureVm(InstanceName(runtime.runtimeName.asString), azureCloudContext) - _ <- streamUntilDoneOrTimeout( - vmStopped, - config.startStopVmPollConfig.maxAttempts, - config.startStopVmPollConfig.interval, - s"The VM was not stopped within ${config.createVmPollConfig.maxAttempts} attempts with ${config.createVmPollConfig.interval} delay" - ) - _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.Stopped, ctx.now).transaction - _ <- logger.info(ctx.loggingCtx)("runtime is stopped") - _ <- welderDao - .flushCache(runtime.cloudContext, runtime.runtimeName) - .handleErrorWith(e => - logger.error(ctx.loggingCtx, e)( - s"Failed to flush welder cache for ${runtime.projectNameString}" - ) - ) - .whenA(runtime.welderEnabled) - } yield () - _ <- asyncTasks.offer( - Task( - ctx.traceId, - task, - Some(e => - handleAzureRuntimeStopError( - AzureRuntimeStoppingError( - runtime.id, - s"stopping runtime ${runtime.projectNameString} failed. Cause: ${e.getMessage}", - ctx.traceId - ), - ctx.now - ) - ), - ctx.now, - TaskMetricsTags("startRuntimeV2", None, Some(false), CloudProvider.Azure) - ) - ) - } yield () - - override def stopAndMonitorRuntime(runtime: Runtime, azureCloudContext: AzureCloudContext)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- ev.ask - vm <- azureVmServiceInterp.getAzureVm(InstanceName(runtime.runtimeName.asString), azureCloudContext) - - // if vm is starting/running --> send stop message and monitor - // if vm is stopping/deallocating --> monitor - // if vm is stopped/deallocated --> update DB - // else --> error - _ <- vm match { - case Some(vm) => - vm.powerState() match { - case PowerState.STARTING | PowerState.RUNNING => - for { - - monoOpt <- azureVmServiceInterp.stopAzureVm(InstanceName(runtime.runtimeName.asString), azureCloudContext) - _ <- monoOpt match { - case None => - F.raiseError[Unit]( - AzureRuntimeStoppingError( - runtime.id, - s"Stopping runtime ${runtime.id} request to Azure failed.", - ctx.traceId - ) - ) - case Some(mono) => - monitorStopRuntime(runtime, azureCloudContext, Some(mono)) - } - } yield () - case PowerState.DEALLOCATING | PowerState.STOPPING => monitorStopRuntime(runtime, azureCloudContext, None) - - case PowerState.DEALLOCATED | PowerState.STOPPED => - for { - _ <- logger.info(s"Runtime ${runtime.runtimeName.asString} already stopped, no-op for stopRuntime") - _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.Stopped, ctx.now).transaction - } yield () - case _ => - F.raiseError( - AzureRuntimeStoppingError( - runtime.id, - s"Runtime ${runtime.runtimeName.asString} cannot be stopped in a ${vm.powerState().toString} state, stopping runtime request failed", - ctx.traceId - ) - ) - } - case None => - F.raiseError( - AzureRuntimeStoppingError( - runtime.id, - s"Runtime ${runtime.runtimeName.asString} cannot be found in Azure, stopping runtime request failed", - ctx.traceId - ) - ) - } - } yield () - - private def createStorageContainer(runtime: Runtime, - landingZoneResources: LandingZoneResources, - workspaceId: WorkspaceId - )(implicit - ev: Ask[F, AppContext] - ): F[CreateStorageContainerResourcesResult] = { - val storageContainerName = ContainerName(s"ls-${runtime.runtimeName.asString}") - for { - ctx <- ev.ask[AppContext] - wsmApi <- buildWsmControlledResourceApiClient - common = getCommonFieldsForWsmGeneratedClient(ControlledResourceName(storageContainerName.value), - "leonardo staging bucket", - runtime.auditInfo.creator - ) - azureStorageContainer = new AzureStorageContainerCreationParameters() - .storageContainerName(storageContainerName.value) - - request = new CreateControlledAzureStorageContainerRequestBody() - .common(common) - .azureStorageContainer(azureStorageContainer) - storageContainer <- F - .delay(wsmApi.createAzureStorageContainer(request, workspaceId.value)) - resourceId = WsmControlledResourceId(storageContainer.getResourceId) - _ <- controlledResourceQuery - .save(runtime.id, resourceId, WsmResourceType.AzureStorageContainer) - .transaction - _ <- clusterQuery - .updateStagingBucket(runtime.id, Some(StagingBucket.Azure(storageContainerName)), ctx.now) - .transaction - } yield CreateStorageContainerResourcesResult(storageContainerName, resourceId) - } - - private def createDiskForRuntime(params: CreateAzureDiskParams)(implicit - ev: Ask[F, AppContext] - ): F[CreateDiskForRuntimeResult] = for { - - diskId <- F.fromOption( - params.runtimeConfig.persistentDiskId, - AzureRuntimeCreationError( - params.runtime.id, - params.workspaceId, - s"No associated diskId found for runtime:${params.runtime.id}", - params.useExistingDisk - ) - ) - - resp <- params.useExistingDisk match { - // if using existing disk, check conditions and update tables - case true => - for { - diskOpt <- persistentDiskQuery.getById(diskId).transaction - disk <- F.fromOption( - diskOpt, - new RuntimeException(s"Disk id:${diskId.value} not found for runtime:${params.runtime.id}") - ) - resourceId <- F.fromOption( - disk.wsmResourceId, - AzureRuntimeCreationError( - params.runtime.id, - params.workspaceId, - s"No associated resourceId found for Disk id:${diskId.value}", - params.useExistingDisk - ) - ) - diskResourceOpt <- controlledResourceQuery - .getWsmRecordFromResourceId(resourceId, WsmResourceType.AzureDisk) - .transaction - wsmDisk <- F.fromOption( - diskResourceOpt, - AzureRuntimeCreationError( - params.runtime.id, - params.workspaceId, - s"WSMResource record:${resourceId.value} not found for disk id:${diskId.value}", - params.useExistingDisk - ) - ) - // set runtime to new runtimeId for disk - _ <- controlledResourceQuery - .updateRuntime(wsmDisk.resourceId, WsmResourceType.AzureDisk, params.runtime.id) - .transaction - diskResp = CreateDiskForRuntimeResult(wsmDisk.resourceId, None) - } yield diskResp - - // if not using existing disk, send a create disk request - case false => - for { - diskOpt <- persistentDiskQuery.getById(diskId).transaction - disk <- F.fromOption( - diskOpt, - AzureRuntimeCreationError(params.runtime.id, - params.workspaceId, - s"Disk $diskId not found", - params.useExistingDisk - ) - ) - common = getCommonFieldsForWsmGeneratedClient(ControlledResourceName(disk.name.value), - config.runtimeDefaults.diskControlledResourceDesc, - params.runtime.auditInfo.creator - ) - - createDiskJobId = WsmJobId(UUID.randomUUID().toString) - jobControl = new JobControl() - .id(createDiskJobId.value) - - azureDisk = new AzureDiskCreationParameters() - .name(disk.name.value) - .size(disk.size.gb) - - request = new CreateControlledAzureDiskRequestV2Body() - .common(common) - .azureDisk(azureDisk) - .jobControl(jobControl) - - _ <- logger.info( - s"[AzurePubsubHandler/createAndPollRuntime] calling createAzureDiskV2 on WSM for runtime ${params.runtime.id}" - ) - wsmApi <- buildWsmControlledResourceApiClient - _ <- F.delay(wsmApi.createAzureDiskV2(request, params.workspaceId.value)) - - syncDiskResp = CreateDiskForRuntimeResult( - WsmControlledResourceId(common.getResourceId), - Some( - PollDiskParams(params.workspaceId, - createDiskJobId, - disk.id, - params.runtime, - WsmControlledResourceId(common.getResourceId) - ) - ) - ) - } yield syncDiskResp - } - } yield resp - - private def monitorCreateDisk(params: PollDiskParams)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - wsmApi <- buildWsmControlledResourceApiClient - getWsmJobResult = F.delay(wsmApi.getCreateAzureDiskResult(params.workspaceId.value, params.jobId.value)) - - _ <- F.sleep( - config.createDiskPollConfig.initialDelay - ) - - // it does not take long to create a disk - resp <- streamFUntilDone( - getWsmJobResult, - config.createDiskPollConfig.maxAttempts, - config.createDiskPollConfig.interval - ).compile.lastOrError - - _ <- resp.getJobReport.getStatus match { - case JobReport.StatusEnum.FAILED => - logger.error(s"Wsm createDisk job failed due to ${resp.getErrorReport.getMessage}") >> F.raiseError[Unit]( - AzureRuntimeCreationError( - params.runtime.id, - params.workspaceId, - s"Wsm createDisk job failed due to ${resp.getErrorReport.getMessage}", - useExistingDisk = false - ) - ) - case JobReport.StatusEnum.RUNNING => - F.raiseError[Unit]( - AzureRuntimeCreationError( - params.runtime.id, - params.workspaceId, - s"Wsm createDisk job was not completed within ${config.createDiskPollConfig.maxAttempts} attempts with ${config.createDiskPollConfig.interval} delay", - useExistingDisk = false - ) - ) - case JobReport.StatusEnum.SUCCEEDED => - for { - _ <- controlledResourceQuery - .save(params.runtime.id, params.wsmResourceId, WsmResourceType.AzureDisk) - .transaction - _ <- persistentDiskQuery.updateStatus(params.diskId, DiskStatus.Ready, ctx.now).transaction - _ <- persistentDiskQuery.updateWSMResourceId(params.diskId, params.wsmResourceId, ctx.now).transaction - } yield () - } - } yield () - - // we expect the caller of this to update the disk status, since this is monitoring for the WSM record deletion - // this is important not to conflate the two, since a Wsm resource can exist without a leo resource and vice versa (if the systems get into a bad state) - private def monitorDeleteDisk(params: PollDeleteDiskParams)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - wsmApi <- buildWsmControlledResourceApiClient - getWsmJobResult = F.delay(wsmApi.getDeleteAzureDiskResult(params.workspaceId.value, params.jobId.value)) - - _ <- F.sleep( - config.deleteDiskPollConfig.initialDelay - ) - - resp <- streamFUntilDone( - getWsmJobResult, - config.deleteDiskPollConfig.maxAttempts, - config.deleteDiskPollConfig.interval - ).compile.lastOrError - - _ <- resp.getJobReport.getStatus match { - case JobReport.StatusEnum.SUCCEEDED => - for { - _ <- logger.info(ctx.loggingCtx)( - s"disk for runtime ${params.runtime.id} with resource id ${params.wsmResourceId} is deleted successfully" - ) - } yield () - case JobReport.StatusEnum.FAILED => - for { - _ <- logger.error(s"Wsm deleteDisk job failed due to ${resp.getErrorReport.getMessage}") - _ <- params.diskId.traverse(id => - dbRef.inTransaction(persistentDiskQuery.updateStatus(id, DiskStatus.Error, ctx.now)) - ) - _ <- F.raiseError[Unit]( - AzureRuntimeDeletionError( - params.runtime.id, - params.diskId, - params.workspaceId, - s"Wsm deleteDisk job failed due to ${resp.getErrorReport.getMessage}, disk resource id ${params.wsmResourceId}" - ) - ) - } yield () - case JobReport.StatusEnum.RUNNING => - params.diskId.traverse(id => - dbRef.inTransaction(persistentDiskQuery.updateStatus(id, DiskStatus.Error, ctx.now)) - ) >> F.raiseError[Unit]( - AzureRuntimeDeletionError( - params.runtime.id, - params.diskId, - params.workspaceId, - s"Wsm deleteDisk was not completed within ${config.deleteDiskPollConfig.maxAttempts} attempts with ${config.deleteDiskPollConfig.interval} delay, disk resource id ${params.wsmResourceId}" - ) - ) - } - } yield () - - private def monitorDeleteVm(params: PollVmParams)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - wsmApi <- buildWsmControlledResourceApiClient - getWsmJobResult = F.delay(wsmApi.getDeleteAzureVmResult(params.workspaceId.value, params.jobId.value)) - - _ <- F.sleep( - config.deleteVmPollConfig.initialDelay - ) - - resp <- streamFUntilDone( - getWsmJobResult, - config.deleteVmPollConfig.maxAttempts, - config.deleteVmPollConfig.interval - ).compile.lastOrError - - _ <- resp.getJobReport.getStatus match { - case JobReport.StatusEnum.SUCCEEDED => - for { - _ <- logger.info(ctx.loggingCtx)( - s"vm ${params.runtime.id} is deleted successfully" - ) - } yield () - case JobReport.StatusEnum.FAILED => - logger.error(s"Wsm deleteVm job failed due to ${resp.getErrorReport.getMessage}") >> F.raiseError[Unit]( - AzureRuntimeDeletionError( - params.runtime.id, - params.diskId, - params.workspaceId, - s"WSM delete VM job failed due to ${resp.getErrorReport.getMessage}" - ) - ) - case JobReport.StatusEnum.RUNNING => - F.raiseError[Unit]( - AzureRuntimeDeletionError( - params.runtime.id, - params.diskId, - params.workspaceId, - s"WSM delete VM job was not completed within ${config.deleteVmPollConfig.maxAttempts} attempts with ${config.deleteVmPollConfig.interval} delay" - ) - ) - } - } yield () - - private def monitorDeleteStorageContainer(params: PollStorageContainerParams)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - wsmApi <- buildWsmControlledResourceApiClient - getWsmJobResult = F.delay( - wsmApi.getDeleteAzureStorageContainerResult(params.workspaceId.value, params.jobId.value) - ) - - _ <- F.sleep( - config.deleteStorageContainerPollConfig.initialDelay - ) - - // it does not take long to delete a storage container - resp <- streamFUntilDone( - getWsmJobResult, - config.deleteStorageContainerPollConfig.maxAttempts, - config.deleteStorageContainerPollConfig.interval - ).compile.lastOrError - - _ <- resp.getJobReport.getStatus match { - case JobReport.StatusEnum.SUCCEEDED => - for { - _ <- logger.info(ctx.loggingCtx)( - s"storage container for runtime ${params.runtime.id} is deleted successfully" - ) - } yield () - case JobReport.StatusEnum.FAILED => - logger.error(s"Wsm deleteStorageContainer job failed due to ${resp.getErrorReport.getMessage}") >> F - .raiseError[Unit]( - AzureRuntimeDeletionError( - params.runtime.id, - params.diskId, - params.workspaceId, - s"WSM storage container delete job failed due to ${resp.getErrorReport.getMessage} for runtime ${params.runtime.id}" - ) - ) - case JobReport.StatusEnum.RUNNING => - F.raiseError[Unit]( - AzureRuntimeDeletionError( - params.runtime.id, - params.diskId, - params.workspaceId, - s"WSM delete storage container job was not completed within ${config.deleteStorageContainerPollConfig.maxAttempts} attempts with ${config.deleteStorageContainerPollConfig.interval} delay" - ) - ) - } - } yield () - private def getCommonFieldsForWsmGeneratedClient(name: ControlledResourceName, - resourceDesc: String, - userEmail: WorkbenchEmail - ) = - new ControlledResourceCommonFields() - .accessScope(bio.terra.workspace.model.AccessScope.PRIVATE_ACCESS) - .cloningInstructions(CloningInstructionsEnum.NOTHING) - .description(resourceDesc) - .name(name.value) - .managedBy(bio.terra.workspace.model.ManagedBy.APPLICATION) - .resourceId(UUID.randomUUID()) - .privateResourceUser( - new bio.terra.workspace.model.PrivateResourceUser() - .userName(userEmail.value) - .privateResourceIamRole(bio.terra.workspace.model.ControlledResourceIamRole.WRITER) - ) - - private def monitorCreateRuntime(params: PollRuntimeParams)(implicit ev: Ask[F, AppContext]): F[Unit] = { - val hybridConnectionName = RelayHybridConnectionName(params.runtime.runtimeName.asString) - - for { - ctx <- ev.ask - createVmJobId = WsmJobId(s"create-vm-${params.runtime.id.toString.take(10)}") - wsmControlledResourceClient <- buildWsmControlledResourceApiClient - getWsmVmJobResult = F.delay( - wsmControlledResourceClient.getCreateAzureVmResult(params.workspaceId.value, createVmJobId.value) - ) - - wsmApi <- buildWsmControlledResourceApiClient - - isJupyterUp = jupyterDAO.isProxyAvailable(params.cloudContext, params.runtime.runtimeName) - isWelderUp = welderDao.isProxyAvailable(params.cloudContext, params.runtime.runtimeName) - - taskToRun = for { - // the result of creating the hybrid connection + storage container are necessary for the createVm request - hcPrimaryKey <- azureRelay.createRelayHybridConnection(params.landingZoneResources.relayNamespace, - hybridConnectionName, - params.cloudContext.value - ) - storageContainer <- createStorageContainer(params.runtime, params.landingZoneResources, params.workspaceId) - - _ <- params.createDiskResult.pollParams.traverse(params => monitorCreateDisk(params)) - - vmRequest = setupCreateVmCreateMessage( - params, - storageContainer, - hcPrimaryKey, - createVmJobId, - hybridConnectionName - ) - - _ <- F.delay(wsmApi.createAzureVm(vmRequest, params.workspaceId.value)) - - // it takes a while to create Azure VM. Hence sleep sometime before we start polling WSM - _ <- F.sleep( - config.createVmPollConfig.initialDelay - ) - - // Poll the WSM createVm job for completion - resp <- streamFUntilDone( - getWsmVmJobResult, - config.createVmPollConfig.maxAttempts, - config.createVmPollConfig.interval - ).compile.lastOrError - _ <- resp.getJobReport.getStatus match { - case JobReport.StatusEnum.FAILED => - F.raiseError[Unit]( - AzureRuntimeCreationError( - params.runtime.id, - params.workspaceId, - s"Wsm createVm job failed due to ${resp.getErrorReport.getMessage}", - params.useExistingDisk - ) - ) - case JobReport.StatusEnum.RUNNING => - F.raiseError[Unit]( - AzureRuntimeCreationError( - params.runtime.id, - params.workspaceId, - s"Wsm createVm job was not completed within ${config.createVmPollConfig.maxAttempts} attempts with ${config.createVmPollConfig.interval} delay", - params.useExistingDisk - ) - ) - case JobReport.StatusEnum.SUCCEEDED => - val hostIp = s"${params.landingZoneResources.relayNamespace.value}${AzureEnvironmentConverter - .relaySuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}" - for { - now <- nowInstant - _ <- clusterQuery.updateClusterHostIp(params.runtime.id, Some(IP(hostIp)), now).transaction - // then poll the azure VM for Running status, retrieving the final azure representation - _ <- streamUntilDoneOrTimeout( - isJupyterUp, - config.createVmPollConfig.maxAttempts, - config.createVmPollConfig.interval, - s"Jupyter was not running within ${config.createVmPollConfig.maxAttempts} attempts with ${config.createVmPollConfig.interval} delay" - ) - _ <- streamUntilDoneOrTimeout( - isWelderUp, - config.createVmPollConfig.maxAttempts, - config.createVmPollConfig.interval, - s"Welder was not running within ${config.createVmPollConfig.maxAttempts} attempts with ${config.createVmPollConfig.interval} delay" - ) - _ <- clusterQuery.setToRunning(params.runtime.id, IP(hostIp), now).transaction - unsanitizedRegion = resp.getAzureVm.getAttributes.getRegion - region = if (unsanitizedRegion == null) None else Some(RegionName(unsanitizedRegion)) - // Update runtime region to the VM region - _ <- RuntimeConfigQueries - .updateRegion(params.runtime.runtimeConfigId, region) - .transaction - _ <- logger.info(ctx.loggingCtx)("runtime is ready") - } yield () - } - } yield () - - _ <- asyncTasks.offer( - Task( - ctx.traceId, - taskToRun, - Some(e => - handleAzureRuntimeCreationError( - AzureRuntimeCreationError(params.runtime.id, params.workspaceId, e.getMessage, params.useExistingDisk), - ctx.now - ) - ), - ctx.now, - TaskMetricsTags("createRuntimeV2", None, Some(false), CloudProvider.Azure) - ) - ) - } yield () - } - - override def deleteAndPollRuntime(msg: DeleteAzureRuntimeMessage)(implicit ev: Ask[F, AppContext]): F[Unit] = { - for { - // Perform initial db retrievals - ctx <- ev.ask - - runtimeOpt <- dbRef.inTransaction(clusterQuery.getClusterById(msg.runtimeId)) - runtime <- F.fromOption(runtimeOpt, PubsubHandleMessageError.ClusterNotFound(msg.runtimeId, msg)) - auth <- samDAO.getLeoAuthToken - wsmApi <- buildWsmControlledResourceApiClient - - // Grab the cloud context for the runtime - cloudContext <- runtime.cloudContext match { - case _: CloudContext.Gcp => - F.raiseError[AzureCloudContext]( - PubsubHandleMessageError.ClusterError(msg.runtimeId, - ctx.traceId, - "Azure runtime should not have GCP cloud context" - ) - ) - case x: CloudContext.Azure => F.pure(x.value) - } - - // Delete the storage container in WSM if it exists - storageContainerResourceOpt <- controlledResourceQuery - .getWsmRecordForRuntime(runtime.id, WsmResourceType.AzureStorageContainer) - .transaction - - deleteStorageContainerActionOpt = storageContainerResourceOpt.fold { - // if the storage container hasn't been created in WSM yet, skip storage container deletion - logger - .info( - s"No storage container resource found for runtime ${msg.runtimeId.toString} in ${msg.workspaceId.value}. No-op for wsmApi.deleteAzureStorageContainer." - ) - .map(_ => none[PollStorageContainerParams]) - } { storageContainerResourceRecord => - val deleteJobControl = getDeleteControlledResourceRequest() - F - .delay( - wsmApi.deleteAzureStorageContainer(deleteJobControl, - msg.workspaceId.value, - storageContainerResourceRecord.resourceId.value - ) - ) - .map(_ => - Some( - PollStorageContainerParams(msg.workspaceId, - WsmJobId(deleteJobControl.getJobControl.getId), - runtime, - msg.diskIdToDelete - ) - ) - ) - } - - // Query the Landing Zone service for the landing zone resources - landingZoneResources <- wsmDao.getLandingZoneResources(msg.billingProfileId, auth) - - // Delete hybrid connection for this VM, delayed later for async task queue - deleteHybridConnectionAction = azureRelay.deleteRelayHybridConnection( - landingZoneResources.relayNamespace, - RelayHybridConnectionName(runtime.runtimeName.asString), - cloudContext - ) - - // if there's a disk to delete, find the disk record associated with the runtime - // - if there's a disk record, then delete in WSM - // - update the disk's Leo state if it exists in leo AND the user specifies deletion - - // Perform lookups needed to get wsm disk information from DB - diskRecordOpt <- msg.diskIdToDelete.flatTraverse { _ => - controlledResourceQuery - .getWsmRecordForRuntime(msg.runtimeId, WsmResourceType.AzureDisk) - .transaction - } - - // Formulate the WSM call and polling params for later use in async disk deletion - wsmDeleteDiskActionOpt = diskRecordOpt.fold { - // if the disk hasn't been created in WSM yet, skip disk deletion - logger - .info( - s"No disk resource found for runtime ${msg.runtimeId.toString} in workspace ${msg.workspaceId.value}. No-op for wsmApi.deleteAzureDisk." - ) - .map(_ => none[PollDeleteDiskParams]) - } { wsmDiskRecord => - for { - deleteJobControl <- F.delay(getDeleteControlledResourceRequest()) - _ <- F.delay( - wsmApi.deleteAzureDisk(deleteJobControl, msg.workspaceId.value, wsmDiskRecord.resourceId.value) - ) - } yield Some( - PollDeleteDiskParams(msg.workspaceId, - WsmJobId(deleteJobControl.getJobControl.getId), - msg.diskIdToDelete, - runtime, - wsmDiskRecord.resourceId - ) - ) - } - - // Even if there is no wsm record yet, we may still have a leo disk record to clean up - leoDiskCleanupActionOpt = msg.diskIdToDelete.traverse { diskId => - dbRef.inTransaction(persistentDiskQuery.delete(diskId, ctx.now)) - } - - // Formulate the WSM call and polling params for later use in async VM deletion - deleteVmActionOpt = msg.wsmResourceId.fold { - // Error'd runtimes might not have a WSM resourceId. We still want deletion to succeed in this case. - logger - .info(ctx.loggingCtx)( - s"No VM wsmResourceId found for delete azure runtime msg $msg. No-op for wsmApi.deleteAzureVm." - ) - .map(_ => none[PollVmParams]) - } { resourceId => - for { - deleteJobControl <- F.delay(getDeleteControlledResourceRequest()) - _ <- F.delay(wsmApi.deleteAzureVm(deleteJobControl, msg.workspaceId.value, resourceId.value)) - } yield Some( - PollVmParams(msg.workspaceId, WsmJobId(deleteJobControl.getJobControl.getId), runtime, msg.diskIdToDelete) - ) - } - - // Delete and poll on all associated resources in WSM, and then mark the runtime as deleted - // The resources handled here are Storage container, Hybrid connection, Virtual Machine, and Disk - // It is worth noting disk error handling is a bit special as it has its own table, the polling method for that is responsible for updating the disk to error state - // A possible optimization is pairing each (wsmCall, pollingOnCompletion) into operations and running them in parallel - // This would ensure that the ordering in conjunction with a single failure doesn't prevent as many things as possible from being deleted - taskToRun = for { - _ <- logger.info(ctx.loggingCtx)( - s"beginning azure storage container deletion and polling for runtime ${msg.runtimeId} in workspace ${msg.workspaceId}" - ) - pollStorageContainerParamsOpt <- deleteStorageContainerActionOpt - _ <- pollStorageContainerParamsOpt.traverse(params => monitorDeleteStorageContainer(params)) - - _ <- logger.info(ctx.loggingCtx)( - s"beginning hybrid connection deletion for runtime ${msg.runtimeId} in workspace ${msg.workspaceId}" - ) - _ <- deleteHybridConnectionAction - - // Vm must be done before disk - _ <- logger.info(ctx.loggingCtx)( - s"beginning azure vm deletion and polling for runtime ${msg.runtimeId} in workspace ${msg.workspaceId}" - ) - pollDeleteVmParamsOpt <- deleteVmActionOpt - _ <- pollDeleteVmParamsOpt.traverse(params => monitorDeleteVm(params)) - - // if Some(leodisk) None(wsmDisk) -> mark deleted - // if Some(leodisk) Some(wsmDisk) -> poll then mark deleted - // if none(leodisk) Some(wsmDisk) -> do not poll, user wants to keep disk, noop - // if none(LeoDisk) none(wsmDisk) -> noop - _ <- logger.info(ctx.loggingCtx)( - s"beginning azure disk deletion and polling for runtime ${msg.runtimeId} in workspace ${msg.workspaceId}" - ) - pollDiskParamsOpt <- - if (msg.diskIdToDelete.isDefined) wsmDeleteDiskActionOpt else F.delay(none[PollDeleteDiskParams]) - _ <- pollDiskParamsOpt.traverse(params => monitorDeleteDisk(params)) - _ <- leoDiskCleanupActionOpt - - _ <- dbRef.inTransaction(clusterQuery.completeDeletion(runtime.id, ctx.now)) - _ <- logger.info(ctx.loggingCtx)( - s"runtime ${msg.runtimeId} with name ${runtime.runtimeName.asString} is deleted successfully" - ) - } yield () - - _ <- asyncTasks.offer( - Task( - ctx.traceId, - taskToRun, - Some { e => - handleAzureRuntimeDeletionError( - AzureRuntimeDeletionError(msg.runtimeId, - msg.diskIdToDelete, - msg.workspaceId, - s"Fail to delete runtime due to ${e.getMessage}" - ) - ) - }, - ctx.now, - TaskMetricsTags("deleteRuntimeV2", None, Some(false), CloudProvider.Azure) - ) - ) - } yield () - } - - def handleAzureRuntimeDeletionError(e: AzureRuntimeDeletionError)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- ev.ask - _ <- logger.error(ctx.loggingCtx, e)(s"Failed to delete Azure VM ${e.runtimeId}") - _ <- clusterErrorQuery - .save(e.runtimeId, RuntimeError(e.errorMsg.take(1024), None, ctx.now, traceId = Some(ctx.traceId))) - .transaction - _ <- clusterQuery.updateClusterStatus(e.runtimeId, RuntimeStatus.Error, ctx.now).transaction - _ <- e.diskId match { - case Some(diskId) => persistentDiskQuery.updateStatus(diskId, DiskStatus.Error, ctx.now).transaction - case None => F.unit - } - } yield () - - def handleAzureRuntimeStartError(e: AzureRuntimeStartingError, now: Instant)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - _ <- logger.error(ctx.loggingCtx)(s"Failed to start Azure VM ${e.runtimeId}") - _ <- clusterErrorQuery - .save(e.runtimeId, RuntimeError(e.errorMsg.take(1024), None, now)) - .transaction - } yield () - - def handleAzureRuntimeStopError(e: AzureRuntimeStoppingError, now: Instant)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - _ <- logger.error(ctx.loggingCtx)(s"Failed to stop Azure VM ${e.runtimeId}") - _ <- clusterErrorQuery - .save(e.runtimeId, RuntimeError(e.errorMsg.take(1024), None, now)) - .transaction - } yield () - - override def handleAzureRuntimeCreationError(e: AzureRuntimeCreationError, now: Instant)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - _ <- logger.error(ctx.loggingCtx, e)(s"Failed to create Azure VM ${e.runtimeId}") - _ <- clusterErrorQuery - .save(e.runtimeId, RuntimeError(e.errorMsg.take(1024), None, now)) - .transaction - _ <- clusterQuery.updateClusterStatus(e.runtimeId, RuntimeStatus.Error, now).transaction - - diskIdOpt <- clusterQuery.getDiskId(e.runtimeId).transaction - - _ <- (e.useExistingDisk, diskIdOpt) match { - // disk was supposed to be created and was - case (false, Some(diskId)) => - for { - diskRecordOpt <- controlledResourceQuery - .getWsmRecordForRuntime(e.runtimeId, WsmResourceType.AzureDisk) - .transaction - _ <- diskRecordOpt match { - // if there is a disk record, the disk finished creating, so it must be deleted in WSM - case Some(diskRecord) => - for { - _ <- deleteDiskInWSM(diskId, diskRecord.resourceId, e.workspaceId, Some(e.runtimeId)) - } yield () - case _ => - for { - _ <- logger.info( - s"No disk resource found for runtime ${e.runtimeId.toString} in ${e.workspaceId.value}. No-op for wsmDao.deleteDisk." - ) - _ <- clusterQuery.setDiskDeleted(e.runtimeId, now).transaction - } yield () - } - } yield () - - // disk was supposed to be created and wasn't - case (false, None) => - logger.info( - s"No disk resource found for runtime ${e.runtimeId.toString} in ${e.workspaceId.value}. No-op for wsmDao.deleteDisk." - ) - // no disk created - case (true, _) => F.unit - } - } yield () - - private def deleteDiskInWSM(diskId: DiskId, - wsmResourceId: WsmControlledResourceId, - workspaceId: WorkspaceId, - runtimeId: Option[Long] - )(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - ctx <- ev.ask - jobId = getWsmJobId("delete-disk", wsmResourceId) - - _ <- logger.info(ctx.loggingCtx)(s"Sending WSM delete message for disk resource ${wsmResourceId.value}") - wsmControlledResourceClient <- buildWsmControlledResourceApiClient - deleteDiskBody = getDeleteControlledResourceRequest(jobId) - _ <- F - .delay(wsmControlledResourceClient.deleteAzureDisk(deleteDiskBody, workspaceId.value, wsmResourceId.value)) - .void - .adaptError(e => - AzureDiskDeletionError( - diskId, - wsmResourceId, - workspaceId, - s"${ctx.traceId.asString} | WSM call to delete disk failed due to ${e.getMessage}. Please retry delete again" - ) - ) - getDeleteJobResult = F.delay(wsmControlledResourceClient.getDeleteAzureDiskResult(workspaceId.value, jobId.value)) - - // We need to wait until WSM deletion job to be done to update the database - taskToRun = for { - resp <- streamFUntilDone( - getDeleteJobResult, - config.deleteDiskPollConfig.maxAttempts, - config.deleteDiskPollConfig.interval - ).compile.lastOrError - - _ <- resp.getJobReport.getStatus match { - case JobReport.StatusEnum.SUCCEEDED => - for { - _ <- logger.info(ctx.loggingCtx)(s"disk ${diskId.value} is deleted successfully") - _ <- runtimeId match { - case Some(runtimeId) => clusterQuery.setDiskDeleted(runtimeId, ctx.now).transaction - case _ => dbRef.inTransaction(persistentDiskQuery.delete(diskId, ctx.now)).void - } - } yield () - case JobReport.StatusEnum.FAILED => - F.raiseError[Unit]( - AzureDiskDeletionError( - diskId, - wsmResourceId, - workspaceId, - s"WSM deleteDisk job failed due to ${resp.getErrorReport.getMessage}" - ) - ) - case JobReport.StatusEnum.RUNNING => - F.raiseError[Unit]( - AzureDiskDeletionError( - diskId, - wsmResourceId, - workspaceId, - s"Wsm deleteDisk job was not completed within ${config.deleteDiskPollConfig.maxAttempts} attempts with ${config.deleteDiskPollConfig.interval} delay" - ) - ) - } - } yield () - - _ <- asyncTasks.offer( - Task( - ctx.traceId, - taskToRun, - Some { e => - handleAzureDiskDeletionError( - AzureDiskDeletionError(diskId, wsmResourceId, workspaceId, s"Fail to delete disk due to ${e.getMessage}") - ) - }, - ctx.now, - TaskMetricsTags("deleteDiskV2", None, Some(false), CloudProvider.Azure) - ) - ) - } yield () - - override def deleteDisk(msg: DeleteDiskV2Message)(implicit ev: Ask[F, AppContext]): F[Unit] = - for { - ctx <- ev.ask - - _ <- msg.wsmResourceId match { - case Some(wsmResourceId) => - deleteDiskInWSM(msg.diskId, wsmResourceId, msg.workspaceId, None) - case None => - for { - _ <- logger.info(s"No WSM resource found for Azure disk ${msg.diskId}, skipping deletion in WSM") - _ <- dbRef.inTransaction(persistentDiskQuery.delete(msg.diskId, ctx.now)) - _ <- logger.info(ctx.loggingCtx)(s"disk ${msg.diskId.value} is deleted successfully") - } yield () - } - } yield () - - def handleAzureDiskDeletionError(e: AzureDiskDeletionError)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- ev.ask - _ <- logger.error(ctx.loggingCtx, e)(s"Failed to delete Azure disk ${e.diskId}") - _ <- dbRef.inTransaction(persistentDiskQuery.updateStatus(e.diskId, DiskStatus.Error, ctx.now)) - } yield () - - def getWsmJobId(jobName: String, resourceId: WsmControlledResourceId): WsmJobId = WsmJobId( - s"$jobName-${resourceId.value.toString.take(10)}" - ) - - private def buildWsmControlledResourceApiClient(implicit ev: Ask[F, AppContext]): F[ControlledAzureResourceApi] = - for { - auth <- samDAO.getLeoAuthToken - token <- auth.credentials match { - case org.http4s.Credentials.Token(_, token) => F.pure(token) - case _ => F.raiseError[String](new RuntimeException("Could not obtain Leo auth token")) - } - wsmControlledResourceClient <- wsmClientProvider.getControlledAzureResourceApi(token) - } yield wsmControlledResourceClient - - private def getDeleteControlledResourceRequest( - jobId: WsmJobId = WsmJobId(UUID.randomUUID().toString) - ): bio.terra.workspace.model.DeleteControlledAzureResourceRequest = { - val jobControl = new JobControl() - .id(jobId.value) - val deleteControlledResource = new bio.terra.workspace.model.DeleteControlledAzureResourceRequest() - .jobControl(jobControl) - - deleteControlledResource - } -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala index 239eac3924..31b84c0b60 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala @@ -1,55 +1,13 @@ package org.broadinstitute.dsde.workbench.leonardo package util -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, ContainerName} +import org.broadinstitute.dsde.workbench.azure.ContainerName import org.broadinstitute.dsde.workbench.leonardo.WsmControlledResourceId import org.broadinstitute.dsde.workbench.leonardo.config.PersistentDiskConfig import org.broadinstitute.dsde.workbench.leonardo.dao.{CreateDiskForRuntimeResult, StorageContainerResponse} -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAzureRuntimeMessage, DeleteAzureRuntimeMessage, DeleteDiskV2Message} import org.broadinstitute.dsde.workbench.leonardo.monitor.PollMonitorConfig -import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError.{AzureRuntimeCreationError, AzureRuntimeDeletionError, AzureRuntimeStartingError, AzureRuntimeStoppingError} import org.http4s.Uri -import java.security.SecureRandom -import java.time.Instant - -trait AzurePubsubHandlerAlgebra[F[_]] { - - /** Creates an Azure VM but doesn't wait for its completion. - * This includes creation of all child Azure resources (disk, network, ip), and assumes these are created synchronously - * */ - def createAndPollRuntime(msg: CreateAzureRuntimeMessage)(implicit ev: Ask[F, AppContext]): F[Unit] - - def deleteAndPollRuntime(msg: DeleteAzureRuntimeMessage)(implicit ev: Ask[F, AppContext]): F[Unit] - - def startAndMonitorRuntime(runtime: Runtime, azureCloudContext: AzureCloudContext)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - def stopAndMonitorRuntime(runtime: Runtime, azureCloudContext: AzureCloudContext)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - def deleteDisk(msg: DeleteDiskV2Message)(implicit ev: Ask[F, AppContext]): F[Unit] - - def handleAzureRuntimeStartError(e: AzureRuntimeStartingError, now: Instant)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - def handleAzureRuntimeStopError(e: AzureRuntimeStoppingError, now: Instant)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - def handleAzureRuntimeCreationError(e: AzureRuntimeCreationError, pubsubMessageSentTime: Instant)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - def handleAzureRuntimeDeletionError(e: AzureRuntimeDeletionError)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - -} final case class CreateAzureDiskParams(workspaceId: WorkspaceId, runtime: Runtime, @@ -145,52 +103,3 @@ final case class AzurePubsubHandlerConfig(samUrl: Uri, ) { def welderImage: String = s"$welderAcrUri:$welderImageHash" } -object AzurePubsubHandler { - private[util] def generateAzureVMSecurePassword(passwordLength: Int): String = { - // Azure is enforcing the following constraints for password generation - // Passwords must not include reserved words or unsupported characters. - // Password must have 3 of the following: 1 lower case character, 1 upper case character, 1 number, and 1 special character that is not '\'or '-'. - // The value must be between 12 and 123 characters long. - - val lowerLetters = 'a' to 'z' - val upperLetters = 'A' to 'Z' - val numbers = '0' to '9' - val specialChars = IndexedSeq('!', '@', '#', '$', '&', '*', '?', '^', '(', ')') - val fullCharset = lowerLetters ++ upperLetters ++ numbers ++ specialChars - - val random = new SecureRandom() - - def pickRandomChars(charSet: IndexedSeq[Char], size: Int): List[Char] = - Iterator - .continually(charSet(random.nextInt(charSet.length))) - .take(size) - .toList - - var password: String = pickRandomChars(fullCharset, passwordLength).mkString - // Keep generating passwords until we find one that has all of the required characters - // This is safer than picking from each subset and then shuffling - - var isPasswordValid: Boolean = - password.exists(_.isLower) && password.exists(_.isUpper) && password.exists(_.isDigit) && password.exists( - specialChars.contains - ) - - while (isPasswordValid == false) { - password = pickRandomChars(fullCharset, passwordLength).mkString - isPasswordValid = - password.exists(_.isLower) && password.exists(_.isUpper) && password.exists(_.isDigit) && password.exists( - specialChars.contains - ) - - } - password - } - - private[util] def getAzureVMSecurePassword(environment: String, sharedPassword: String): String = - // Generate random password for Azure VM in production, for the other lower level envs we can used the shared password - // The password must be between 12 and 123 characters long. We are choosing 25 here - environment match { - case "prod" => generateAzureVMSecurePassword(25) - case _ => sharedPassword - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala index 13749072b8..a51703cc5a 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala @@ -3,21 +3,16 @@ package monitor import akka.actor.ActorSystem import akka.testkit.TestKit -import bio.terra.workspace.client.ApiException -import bio.terra.workspace.model.JobReport import cats.data.Kleisli import cats.effect.IO import cats.effect.std.Queue import cats.mtl.Ask import cats.syntax.all._ -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes import com.github.benmanes.caffeine.cache.Caffeine import com.google.api.gax.longrunning.OperationFuture import com.google.cloud.compute.v1.{Disk, Operation} import com.google.protobuf.Timestamp import fs2.Stream -import org.broadinstitute.dsde.workbench.azure.mock.{FakeAzureRelayService, FakeAzureVmService} -import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, AzureRelayService, AzureVmService} import org.broadinstitute.dsde.workbench.google.GoogleStorageDAO import org.broadinstitute.dsde.workbench.google.mock._ import org.broadinstitute.dsde.workbench.google2.KubernetesModels.PodStatus @@ -30,7 +25,7 @@ import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{makeApp, makeKubeCluster, makeNodepool, makeService} import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.BootSource import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.{ApplicationConfig, Config} +import org.broadinstitute.dsde.workbench.leonardo.config.Config import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.http._ @@ -50,12 +45,9 @@ import org.scalatest.BeforeAndAfterEach import org.scalatest.concurrent._ import org.scalatest.flatspec.AnyFlatSpecLike import org.scalatest.matchers.should.Matchers -import org.scalatest.prop.TableDrivenPropertyChecks._ import org.scalatestplus.mockito.MockitoSugar import scalacache.caffeine.CaffeineCache -import java.net.URL -import java.nio.file.Paths import java.time.Instant import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global @@ -76,8 +68,6 @@ class LeoPubsubMessageSubscriberSpec val mockWelderDAO = mock[WelderDAO[IO]] - val mockAzurePubsubHandlerInterp = mock[AzurePubsubHandlerAlgebra[IO]] - val mockGoogleDirectoryDAO = new MockGoogleDirectoryDAO() { override def isGroupMember(groupEmail: WorkbenchEmail, memberEmail: WorkbenchEmail @@ -1954,230 +1944,6 @@ class LeoPubsubMessageSubscriberSpec res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) } - it should "handle top-level error in create azure vm properly" in isolatedDbTest { - val exceptionMsg = "test exception" - - val (mockWsm, controlledResourceApi, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider() - when { - controlledResourceApi.getCreateAzureVmResult(any, any) - } thenThrow { - new ApiException(exceptionMsg) - } - val mockAckConsumer = mock[AckHandler] - val queue = makeTaskQueue() - val leoSubscriber = makeLeoSubscriber(azureInterp = - makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm), - asyncTaskQueue = queue - ) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - jobId <- IO.delay(UUID.randomUUID()) - msg = CreateAzureRuntimeMessage(runtime.id, - workspaceId, - false, - None, - "WorkspaceName", - BillingProfileId("spend-profile") - ) - - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, None, instantTimestamp, mockAckConsumer)) - - assertions = for { - error <- clusterErrorQuery.get(runtime.id).transaction - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - } yield { - getRuntimeOpt.map(_.status) shouldBe Some(RuntimeStatus.Error) - error.length shouldBe 1 - error.map(_.errorMessage).head should include(exceptionMsg) - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle top-level error in create azure disk properly" in isolatedDbTest { - val mockAckConsumer = mock[AckHandler] - val queue = makeTaskQueue() - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(JobReport.StatusEnum.FAILED) - val leoSubscriber = makeLeoSubscriber(azureInterp = - makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm), - asyncTaskQueue = queue - ) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - msg = CreateAzureRuntimeMessage(runtime.id, - workspaceId, - false, - None, - "WorkspaceName", - BillingProfileId("spend-profile") - ) - - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, None, instantTimestamp, mockAckConsumer)) - - assertions = for { - error <- clusterErrorQuery.get(runtime.id).transaction - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - } yield { - getRuntimeOpt.map(_.status) shouldBe Some(RuntimeStatus.Error) - error.length shouldBe 1 - error.map(_.errorMessage).head should include("Wsm createDisk job failed due to") - - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle delete azure vm failure properly" in isolatedDbTest { - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(vmJobStatus = JobReport.StatusEnum.FAILED) - val mockAckConsumer = mock[AckHandler] - val queue = makeTaskQueue() - val leoSubscriber = - makeLeoSubscriber(azureInterp = makeAzureInterp(asyncTaskQueue = queue, mockWsmClient = mockWsm), - asyncTaskQueue = queue - ) - - val res = - for { - ctx <- appContext.ask[AppContext] - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - vmResourceId = WsmControlledResourceId(UUID.randomUUID()) - - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(vmResourceId), - BillingProfileId("spend-profile"), - None - ) - - assertions = for { - errors <- clusterErrorQuery.get(runtime.id).transaction - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - } yield { - getRuntimeOpt.map(_.status) shouldBe Some(RuntimeStatus.Error) - errors.length shouldBe 1 - val error = errors.head - error.errorMessage should include( - s"WSM delete VM job failed due to" - ) - error.traceId shouldBe (Some(ctx.traceId)) - } - - _ <- leoSubscriber.messageHandler(ReceivedMessage(msg, Some(ctx.traceId), instantTimestamp, mockAckConsumer)) - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle Azure StartRuntimeMessage and start runtime" in isolatedDbTest { - val azurePubsubHandlerMock = mock[AzurePubsubHandlerInterp[IO]] - when(azurePubsubHandlerMock.startAndMonitorRuntime(any[Runtime], any[AzureCloudContext])(any())) - .thenReturn(IO.unit) - val leoSubscriber = makeLeoSubscriber(azureInterp = azurePubsubHandlerMock) - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime <- IO( - makeCluster(1) - .copy(status = RuntimeStatus.Starting, cloudContext = CloudContext.Azure(azureCloudContext)) - .saveWithRuntimeConfig(azureRuntimeConfig) - ) - tr <- traceId.ask[TraceId] - _ <- leoSubscriber.messageResponder(StartRuntimeMessage(runtime.id, Some(tr))) - - } yield verify(azurePubsubHandlerMock, times(1)) - .startAndMonitorRuntime(any[Runtime], any[AzureCloudContext])(any()) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle Azure StopRuntimeMessage and stop runtime" in isolatedDbTest { - val azurePubsubHandlerMock = mock[AzurePubsubHandlerInterp[IO]] - when(azurePubsubHandlerMock.stopAndMonitorRuntime(any[Runtime], any[AzureCloudContext])(any())) - .thenReturn(IO.unit) - val leoSubscriber = makeLeoSubscriber(azureInterp = azurePubsubHandlerMock) - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime <- IO( - makeCluster(1) - .copy(status = RuntimeStatus.Stopping, cloudContext = CloudContext.Azure(azureCloudContext)) - .saveWithRuntimeConfig(azureRuntimeConfig) - ) - tr <- traceId.ask[TraceId] - _ <- leoSubscriber.messageResponder(StopRuntimeMessage(runtime.id, Some(tr))) - - } yield verify(azurePubsubHandlerMock, times(1)).stopAndMonitorRuntime(any[Runtime], any[AzureCloudContext])(any()) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle Azure delete disk message" in isolatedDbTest { - val azurePubsubHandlerMock = mock[AzurePubsubHandlerInterp[IO]] - when(azurePubsubHandlerMock.deleteDisk(any[DeleteDiskV2Message])(any())) - .thenReturn(IO.unit) - val leoSubscriber = makeLeoSubscriber(azureInterp = azurePubsubHandlerMock) - val res = for { - disk <- makePersistentDisk(cloudContextOpt = Some(cloudContextAzure)).copy(status = DiskStatus.Ready).save() - _ <- leoSubscriber.messageResponder( - DeleteDiskV2Message(disk.id, disk.workspaceId.get, disk.cloudContext, disk.wsmResourceId, None) - ) - - } yield verify(azurePubsubHandlerMock, times(1)).deleteDisk(any[DeleteDiskV2Message])(any()) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "create a metric for a successful and failed condition" in isolatedDbTest { val savedCluster1 = makeKubeCluster(1).save() val savedNodepool1 = makeNodepool(1, savedCluster1.id).save() @@ -2256,8 +2022,7 @@ class LeoPubsubMessageSubscriberSpec diskService: GoogleDiskService[IO] = MockGoogleDiskService, storageService: GoogleStorageService[IO] = FakeGoogleStorageService, dataprocRuntimeAlgebra: RuntimeAlgebra[IO] = dataprocInterp, - gceRuntimeAlgebra: RuntimeAlgebra[IO] = gceInterp, - azureInterp: AzurePubsubHandlerAlgebra[IO] = makeAzureInterp() + gceRuntimeAlgebra: RuntimeAlgebra[IO] = gceInterp )(implicit metrics: OpenTelemetryMetrics[IO]): LeoPubsubMessageSubscriber[IO] = { implicit val runtimeInstances = new RuntimeInstances[IO](dataprocRuntimeAlgebra, gceRuntimeAlgebra) @@ -2294,7 +2059,6 @@ class LeoPubsubMessageSubscriberSpec cloudSubscriber, asyncTaskQueue, MockAuthProvider, - azureInterp, operationFutureCache, subscriberServicesRegistry, MockSamService @@ -2303,35 +2067,6 @@ class LeoPubsubMessageSubscriberSpec val (mockWsm, mockControlledResourceApi, mockResourceApi, workspaceApi) = AzureTestUtils.setUpMockWsmApiClientProvider() - // Needs to be made for each test its used in, otherwise queue will overlap - def makeAzureInterp(asyncTaskQueue: Queue[IO, Task[IO]] = makeTaskQueue(), - relayService: AzureRelayService[IO] = FakeAzureRelayService, - wsmDAO: MockWsmDAO = new MockWsmDAO, - azureVmService: AzureVmService[IO] = FakeAzureVmService, - mockWsmClient: WsmApiClientProvider[IO] = mockWsm - ): AzurePubsubHandlerAlgebra[IO] = - new AzurePubsubHandlerInterp[IO]( - ConfigReader.appConfig.azure.pubsubHandler, - new ApplicationConfig("test", - GoogleProject("test"), - Paths.get("x.y"), - WorkbenchEmail("z@x.y"), - new URL("https://leonardo.foo.org"), - "dev", - 0L - ), - contentSecurityPolicy, - asyncTaskQueue, - wsmDAO, - new MockSamDAO(), - new MockWelderDAO(), - new MockJupyterDAO(), - relayService, - azureVmService, - refererConfig, - mockWsmClient - ) - def makeTaskQueue(): Queue[IO, Task[IO]] = Queue.bounded[IO, Task[IO]](10).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala deleted file mode 100644 index 8942f5d29d..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerSpec.scala +++ /dev/null @@ -1,1636 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package util - -import akka.actor.ActorSystem -import akka.testkit.TestKit -import bio.terra.workspace.client.ApiException -import bio.terra.workspace.model._ -import cats.effect.IO -import cats.effect.std.Queue -import cats.mtl.Ask -import com.azure.resourcemanager.compute.models.{PowerState, VirtualMachine, VirtualMachineSizeTypes} -import com.azure.resourcemanager.network.models.PublicIpAddress -import org.broadinstitute.dsde.workbench.azure.mock.{FakeAzureRelayService, FakeAzureVmService} -import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, AzureRelayService, AzureVmService} -import org.broadinstitute.dsde.workbench.google2.{MachineTypeName, RegionName} -import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor.Task -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.PrivateAzureStorageAccountSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.ApplicationConfig -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http.{ConfigReader, _} -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage._ -import org.broadinstitute.dsde.workbench.leonardo.monitor.PubsubHandleMessageError._ -import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} -import org.broadinstitute.dsde.workbench.util2.InstanceName -import org.http4s.headers.Authorization -import org.mockito.ArgumentMatchers.{any, eq => mockitoEq} -import org.mockito.Mockito.{spy, times, verify, when} -import org.scalatest.concurrent.Eventually -import org.scalatest.flatspec.AnyFlatSpecLike -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.mockito.MockitoSugar -import reactor.core.publisher.Mono - -import java.net.URL -import java.nio.file.Paths -import java.time.ZonedDateTime -import java.util.UUID -import scala.concurrent.ExecutionContext.Implicits.global - -class AzurePubsubHandlerSpec - extends TestKit(ActorSystem("leonardotest")) - with AnyFlatSpecLike - with TestComponent - with Matchers - with MockitoSugar - with Eventually - with LeonardoTestSuite { - val storageContainerResourceId = WsmControlledResourceId(UUID.randomUUID()) - - val (mockWsm, mockControlledResourceApi, mockResourceApi, workspaceApi) = - AzureTestUtils.setUpMockWsmApiClientProvider() - - it should "generate an Azure VM password properly" in { - // Test this with a short password to make sure that the while loop works properly - val password = AzurePubsubHandler.generateAzureVMSecurePassword(4) - password.length shouldBe 4 - password.exists(_.isLower) shouldBe true - password.exists(_.isUpper) shouldBe true - password.exists(_.isDigit) shouldBe true - password.exists(IndexedSeq('!', '@', '#', '$', '&', '*', '?', '^', '(', ')').contains) shouldBe true - } - - it should "not use the shared Azure VM credentials in prod" in { - val password = AzurePubsubHandler.getAzureVMSecurePassword("prod", "sharedPassword") - assert(password != "sharedPassword") - password.length shouldBe 25 - } - - it should "create azure vm properly" in isolatedDbTest { - val vmReturn = mock[VirtualMachine] - val ipReturn: PublicIpAddress = mock[PublicIpAddress] - - when(vmReturn.powerState()).thenReturn(PowerState.RUNNING) - - val stubIp = "0.0.0.0" - when(vmReturn.getPrimaryPublicIPAddress()).thenReturn(ipReturn) - when(ipReturn.ipAddress()).thenReturn(stubIp) - - val queue = QueueFactory.asyncTaskQueue() - val azurePubsubHandler = - makeAzurePubsubHandler(asyncTaskQueue = queue) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - getRuntimeConfig <- RuntimeConfigQueries.getRuntimeConfig(getRuntime.runtimeConfigId).transaction - } yield { - getRuntime.asyncRuntimeFields.flatMap(_.hostIp).isDefined shouldBe true - getRuntime.status shouldBe RuntimeStatus.Running - getRuntimeConfig shouldBe azureRuntimeConfig.copy(region = Some(RegionName("southcentralus"))) - } - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, false, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azurePubsubHandler.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - controlledResources <- controlledResourceQuery.getAllForRuntime(runtime.id).transaction - } yield { - controlledResources.length shouldBe 2 - val resourceTypes = controlledResources.map(_.resourceType) - resourceTypes.contains(WsmResourceType.AzureDisk) shouldBe true - resourceTypes.contains(WsmResourceType.AzureStorageContainer) shouldBe true - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "create azure vm properly when WSM is slow" in isolatedDbTest { - val vmReturn = mock[VirtualMachine] - val ipReturn: PublicIpAddress = mock[PublicIpAddress] - - when(vmReturn.powerState()).thenReturn(PowerState.RUNNING) - val (mockWsm, controlledResourceApi, _, _) = - AzureTestUtils.setUpMockWsmApiClientProvider() - - val now = ZonedDateTime.now().toString - - // create vm result - when { - controlledResourceApi.getCreateAzureVmResult(any, any) - } thenAnswer { _ => - new CreatedControlledAzureVmResult() - .jobReport( - new JobReport().status(JobReport.StatusEnum.SUCCEEDED).completed(now) - ) - .azureVm(new AzureVmResource().attributes(new AzureVmAttributes().region("southcentralus"))) - } - - val queue = QueueFactory.asyncTaskQueue() - val azurePubsubHandler = - makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - getRuntimeConfig <- RuntimeConfigQueries.getRuntimeConfig(getRuntime.runtimeConfigId).transaction - } yield { - getRuntime.asyncRuntimeFields.flatMap(_.hostIp).isDefined shouldBe true - getRuntime.status shouldBe RuntimeStatus.Running - getRuntimeConfig shouldBe azureRuntimeConfig.copy(region = Some(RegionName("southcentralus"))) - } - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, false, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azurePubsubHandler.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - controlledResources <- controlledResourceQuery.getAllForRuntime(runtime.id).transaction - } yield { - controlledResources.length shouldBe 2 - val resourceTypes = controlledResources.map(_.resourceType) - resourceTypes.contains(WsmResourceType.AzureDisk) shouldBe true - resourceTypes.contains(WsmResourceType.AzureStorageContainer) shouldBe true - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "create azure vm properly with a persistent disk" in isolatedDbTest { - val vmReturn = mock[VirtualMachine] - val ipReturn: PublicIpAddress = mock[PublicIpAddress] - - when(vmReturn.powerState()).thenReturn(PowerState.RUNNING) - - val stubIp = "0.0.0.0" - when(vmReturn.getPrimaryPublicIPAddress()).thenReturn(ipReturn) - when(ipReturn.ipAddress()).thenReturn(stubIp) - - val queue = QueueFactory.asyncTaskQueue() - val azurePubsubHandler = - makeAzurePubsubHandler(asyncTaskQueue = queue) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Restoring).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime1 = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - runtime2 = makeCluster(2) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - resourceId = WsmControlledResourceId(UUID.randomUUID()) - _ <- controlledResourceQuery - .save(runtime1.id, resourceId, WsmResourceType.AzureDisk) - .transaction - now <- IO.realTimeInstant - _ <- persistentDiskQuery.updateWSMResourceId(disk.id, resourceId, now).transaction - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime2.id).transaction - getRuntime = getRuntimeOpt.get - getRuntimeConfig <- RuntimeConfigQueries.getRuntimeConfig(getRuntime.runtimeConfigId).transaction - } yield { - // check diskId is correct - getRuntime.asyncRuntimeFields.flatMap(_.hostIp).isDefined shouldBe true - getRuntime.status shouldBe RuntimeStatus.Running - getRuntimeConfig shouldBe azureRuntimeConfig.copy(region = Some(RegionName("southcentralus"))) - } - - msg = CreateAzureRuntimeMessage(runtime2.id, workspaceId, true, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azurePubsubHandler.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - controlledResources <- controlledResourceQuery.getAllForRuntime(runtime2.id).transaction - } yield { - controlledResources.length shouldBe 2 - val resourceTypes = controlledResources.map(_.resourceType) - resourceTypes.contains(WsmResourceType.AzureDisk) shouldBe true - resourceTypes.contains(WsmResourceType.AzureStorageContainer) shouldBe true - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "create an azure vm properly with an action managed identity" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val mockSamDAO = new MockSamDAO { - override def getAzureActionManagedIdentity(authHeader: Authorization, - resource: PrivateAzureStorageAccountSamResourceId, - action: PrivateAzureStorageAccountAction - )(implicit ev: Ask[IO, TraceId]): IO[Option[String]] = IO(Some("awesome-identity")) - } - val azurePubsubHandler = - makeAzurePubsubHandler(asyncTaskQueue = queue, samDAO = mockSamDAO) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - getRuntimeConfig <- RuntimeConfigQueries.getRuntimeConfig(getRuntime.runtimeConfigId).transaction - } yield { - getRuntime.asyncRuntimeFields.flatMap(_.hostIp).isDefined shouldBe true - getRuntime.status shouldBe RuntimeStatus.Running - getRuntimeConfig shouldBe azureRuntimeConfig.copy(region = Some(RegionName("southcentralus"))) - } - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, false, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azurePubsubHandler.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - controlledResources <- controlledResourceQuery.getAllForRuntime(runtime.id).transaction - } yield { - controlledResources.length shouldBe 2 - val resourceTypes = controlledResources.map(_.resourceType) - resourceTypes.contains(WsmResourceType.AzureDisk) shouldBe true - resourceTypes.contains(WsmResourceType.AzureStorageContainer) shouldBe true - } - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle azure vm creation failure properly" in isolatedDbTest { - val vmReturn = mock[VirtualMachine] - val ipReturn: PublicIpAddress = mock[PublicIpAddress] - - when(vmReturn.powerState()).thenReturn(PowerState.RUNNING) - - val stubIp = "0.0.0.0" - when(vmReturn.getPrimaryPublicIPAddress()).thenReturn(ipReturn) - when(ipReturn.ipAddress()).thenReturn(stubIp) - - val queue = QueueFactory.asyncTaskQueue() - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(vmJobStatus = JobReport.StatusEnum.FAILED) - - val azurePubsubHandler = - makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - runtimeStatus <- clusterQuery.getClusterStatus(runtime.id).transaction - diskStatus <- persistentDiskQuery.getStatus(disk.id).transaction - } yield { - runtimeStatus shouldBe Some(RuntimeStatus.Error) - diskStatus shouldBe Some(DiskStatus.Deleted) - } - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, false, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azurePubsubHandler.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle azure disk creation failure from wsm properly" in isolatedDbTest { - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(diskJobStatus = JobReport.StatusEnum.FAILED) - - val queue = QueueFactory.asyncTaskQueue() - - val azurePubsubHandler = - makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, false, None, "WorkspaceName", billingProfileId) - - _ <- azurePubsubHandler.createAndPollRuntime(msg) - - assertions = for { - error <- clusterErrorQuery.get(runtime.id).transaction - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - } yield { - getRuntimeOpt.map(_.status) shouldBe Some(RuntimeStatus.Error) - error.length shouldBe 1 - error.map(_.errorMessage).head should include("Wsm createDisk job failed due to") - - } - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to create azure vm if welder doesn't come up" in isolatedDbTest { - val vmReturn = mock[VirtualMachine] - val ipReturn: PublicIpAddress = mock[PublicIpAddress] - - when(vmReturn.powerState()).thenReturn(PowerState.RUNNING) - - val stubIp = "0.0.0.0" - when(vmReturn.getPrimaryPublicIPAddress()).thenReturn(ipReturn) - when(ipReturn.ipAddress()).thenReturn(stubIp) - - val queue = QueueFactory.asyncTaskQueue() - val fakeWelderDao = new MockWelderDAO() { - override def isProxyAvailable(cloudContext: CloudContext, clusterName: RuntimeName): IO[Boolean] = IO.pure(false) - } - val azurePubsubHandler = - makeAzurePubsubHandler(asyncTaskQueue = queue, welderDao = fakeWelderDao) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - } yield getRuntime.status shouldBe RuntimeStatus.Error - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, false, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azurePubsubHandler.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete azure vm properly" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val mockWsmDao = mock[WsmDao[IO]] - val (mockWsm, mockControlledResourceApi, _, _) = - AzureTestUtils.setUpMockWsmApiClientProvider() - when { - mockWsmDao.getLandingZoneResources(BillingProfileId(any[String]), any[Authorization])(any[Ask[IO, AppContext]]) - } thenReturn IO.pure(landingZoneResources) - - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmDAO = mockWsmDao, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - controlledResources <- controlledResourceQuery.getAllForRuntime(runtime.id).transaction - } yield { - verify(mockControlledResourceApi, times(1)).deleteAzureStorageContainer(any, any, any) - verify(mockControlledResourceApi, times(1)).deleteAzureDisk(any, any, any) - verify(mockControlledResourceApi, times(1)).deleteAzureVm(any, any, any) - getRuntime.status shouldBe RuntimeStatus.Deleted - controlledResources.length shouldBe 3 - } - - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDisk) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureStorageContainer) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDatabase) - .transaction - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "poll on azure delete vm and disk" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val mockWsmDao = mock[WsmDao[IO]] - when { - mockWsmDao.getLandingZoneResources(BillingProfileId(any[String]), any[Authorization])(any[Ask[IO, AppContext]]) - } thenReturn IO.pure(landingZoneResources) - - val (mockWsm, mockControlledResourceApi, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider() - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmDAO = mockWsmDao, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - controlledResources <- controlledResourceQuery.getAllForRuntime(runtime.id).transaction - } yield { - verify(mockControlledResourceApi, times(1)).getDeleteAzureDiskResult(any[UUID], any[String]) - verify(mockControlledResourceApi, times(1)).getDeleteAzureVmResult(any[UUID], any[String]) - verify(mockControlledResourceApi, times(1)).getDeleteAzureStorageContainerResult(any[UUID], any[String]) - getRuntime.status shouldBe RuntimeStatus.Deleted - controlledResources.length shouldBe 3 - } - - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDisk) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureStorageContainer) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDatabase) - .transaction - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete azure vm but keep the disk if no disk specified" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val mockWsmDao = mock[WsmDao[IO]] - when { - mockWsmDao.getLandingZoneResources(BillingProfileId(any[String]), any[Authorization])(any[Ask[IO, AppContext]]) - } thenReturn IO.pure(landingZoneResources) - val (mockWsm, mockControlledResourceApi, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider() - - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmDAO = mockWsmDao, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - wsmDiskId = WsmControlledResourceId(UUID.randomUUID()) - wsmStorageContainerId = WsmControlledResourceId(UUID.randomUUID()) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - controlledResources <- controlledResourceQuery.getAllForRuntime(runtime.id).transaction - diskStatusOpt <- persistentDiskQuery.getStatus(disk.id).transaction - diskStatus = diskStatusOpt.get - isAttached <- persistentDiskQuery.isDiskAttached(disk.id).transaction - } yield { - verify(mockControlledResourceApi, times(1)).deleteAzureStorageContainer( - any[DeleteControlledAzureResourceRequest], - mockitoEq(workspaceId.value), - mockitoEq(wsmStorageContainerId.value) - ) - verify(mockControlledResourceApi, times(1)).getDeleteAzureStorageContainerResult(mockitoEq(workspaceId.value), - any[String] - ) - verify(mockControlledResourceApi, times(1)).getDeleteAzureVmResult(mockitoEq(workspaceId.value), any[String]) - verify(mockControlledResourceApi, times(0)).deleteAzureDisk(any[DeleteControlledAzureResourceRequest], - any[UUID], - any[UUID] - ) - verify(mockControlledResourceApi, times(1)).deleteAzureVm(any[DeleteControlledAzureResourceRequest], - mockitoEq(workspaceId.value), - mockitoEq(wsmResourceId.value) - ) - getRuntime.status shouldBe RuntimeStatus.Deleted - controlledResources.length shouldBe 2 - val resourceTypes = controlledResources.map(_.resourceType) - resourceTypes.contains(WsmResourceType.AzureDisk) shouldBe true - diskStatus shouldBe DiskStatus.Ready - isAttached shouldBe false - } - - _ <- controlledResourceQuery - .save(runtime.id, wsmDiskId, WsmResourceType.AzureDisk) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, wsmStorageContainerId, WsmResourceType.AzureStorageContainer) - .transaction - msg = DeleteAzureRuntimeMessage(runtime.id, None, workspaceId, Some(wsmResourceId), billingProfileId, None) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - - isAttachedBeforeInterp <- persistentDiskQuery.isDiskAttached(disk.id).transaction - _ = isAttachedBeforeInterp shouldBe true - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle storage container creation error in async task properly" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val exceptionMsg = "storage container failed to create" - val (mockWsm, _, _, _) = - AzureTestUtils.setUpMockWsmApiClientProvider(storageContainerJobStatus = JobReport.StatusEnum.FAILED) - - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - error <- clusterErrorQuery.get(runtime.id).transaction - getDiskOpt <- persistentDiskQuery.getById(disk.id).transaction - getDisk = getDiskOpt.get - } yield { - getRuntime.status shouldBe RuntimeStatus.Error - error.length shouldBe 1 - error.map(_.errorMessage).head should include(exceptionMsg) - getDisk.status shouldBe DiskStatus.Deleted - } - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId2, false, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle vm creation error in async task properly" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val exceptionMsg = "test exception" - val (mockWsm, controlledResourceApi, _, _) = - AzureTestUtils.setUpMockWsmApiClientProvider(JobReport.StatusEnum.FAILED) - - when { - controlledResourceApi.getCreateAzureVmResult(any, any) - } thenThrow { - new ApiException(exceptionMsg) - } - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - error <- clusterErrorQuery.get(runtime.id).transaction - getDiskOpt <- persistentDiskQuery.getById(disk.id).transaction - getDisk = getDiskOpt.get - } yield { - getRuntime.status shouldBe RuntimeStatus.Error - error.length shouldBe 1 - error.map(_.errorMessage).head should include(exceptionMsg) - getDisk.status shouldBe DiskStatus.Deleted - } - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, false, None, "WorkspaceName", billingProfileId) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.createAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to create runtime with persistent disk if no resourceId" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, true, None, "WorkspaceName", billingProfileId) - - err <- azureInterp.createAndPollRuntime(msg).attempt - - } yield err shouldBe Left( - AzureRuntimeCreationError( - runtime.id, - workspaceId, - s"WSMResource record:${wsmResourceId.value} not found for disk id:${disk.id.value}", - true - ) - ) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail to create runtime with persistent disk if WSMresource not found" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val resourceId = WsmControlledResourceId(UUID.randomUUID()) - - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - now <- IO.realTimeInstant - _ <- persistentDiskQuery.updateWSMResourceId(disk.id, resourceId, now).transaction - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - msg = CreateAzureRuntimeMessage(runtime.id, workspaceId, true, None, "WorkspaceName", billingProfileId) - - err <- azureInterp.createAndPollRuntime(msg).attempt - - } yield err shouldBe - Left( - AzureRuntimeCreationError( - runtime.id, - workspaceId, - s"WSMResource record:${resourceId.value} not found for disk id:${disk.id.value}", - true - ) - ) - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle error in delete azure vm async task properly" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(vmJobStatus = JobReport.StatusEnum.FAILED) - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - // Here we manually save a controlled resource with the runtime because we want too ensure it isn't deleted on error - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDatabase) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDisk) - .transaction - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - error <- clusterErrorQuery.get(runtime.id).transaction - getDiskOpt <- persistentDiskQuery.getById(disk.id).transaction - getDisk = getDiskOpt.get - } yield { - getRuntime.status shouldBe RuntimeStatus.Error - error.map(_.errorMessage).head should include("WSM delete VM job failed due") - getDisk.status shouldBe DiskStatus.Error - } - - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail if WSM delete VM job doesn't complete in time" in isolatedDbTest { - val exceptionMsg = "WSM delete VM job was not completed within" - val queue = QueueFactory.asyncTaskQueue() - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(vmJobStatus = JobReport.StatusEnum.RUNNING) - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - // Here we manually save a controlled resource with the runtime because we want too ensure it isn't deleted on error - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDatabase) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDisk) - .transaction - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - error <- clusterErrorQuery.get(runtime.id).transaction - getDiskOpt <- persistentDiskQuery.getById(disk.id).transaction - getDisk = getDiskOpt.get - } yield { - getRuntime.status shouldBe RuntimeStatus.Error - error.map(_.errorMessage).head should include(exceptionMsg) - getDisk.status shouldBe DiskStatus.Error - } - - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "update state correctly if WSM deleteDisk job fails during runtime deletion" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(diskJobStatus = JobReport.StatusEnum.FAILED) - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - // Here we manually save a controlled resource with the runtime because we want too ensure it isn't deleted on error - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDatabase) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDisk) - .transaction - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - runtime = getRuntimeOpt.get - getDiskOpt <- persistentDiskQuery.getById(disk.id).transaction - getDisk = getDiskOpt.get - diskAttached <- persistentDiskQuery.isDiskAttached(disk.id).transaction - } yield { - // VM must be deleted successfully for deleteDisk action to start - // disk can then be deleted from the cloud environment page if desired - runtime.status shouldBe RuntimeStatus.Error - getDisk.status shouldBe DiskStatus.Error - diskAttached shouldBe true - } - - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "update runtime correctly when wsm deleteDisk call errors on runtime deletion" in isolatedDbTest { - val exceptionMsg = "Wsm deleteDisk job failed due to" - val queue = QueueFactory.asyncTaskQueue() - val (mockWsm, _, _, _) = AzureTestUtils.setUpMockWsmApiClientProvider(diskJobStatus = JobReport.StatusEnum.FAILED) - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - // Here we manually save a controlled resource with the runtime because we want too ensure it isn't deleted on error - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDatabase) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDisk) - .transaction - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - error <- clusterErrorQuery.get(runtime.id).transaction - getDiskOpt <- persistentDiskQuery.getById(disk.id).transaction - getDisk = getDiskOpt.get - } yield { - getRuntime.status shouldBe RuntimeStatus.Error - getRuntime.auditInfo.destroyedDate shouldBe None - error.map(_.errorMessage).head should include(exceptionMsg) - getDisk.status shouldBe DiskStatus.Error - } - - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "update runtime correctly when wsm deleteStorageContainer call errors on runtime deletion" in isolatedDbTest { - val exceptionMsg = "WSM storage container delete job failed due to" - val queue = QueueFactory.asyncTaskQueue() - val (mockWsm, _, _, _) = - AzureTestUtils.setUpMockWsmApiClientProvider(storageContainerJobStatus = JobReport.StatusEnum.FAILED) - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - // Here we manually save a controlled resource with the runtime because we want too ensure it isn't deleted on error - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDatabase) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureDisk) - .transaction - _ <- controlledResourceQuery - .save(runtime.id, WsmControlledResourceId(UUID.randomUUID()), WsmResourceType.AzureStorageContainer) - .transaction - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - error <- clusterErrorQuery.get(runtime.id).transaction - getDiskOpt <- persistentDiskQuery.getById(disk.id).transaction - getDisk = getDiskOpt.get - } yield { - getRuntime.status shouldBe RuntimeStatus.Error - getRuntime.auditInfo.destroyedDate shouldBe None - error.map(_.errorMessage).head should include(exceptionMsg) - getDisk.status shouldBe DiskStatus.Error - } - - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "start azure vm" in isolatedDbTest { - // Set up virtual machine mock. - val fakeAzureVmService = AzureTestUtils.setupFakeAzureVmService(vmState = PowerState.DEALLOCATED) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = fakeAzureVmService) - - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - } yield getRuntime.status shouldBe RuntimeStatus.Running - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.startAndMonitorRuntime(runtime, azureCloudContext) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "start azure vm - in starting status" in isolatedDbTest { - // Set up virtual machine mock. - val fakeAzureVmService = AzureTestUtils.setupFakeAzureVmService(vmState = PowerState.STARTING) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = fakeAzureVmService) - - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - } yield getRuntime.status shouldBe RuntimeStatus.Running - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.startAndMonitorRuntime(runtime, azureCloudContext) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "start azure vm - no-op if already running " in isolatedDbTest { - val vmName = "RunningRuntime" - val fakeAzureVmService = AzureTestUtils.setupFakeAzureVmService(vmState = PowerState.RUNNING) - - val spyAzureVmService = spy(fakeAzureVmService) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = fakeAzureVmService) - - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext), - runtimeName = RuntimeName(vmName) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - } yield { - verify(spyAzureVmService, times(0)).startAzureVm(InstanceName(vmName), azureCloudContext) - getRuntime.status shouldBe RuntimeStatus.Running - } - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.startAndMonitorRuntime(runtime, azureCloudContext) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail on startAzureVm - Azure runtime starting error" in isolatedDbTest { - - val failAzureVmService = AzureTestUtils.setupFakeAzureVmService(startVm = false, vmState = PowerState.DEALLOCATED) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = failAzureVmService) - - val res = for { - ctx <- appContext.ask[AppContext] - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - startResult <- azureInterp.startAndMonitorRuntime(runtime, azureCloudContext).attempt - - } yield startResult shouldBe Left( - AzureRuntimeStartingError(runtime.id, s"Starting runtime ${runtime.id} request to Azure failed.", ctx.traceId) - ) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail on startAzureVm - Azure vm in an unstartable status" in isolatedDbTest { - val fakeAzureVmService = AzureTestUtils.setupFakeAzureVmService(vmState = PowerState.UNKNOWN) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = fakeAzureVmService) - - val res = for { - ctx <- appContext.ask[AppContext] - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - startResult <- azureInterp.startAndMonitorRuntime(runtime, azureCloudContext).attempt - - } yield startResult shouldBe Left( - AzureRuntimeStartingError( - runtime.id, - s"Runtime ${runtime.runtimeName.asString} cannot be started in a ${PowerState.UNKNOWN.toString} state, starting runtime request failed", - ctx.traceId - ) - ) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "stop azure vm" in isolatedDbTest { - val vmName = "RunningRuntime" - val vmReturn = mock[VirtualMachine] - when(vmReturn.powerState()) - .thenReturn(PowerState.RUNNING) - .thenReturn(PowerState.DEALLOCATED) - - val passAzureVmService = new FakeAzureVmService { - override def stopAzureVm(name: InstanceName, cloudContext: AzureCloudContext)(implicit - ev: Ask[IO, TraceId] - ): IO[Option[Mono[Void]]] = IO.some(Mono.empty[Void]()) - - override def getAzureVm(name: InstanceName, cloudContext: AzureCloudContext)(implicit - ev: Ask[IO, TraceId] - ): IO[Option[VirtualMachine]] = IO.some(vmReturn) - } - - val spyAzureVmService = spy(passAzureVmService) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = spyAzureVmService) - - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext), - runtimeName = RuntimeName(vmName) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - } yield getRuntime.status shouldBe RuntimeStatus.Stopped - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.stopAndMonitorRuntime(runtime, azureCloudContext) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "stop azure vm - in stopping status" in isolatedDbTest { - val vmName = "StoppingRuntime" - val vmReturn = mock[VirtualMachine] - when(vmReturn.powerState()) - .thenReturn(PowerState.DEALLOCATING) - .thenReturn(PowerState.DEALLOCATED) - - val passAzureVmService = new FakeAzureVmService { - override def stopAzureVm(name: InstanceName, cloudContext: AzureCloudContext)(implicit - ev: Ask[IO, TraceId] - ): IO[Option[Mono[Void]]] = IO.some(Mono.empty[Void]()) - - override def getAzureVm(name: InstanceName, cloudContext: AzureCloudContext)(implicit - ev: Ask[IO, TraceId] - ): IO[Option[VirtualMachine]] = IO.some(vmReturn) - } - - val spyAzureVmService = spy(passAzureVmService) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = spyAzureVmService) - - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext), - runtimeName = RuntimeName(vmName) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - } yield { - verify(spyAzureVmService, times(0)).stopAzureVm(InstanceName(vmName), azureCloudContext) - getRuntime.status shouldBe RuntimeStatus.Stopped - } - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.stopAndMonitorRuntime(runtime, azureCloudContext) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "stop azure vm - no-op if already stopped" in isolatedDbTest { - val vmName = "StoppedRuntime" - - val fakeAzureVmService = AzureTestUtils.setupFakeAzureVmService(vmState = PowerState.STOPPED) - - val spyAzureVmService = spy(fakeAzureVmService) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = spyAzureVmService) - - val res = for { - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext), - runtimeName = RuntimeName(vmName) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - getRuntimeOpt <- clusterQuery.getClusterById(runtime.id).transaction - getRuntime = getRuntimeOpt.get - } yield { - verify(spyAzureVmService, times(0)).stopAzureVm(InstanceName(vmName), azureCloudContext) - getRuntime.status shouldBe RuntimeStatus.Stopped - } - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.stopAndMonitorRuntime(runtime, azureCloudContext) - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail on stopAzureVm - Azure runtime stopping error" in isolatedDbTest { - val failAzureVmService = AzureTestUtils.setupFakeAzureVmService(stopVm = false) - - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = failAzureVmService) - - val res = for { - ctx <- appContext.ask[AppContext] - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - stopResult <- azureInterp.stopAndMonitorRuntime(runtime, azureCloudContext).attempt - - } yield stopResult shouldBe Left( - AzureRuntimeStoppingError(runtime.id, s"Stopping runtime ${runtime.id} request to Azure failed.", ctx.traceId) - ) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "fail on stopAzureVm - Azure runtime not in stoppable status" in isolatedDbTest { - val fakeAzureVmService = AzureTestUtils.setupFakeAzureVmService(vmState = PowerState.UNKNOWN) - val queue = QueueFactory.asyncTaskQueue() - - val azureInterp = - makeAzurePubsubHandler(asyncTaskQueue = queue, azureVmService = fakeAzureVmService) - - val res = for { - ctx <- appContext.ask[AppContext] - disk <- makePersistentDisk().copy(status = DiskStatus.Ready).save() - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(1) - .copy( - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - stopResult <- azureInterp.stopAndMonitorRuntime(runtime, azureCloudContext).attempt - - } yield stopResult shouldBe Left( - AzureRuntimeStoppingError( - runtime.id, - s"Runtime ${runtime.runtimeName.asString} cannot be stopped in a ${PowerState.UNKNOWN.toString} state, stopping runtime request failed", - ctx.traceId - ) - ) - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "delete azure disk properly" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - - val (mockWsm, mockControlledResourceApi, _, _) = - AzureTestUtils.setUpMockWsmApiClientProvider(storageContainerJobStatus = JobReport.StatusEnum.SUCCEEDED) - - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val resourceId = WsmControlledResourceId(UUID.randomUUID()) - - val res = - for { - disk <- makePersistentDisk(wsmResourceId = Some(resourceId)).copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - _ <- controlledResourceQuery - .save(runtime.id, resourceId, WsmResourceType.AzureDisk) - .transaction - - assertions = for { - diskStatusOpt <- persistentDiskQuery.getStatus(disk.id).transaction - diskStatus = diskStatusOpt.get - } yield { - verify(mockControlledResourceApi, times(1)).deleteAzureDisk(any[DeleteControlledAzureResourceRequest], - mockitoEq(workspaceId.value), - mockitoEq(disk.wsmResourceId.get.value) - ) - diskStatus shouldBe DiskStatus.Deleted - } - msg = DeleteDiskV2Message(disk.id, workspaceId, cloudContextAzure, disk.wsmResourceId, None) - - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteDisk(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not send delete to WSM without a disk record" in isolatedDbTest { - val queue = QueueFactory.asyncTaskQueue() - - val (mockWsm, mockControlledResourceApi, _, _) = - AzureTestUtils.setUpMockWsmApiClientProvider(storageContainerJobStatus = JobReport.StatusEnum.FAILED) - - val azureInterp = makeAzurePubsubHandler(asyncTaskQueue = queue, wsmClient = mockWsm) - - val res = - for { - disk <- makePersistentDisk(wsmResourceId = None).copy(status = DiskStatus.Ready).save() - - azureRuntimeConfig = RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A1.toString), - Some(disk.id), - None - ) - runtime = makeCluster(2) - .copy( - status = RuntimeStatus.Running, - cloudContext = CloudContext.Azure(azureCloudContext) - ) - .saveWithRuntimeConfig(azureRuntimeConfig) - - assertions = for { - diskStatusOpt <- persistentDiskQuery.getStatus(disk.id).transaction - diskStatus = diskStatusOpt.get - } yield { - verify(mockControlledResourceApi, times(0)).deleteAzureDisk(any[DeleteControlledAzureResourceRequest], - any[UUID], - any[UUID] - ) - diskStatus shouldBe DiskStatus.Deleted - } - msg = DeleteAzureRuntimeMessage(runtime.id, - Some(disk.id), - workspaceId, - Some(wsmResourceId), - billingProfileId, - None - ) - asyncTaskProcessor = AsyncTaskProcessor(AsyncTaskProcessor.Config(10, 10), queue) - _ <- azureInterp.deleteAndPollRuntime(msg) - - _ <- withInfiniteStream(asyncTaskProcessor.process, assertions) - } yield () - - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - // Needs to be made for each test its used in, otherwise queue will overlap - def makeAzurePubsubHandler(asyncTaskQueue: Queue[IO, Task[IO]] = QueueFactory.asyncTaskQueue(), - relayService: AzureRelayService[IO] = FakeAzureRelayService, - wsmDAO: WsmDao[IO] = new MockWsmDAO, - welderDao: WelderDAO[IO] = new MockWelderDAO(), - azureVmService: AzureVmService[IO] = FakeAzureVmService, - wsmClient: WsmApiClientProvider[IO] = mockWsm, - samDAO: SamDAO[IO] = new MockSamDAO() - ): AzurePubsubHandlerAlgebra[IO] = - new AzurePubsubHandlerInterp[IO]( - ConfigReader.appConfig.azure.pubsubHandler, - new ApplicationConfig("test", - GoogleProject("test"), - Paths.get("x.y"), - WorkbenchEmail("z@x.y"), - new URL("https://leonardo.foo.broadinstitute.org"), - "dev", - 0L - ), - contentSecurityPolicy, - asyncTaskQueue, - wsmDAO, - samDAO, - welderDao, - new MockJupyterDAO(), - relayService, - azureVmService, - refererConfig, - wsmClient - ) - -} From 68a300866b8f91d7e5c2aecf8108326de798d780 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 14:31:43 -0400 Subject: [PATCH 13/43] Remove config and message types for AzurePubSubHandler --- http/src/main/resources/leo.conf | 14 --- http/src/main/resources/reference.conf | 70 ------------ .../workbench/leonardo/LeoPublisher.scala | 6 - .../dsde/workbench/leonardo/dao/WsmDao.scala | 7 +- .../leonardo/http/ConfigReader.scala | 3 +- .../monitor/LeoPubsubMessageSubscriber.scala | 3 - .../leonardo/monitor/MonitorAtBoot.scala | 82 +------------- .../leoPubsubMessageSubscriberModels.scala | 99 ----------------- .../util/AzurePubsubHandlerAlgebra.scala | 105 ------------------ .../workbench/leonardo/CommonTestData.scala | 24 ++-- .../leonardo/http/ConfigReaderSpec.scala | 45 +------- .../leonardo/monitor/LeoPubsubCodecSpec.scala | 21 +--- 12 files changed, 17 insertions(+), 462 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala diff --git a/http/src/main/resources/leo.conf b/http/src/main/resources/leo.conf index fce8712a83..7debf83fbc 100644 --- a/http/src/main/resources/leo.conf +++ b/http/src/main/resources/leo.conf @@ -203,20 +203,6 @@ azure { url = ${?DATA_REPO_URL} } - pubsub-handler { - runtime-defaults { - acr-credential { - username = ${?AZURE_PUBSUB_ACR_USER} - password = ${?AZURE_PUBSUB_ACR_PASSWORD} - } - - vm-credential { - username = ${?AZURE_VM_USER} - password = ${?AZURE_VM_PASSWORD} - } - } - } - app-registration { client-id = ${?LEO_MANAGED_APP_CLIENT_ID} client-secret = ${?LEO_MANAGED_APP_CLIENT_SECRET} diff --git a/http/src/main/resources/reference.conf b/http/src/main/resources/reference.conf index f2ffcb8233..9df4207f0f 100644 --- a/http/src/main/resources/reference.conf +++ b/http/src/main/resources/reference.conf @@ -197,76 +197,6 @@ azure { url = "https://jade.datarepo-dev.broadinstitute.org" } - pubsub-handler { - sam-url = ${sam.server} - wsm-url = ${azure.wsm.uri} - welder-acr-uri = "terradevacrpublic.azurecr.io/welder-server" - welder-image-hash = ${image.welderHash} - - // These timeouts are set to take longer than the WSM job timeout with some buffer: https://github.com/DataBiosphere/terra-workspace-manager/blob/main/service/src/main/resources/application.yml#L77 - create-vm-poll-config { - initial-delay = 2 minutes - max-attempts = 240 # 10 seconds * 240 is 40 min, we wait for wsm to error if at all possible (which is at 30min) - interval = 10 seconds - } - delete-disk-poll-config { - initial-delay = 1 minute - max-attempts = 240 # 1 + 240*10 sec = 40 min - interval = 10 seconds - } - delete-vm-poll-config { - initial-delay = 30 seconds - max-attempts = 240 # 10 seconds * 240 is 40 min - interval = 10 seconds - } - start-stop-vm-poll-config { - initial-delay = 10 seconds - max-attempts = 30 # (10 seconds * 30 + 5) = 5:10 - interval = 10 seconds - } - create-disk-poll-config { - initial-delay = 5 seconds - max-attempts = 20 # 5 seconds * 10 + 5 is 55 sec - interval = 5 seconds - } - delete-storage-container-poll-config { - initial-delay = 5 seconds - max-attempts = 20 # 5 seconds * 10 + 5 is 55 sec - interval = 5 seconds - } - - - runtime-defaults { - ip-name-prefix = "ip" - ip-controlled-resource-desc = "Azure Ip" - network-controlled-resource-desc = "Azure Network" - network-name-prefix = "network" - subnet-name-prefix = "subnet" - address-space-cidr = "192.168.0.0/16" - subnet-address-cidr = "192.168.0.0/24" - disk-controlled-resource-desc = "Azure Disk" - vm-controlled-resource-desc = "Azure Vm" - image { - publisher = "microsoft-dsvm" - offer = "ubuntu-2004" - sku = "2004-gen2" - version = "23.04.24" - } - custom-script-extension { - name = "vm-custom-script-extension", - publisher = "Microsoft.Azure.Extensions", - type = "CustomScript", - version = "2.1", - minor-version-auto-upgrade = true, - file-uris = ["https://raw.githubusercontent.com/DataBiosphere/leonardo/4ae6ec54e73d3fb20e8c3a142488bd09db814160/http/src/main/resources/init-resources/azure_vm_init_script.sh"] - } - # [IA-4997] to support CHIPS by setting partitioned cookies - # listener-image = "terradevacrpublic.azurecr.io/terra-azure-relay-listeners:474f157" - listener-image = "terradevacrpublic.azurecr.io/terra-azure-relay-listeners:76d982c" - } - } - - # We need the leo azure entity for this app-registration { client-id = "" diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala index ed85bdc977..2264c9be73 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/LeoPublisher.scala @@ -76,16 +76,10 @@ final class LeoPublisher[F[_]]( _ <- msg match { case m: LeoPubsubMessage.CreateRuntimeMessage => clusterQuery.updateClusterStatus(m.runtimeId, RuntimeStatus.Creating, now).transaction - case m: LeoPubsubMessage.CreateAzureRuntimeMessage => - clusterQuery.updateClusterStatus(m.runtimeId, RuntimeStatus.Creating, now).transaction - case m: LeoPubsubMessage.DeleteAzureRuntimeMessage => - clusterQuery.updateClusterStatus(m.runtimeId, RuntimeStatus.Deleting, now).transaction case m: LeoPubsubMessage.CreateDiskMessage => persistentDiskQuery.updateStatus(m.diskId, DiskStatus.Creating, now).transaction case m: LeoPubsubMessage.DeleteDiskMessage => persistentDiskQuery.updateStatus(m.diskId, DiskStatus.Deleting, now).transaction - case m: LeoPubsubMessage.DeleteDiskV2Message => - persistentDiskQuery.updateStatus(m.diskId, DiskStatus.Deleting, now).transaction case m: LeoPubsubMessage.StopRuntimeMessage => clusterQuery.updateClusterStatus(m.runtimeId, RuntimeStatus.Stopping, now).transaction case m: LeoPubsubMessage.StartRuntimeMessage => diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala index 0ae7accf16..c8897d1a5c 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala @@ -11,7 +11,6 @@ import org.broadinstitute.dsde.workbench.leonardo.JsonCodec.{ wsmControlledResourceIdDecoder } import org.broadinstitute.dsde.workbench.leonardo.dao.LandingZoneResourcePurpose.LandingZoneResourcePurpose -import org.broadinstitute.dsde.workbench.leonardo.util.PollDiskParams import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} import org.http4s.headers.Authorization @@ -80,6 +79,12 @@ final case class WsmResource(metadata: WsmResourceMetadata, resourceAttributes: final case class GetWsmResourceResponse(resources: List[WsmResource]) // Azure Disk models +final case class PollDiskParams(workspaceId: WorkspaceId, + jobId: WsmJobId, + diskId: DiskId, + runtime: Runtime, + wsmResourceId: WsmControlledResourceId + ) final case class CreateDiskForRuntimeResult(resourceId: WsmControlledResourceId, pollParams: Option[PollDiskParams]) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala index c3efbf6071..84f96cae56 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala @@ -7,7 +7,7 @@ import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.Serv import org.broadinstitute.dsde.workbench.leonardo.ConfigImplicits._ import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetricsMonitorConfig -import org.broadinstitute.dsde.workbench.leonardo.util.{AzurePubsubHandlerConfig, TerraAppSetupChartConfig} +import org.broadinstitute.dsde.workbench.leonardo.util.TerraAppSetupChartConfig import org.broadinstitute.dsp.{ChartName, ChartVersion} import org.http4s.Uri import pureconfig.ConfigSource @@ -19,7 +19,6 @@ object ConfigReader { .loadOrThrow[AppConfig] } final case class AzureConfig( - pubsubHandler: AzurePubsubHandlerConfig, wsm: HttpWsmDaoConfig, bpm: BpmConfig, appRegistration: AzureAppRegistrationConfig, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala index 124717d52e..fcb50c6a7f 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala @@ -108,9 +108,6 @@ class LeoPubsubMessageSubscriber[F[_]]( handleStartAppMessage(msg) case msg: UpdateAppMessage => handleUpdateAppMessage(msg) - case _: CreateAzureRuntimeMessage => ??? - case _: DeleteAzureRuntimeMessage => ??? - case _: DeleteDiskV2Message => ??? } } yield resp diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala index 2686f2c3d7..6ea5d7028c 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala @@ -1,7 +1,6 @@ package org.broadinstitute.dsde.workbench.leonardo package monitor -import akka.http.scaladsl.model.StatusCodes import cats.effect.Async import cats.effect.std.Queue import cats.mtl.Ask @@ -17,17 +16,8 @@ import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics import org.typelevel.log4cats.Logger -import java.util.UUID import scala.concurrent.ExecutionContext -// TODO delete with Azure code -final case class WorkspaceNotFoundException(workspaceId: WorkspaceId, traceId: TraceId) - extends LeoException( - s"WorkspaceId not found in workspace manager for workspace ${workspaceId}", - StatusCodes.NotFound, - traceId = Some(traceId) - ) - class MonitorAtBoot[F[_]](publisherQueue: Queue[F, LeoPubsubMessage], computeService: Option[GoogleComputeService[F]], samDAO: SamDAO[F], @@ -88,12 +78,7 @@ class MonitorAtBoot[F[_]](publisherQueue: Queue[F, LeoPubsubMessage], ): F[Unit] = { val res = for { traceId <- ev.ask[TraceId] - msg <- runtimeToMonitor.cloudContext match { - case CloudContext.Gcp(_) => - runtimeStatusToMessageGCP(runtimeToMonitor, traceId, checkToolsInterruptAfter) - case CloudContext.Azure(_) => - runtimeStatusToMessageAzure(runtimeToMonitor, traceId) - } + msg <- runtimeStatusToMessageGCP(runtimeToMonitor, traceId, checkToolsInterruptAfter) _ <- publisherQueue.offer(msg) } yield () res.handleErrorWith(e => logger.error(e)(s"MonitorAtBoot: Error monitoring runtime ${runtimeToMonitor.id}")) @@ -323,71 +308,6 @@ class MonitorAtBoot[F[_]](publisherQueue: Queue[F, LeoPubsubMessage], case x => F.raiseError(MonitorAtBootException(s"Unexpected status for runtime ${runtime.id}: ${x}", traceId)) } - private def runtimeStatusToMessageAzure(runtime: RuntimeToMonitor, traceId: TraceId): F[LeoPubsubMessage] = - runtime.status match { - case RuntimeStatus.Stopping => - F.pure( - LeoPubsubMessage.StopRuntimeMessage( - runtimeId = runtime.id, - traceId = Some(traceId) - ) - ) - case RuntimeStatus.Deleting => - for { - now <- F.realTimeInstant - implicit0(appContext: Ask[F, AppContext]) <- F.pure(Ask.const(AppContext(traceId, now))) - wid <- F.fromOption(runtime.workspaceId, - MonitorAtBootException(s"no workspaceId found for ${runtime.id.toString}", traceId) - ) - controlledResourceOpt = WsmControlledResourceId(UUID.fromString(runtime.internalId)) - leoAuth <- samDAO.getLeoAuthToken - workspaceDescOpt <- wsmClientProvider.getWorkspace( - leoAuth.credentials.renderString, - wid - ) - workspaceDesc <- F.fromOption(workspaceDescOpt, WorkspaceNotFoundException(wid, traceId)) - } yield LeoPubsubMessage.DeleteAzureRuntimeMessage( - runtimeId = runtime.id, - None, - workspaceId = wid, - wsmResourceId = Some(controlledResourceOpt), - BillingProfileId(workspaceDesc.spendProfile), - traceId = Some(traceId) - ) - case RuntimeStatus.Starting => - for { - now <- F.realTimeInstant - implicit0(appContext: Ask[F, AppContext]) <- F.pure(Ask.const(AppContext(traceId, now))) - } yield LeoPubsubMessage.StartRuntimeMessage( - runtimeId = runtime.id, - traceId = Some(traceId) - ) - case RuntimeStatus.Creating => - for { - now <- F.realTimeInstant - implicit0(appContext: Ask[F, AppContext]) <- F.pure(Ask.const(AppContext(traceId, now))) - wid <- F.fromOption(runtime.workspaceId, - MonitorAtBootException(s"no workspaceId found for ${runtime.id.toString}", traceId) - ) - leoAuth <- samDAO.getLeoAuthToken - token = leoAuth.credentials.toString().split(" ")(1) - workspaceDescOpt <- wsmClientProvider.getWorkspace( - token, - wid - ) - - workspaceDesc <- F.fromOption(workspaceDescOpt, WorkspaceNotFoundException(wid, traceId)) - } yield LeoPubsubMessage.CreateAzureRuntimeMessage( - runtime.id, - wid, - false, - Some(traceId), - workspaceDesc.displayName, - BillingProfileId(workspaceDesc.spendProfile) - ) - case x => F.raiseError(MonitorAtBootException(s"Unexpected status for runtime ${runtime.id}: ${x}", traceId)) - } - private def getAuthToken(creator: WorkbenchEmail)(implicit ev: Ask[F, TraceId] ): F[String] = diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala index 39da2536b5..79d41ecfea 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala @@ -160,16 +160,6 @@ object LeoPubsubMessageType extends Enum[LeoPubsubMessageType] { final case object UpdateApp extends LeoPubsubMessageType { val asString = "updateApp" } - final case object CreateAzureRuntime extends LeoPubsubMessageType { - val asString = "createAzureRuntime" - } - final case object DeleteAzureRuntime extends LeoPubsubMessageType { - val asString = "deleteAzureRuntime" - } - - final case object DeleteDiskV2 extends LeoPubsubMessageType { - val asString = "deleteDiskV2" - } } sealed trait LeoPubsubMessage { @@ -338,37 +328,6 @@ object LeoPubsubMessage { ) extends LeoPubsubMessage { val messageType: LeoPubsubMessageType = LeoPubsubMessageType.UpdateApp } - - final case class CreateAzureRuntimeMessage( - runtimeId: Long, - workspaceId: WorkspaceId, - useExistingDisk: Boolean, // if using existing disk, will attach pd to new runtime - traceId: Option[TraceId], - workspaceName: String, - billingProfileId: BillingProfileId - ) extends LeoPubsubMessage { - val messageType: LeoPubsubMessageType = LeoPubsubMessageType.CreateAzureRuntime - } - - final case class DeleteAzureRuntimeMessage(runtimeId: Long, - diskIdToDelete: Option[DiskId], - workspaceId: WorkspaceId, - wsmResourceId: Option[WsmControlledResourceId], - billingProfileId: BillingProfileId, - traceId: Option[TraceId] - ) extends LeoPubsubMessage { - val messageType: LeoPubsubMessageType = LeoPubsubMessageType.DeleteAzureRuntime - } - - final case class DeleteDiskV2Message(diskId: DiskId, - workspaceId: WorkspaceId, - cloudContext: CloudContext, - wsmResourceId: Option[WsmControlledResourceId], - traceId: Option[TraceId] - ) extends LeoPubsubMessage { - val messageType: LeoPubsubMessageType = LeoPubsubMessageType.DeleteDiskV2 - } - } sealed trait ClusterNodepoolActionType extends Product with Serializable { @@ -543,23 +502,6 @@ object LeoPubsubCodec { UpdateAppMessage.apply ) - implicit val createAzureRuntimeMessageDecoder: Decoder[CreateAzureRuntimeMessage] = - Decoder.forProduct6( - "runtimeId", - "workspaceId", - "useExistingDisk", - "traceId", - "workspaceName", - "billingProfileId" - )( - CreateAzureRuntimeMessage.apply - ) - - implicit val deleteAzureRuntimeDecoder: Decoder[DeleteAzureRuntimeMessage] = - Decoder.forProduct6("runtimeId", "diskId", "workspaceId", "wsmResourceId", "billingProfileId", "traceId")( - DeleteAzureRuntimeMessage.apply - ) - implicit val leoPubsubMessageTypeDecoder: Decoder[LeoPubsubMessageType] = Decoder.decodeString.emap { x => Either.catchNonFatal(LeoPubsubMessageType.withName(x)).leftMap(_.getMessage) } @@ -567,11 +509,6 @@ object LeoPubsubCodec { implicit val storageContainerResponseDecoder: Decoder[StorageContainerResponse] = Decoder.forProduct2("name", "resourceId")(StorageContainerResponse.apply) - implicit val deleteDiskV2Decoder: Decoder[DeleteDiskV2Message] = - Decoder.forProduct5("diskId", "workspaceId", "cloudContext", "wsmResourceId", "traceId")( - DeleteDiskV2Message.apply - ) - implicit val leoPubsubMessageDecoder: Decoder[LeoPubsubMessage] = Decoder.instance { message => for { messageType <- message.downField("messageType").as[LeoPubsubMessageType] @@ -589,9 +526,6 @@ object LeoPubsubCodec { case LeoPubsubMessageType.StopApp => message.as[StopAppMessage] case LeoPubsubMessageType.StartApp => message.as[StartAppMessage] case LeoPubsubMessageType.UpdateApp => message.as[UpdateAppMessage] - case LeoPubsubMessageType.CreateAzureRuntime => message.as[CreateAzureRuntimeMessage] - case LeoPubsubMessageType.DeleteAzureRuntime => message.as[DeleteAzureRuntimeMessage] - case LeoPubsubMessageType.DeleteDiskV2 => message.as[DeleteDiskV2Message] } } yield value @@ -922,36 +856,6 @@ object LeoPubsubCodec { "traceId" )(x => (x.messageType, x.jobId, x.appId, x.appName, x.cloudContext, x.workspaceId, x.googleProject, x.traceId)) - implicit val createAzureRuntimeMessageEncoder: Encoder[CreateAzureRuntimeMessage] = - Encoder.forProduct7( - "messageType", - "runtimeId", - "workspaceId", - "useExistingDisk", - "traceId", - "workspaceName", - "billingProfileId" - )(x => - (x.messageType, x.runtimeId, x.workspaceId, x.useExistingDisk, x.traceId, x.workspaceName, x.billingProfileId) - ) - - implicit val deleteAzureMessageEncoder: Encoder[DeleteAzureRuntimeMessage] = - Encoder.forProduct7("messageType", - "runtimeId", - "diskId", - "workspaceId", - "wsmResourceId", - "billingProfileId", - "traceId" - )(x => - (x.messageType, x.runtimeId, x.diskIdToDelete, x.workspaceId, x.wsmResourceId, x.billingProfileId, x.traceId) - ) - - implicit val deleteDiskV2MessageEncoder: Encoder[DeleteDiskV2Message] = - Encoder.forProduct6("messageType", "diskId", "workspaceId", "cloudContext", "wsmResourceId", "traceId")(x => - (x.messageType, x.diskId, x.workspaceId, x.cloudContext, x.wsmResourceId, x.traceId) - ) - implicit val leoPubsubMessageEncoder: Encoder[LeoPubsubMessage] = Encoder.instance { case m: CreateDiskMessage => m.asJson case m: UpdateDiskMessage => m.asJson @@ -966,9 +870,6 @@ object LeoPubsubCodec { case m: StopAppMessage => m.asJson case m: StartAppMessage => m.asJson case m: UpdateAppMessage => m.asJson - case m: CreateAzureRuntimeMessage => m.asJson - case m: DeleteAzureRuntimeMessage => m.asJson - case m: DeleteDiskV2Message => m.asJson } } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala deleted file mode 100644 index 31b84c0b60..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzurePubsubHandlerAlgebra.scala +++ /dev/null @@ -1,105 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package util - -import org.broadinstitute.dsde.workbench.azure.ContainerName -import org.broadinstitute.dsde.workbench.leonardo.WsmControlledResourceId -import org.broadinstitute.dsde.workbench.leonardo.config.PersistentDiskConfig -import org.broadinstitute.dsde.workbench.leonardo.dao.{CreateDiskForRuntimeResult, StorageContainerResponse} -import org.broadinstitute.dsde.workbench.leonardo.monitor.PollMonitorConfig -import org.http4s.Uri - - -final case class CreateAzureDiskParams(workspaceId: WorkspaceId, - runtime: Runtime, - useExistingDisk: Boolean, - runtimeConfig: RuntimeConfig.AzureConfig -) - -/** - * This case class represents the necessary information to poll all objects associated with the runtime, - * namely disk, storage container and vm - */ -final case class PollRuntimeParams(workspaceId: WorkspaceId, - runtime: Runtime, - useExistingDisk: Boolean, - createDiskResult: CreateDiskForRuntimeResult, - landingZoneResources: LandingZoneResources, - runtimeConfig: RuntimeConfig.AzureConfig, - vmImage: AzureImage, - workspaceStorageContainer: StorageContainerResponse, - workspaceName: String, - storageAccountUrlDomain: String, - cloudContext: CloudContext.Azure, - userAssignedIdentities: List[String] -) - -final case class PollDiskParams(workspaceId: WorkspaceId, - jobId: WsmJobId, - diskId: DiskId, - runtime: Runtime, - wsmResourceId: WsmControlledResourceId -) - -final case class PollDeleteDiskParams(workspaceId: WorkspaceId, - jobId: WsmJobId, - diskId: Option[DiskId], - runtime: Runtime, - wsmResourceId: WsmControlledResourceId -) - -final case class PollVmParams(workspaceId: WorkspaceId, jobId: WsmJobId, runtime: Runtime, diskId: Option[DiskId]) - -final case class PollStorageContainerParams(workspaceId: WorkspaceId, - jobId: WsmJobId, - runtime: Runtime, - diskId: Option[DiskId] -) - -final case class CreateStorageContainerResourcesResult(containerName: ContainerName, - resourceId: WsmControlledResourceId -) - -final case class CustomScriptExtensionConfig(name: String, - publisher: String, - `type`: String, - version: String, - minorVersionAutoUpgrade: Boolean, - fileUris: List[String] - ) - -final case class AzureServiceConfig(diskConfig: PersistentDiskConfig, - image: AzureImage, - listenerImage: String, - welderImage: String - ) -final case class VMCredential(username: String, password: String) - -final case class AzureRuntimeDefaults(ipControlledResourceDesc: String, - ipNamePrefix: String, - networkControlledResourceDesc: String, - networkNamePrefix: String, - subnetNamePrefix: String, - addressSpaceCidr: CidrIP, - subnetAddressCidr: CidrIP, - diskControlledResourceDesc: String, - vmControlledResourceDesc: String, - image: AzureImage, - customScriptExtension: CustomScriptExtensionConfig, - listenerImage: String, - vmCredential: VMCredential - ) - -final case class AzurePubsubHandlerConfig(samUrl: Uri, - wsmUrl: Uri, - welderAcrUri: String, - welderImageHash: String, - createVmPollConfig: PollMonitorConfig, - deleteVmPollConfig: PollMonitorConfig, - startStopVmPollConfig: PollMonitorConfig, - deleteDiskPollConfig: PollMonitorConfig, - runtimeDefaults: AzureRuntimeDefaults, - createDiskPollConfig: PollMonitorConfig, - deleteStorageContainerPollConfig: PollMonitorConfig -) { - def welderImage: String = s"$welderAcrUri:$welderImageHash" -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index 9650bd4800..60e518d753 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -1,17 +1,18 @@ package org.broadinstitute.dsde.workbench.leonardo -import akka.http.scaladsl.model.{StatusCode, StatusCodes} import akka.http.scaladsl.model.headers.{HttpCookiePair, OAuth2BearerToken} +import akka.http.scaladsl.model.{StatusCode, StatusCodes} import bio.terra.workspace.client.ApiException import bio.terra.workspace.model.{AzureContext, GcpContext, WorkspaceDescription} -import cats.effect.IO -import cats.effect.Ref +import cats.effect.{IO, Ref} import cats.mtl.Ask +import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes import com.google.auth.oauth2.{AccessToken, GoogleCredentials} import com.google.cloud.compute.v1.Instance.Status import com.google.cloud.compute.v1._ import com.typesafe.config.ConfigFactory import net.ceedubs.ficus.Ficus._ +import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.mock.BaseFakeGoogleStorage import org.broadinstitute.dsde.workbench.google2.{DataprocRole, DiskName, MachineTypeName, NetworkName, OperationName, RegionName, SubnetworkName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo @@ -22,20 +23,16 @@ import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.dao.{AccessScope, CloningInstructions, ControlledResourceDescription, ControlledResourceIamRole, ControlledResourceName, InternalDaoControlledResourceCommonFields, ManagedBy, MockSamDAO, PrivateResourceUser} import org.broadinstitute.dsde.workbench.leonardo.db.ClusterRecord -import org.broadinstitute.dsde.workbench.leonardo.http.{ConfigReader, CreateRuntimeRequest, RuntimeConfigRequest, userScriptStartupOutputUriMetadataKey} +import org.broadinstitute.dsde.workbench.leonardo.http.{CreateRuntimeRequest, RuntimeConfigRequest, userScriptStartupOutputUriMetadataKey} import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google._ +import org.broadinstitute.dsde.workbench.oauth2.mock.FakeOpenIDConnectConfiguration +import org.broadinstitute.dsde.workbench.util2.InstanceName import java.nio.file.Paths import java.time.Instant import java.time.temporal.ChronoUnit import java.util.{Date, UUID} -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes -import org.broadinstitute.dsde.workbench.azure.{ApplicationInsightsName, AzureCloudContext, BatchAccountName, ManagedResourceGroupName, RelayNamespace, SubscriptionId, TenantId} -import org.broadinstitute.dsde.workbench.leonardo.util.AzureServiceConfig -import org.broadinstitute.dsde.workbench.oauth2.mock.FakeOpenIDConnectConfiguration -import org.broadinstitute.dsde.workbench.util2.InstanceName - import scala.concurrent.duration._ object CommonTestData { @@ -131,13 +128,6 @@ object CommonTestData { val refererConfig = Config.refererConfig val leoKubernetesConfig = Config.leoKubernetesConfig val openIdConnectionConfiguration = FakeOpenIDConnectConfiguration - val azureServiceConfig = AzureServiceConfig( - // For now azure disks share same defaults as normal disks - ConfigReader.appConfig.persistentDisk, - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.image, - ConfigReader.appConfig.azure.pubsubHandler.runtimeDefaults.listenerImage, - ConfigReader.appConfig.azure.pubsubHandler.welderImageHash - ) val singleNodeDefaultMachineConfig = dataprocConfig.runtimeConfigDefaults val singleNodeDefaultMachineConfigRequest = RuntimeConfigRequest.DataprocConfig( Some(singleNodeDefaultMachineConfig.numberOfWorkers), diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala index decd78df31..df21d4d276 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala @@ -5,7 +5,7 @@ import com.azure.core.management.AzureEnvironment import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.ZoneName import org.broadinstitute.dsde.workbench.leonardo.config._ -import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoMetricsMonitorConfig, PollMonitorConfig} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetricsMonitorConfig import org.broadinstitute.dsde.workbench.leonardo.util._ import org.broadinstitute.dsp._ import org.http4s.Uri @@ -28,49 +28,6 @@ class ConfigReaderSpec extends AnyFlatSpec with Matchers { Vector("bogus") ), AzureConfig( - AzurePubsubHandlerConfig( - Uri.unsafeFromString("https://sam.test.org:443"), - Uri.unsafeFromString("https://localhost:8000"), - "terradevacrpublic.azurecr.io/welder-server", - "0c1d0eb", - PollMonitorConfig(1 seconds, 10, 1 seconds), - PollMonitorConfig(1 seconds, 20, 1 seconds), - PollMonitorConfig(1 seconds, 10, 1 seconds), - PollMonitorConfig(1 seconds, 10, 1 seconds), - AzureRuntimeDefaults( - "Azure Ip", - "ip", - "Azure Network", - "network", - "subnet", - CidrIP("192.168.0.0/16"), - CidrIP("192.168.0.0/24"), - "Azure Disk", - "Azure Vm", - AzureImage( - "microsoft-dsvm", - "ubuntu-2004", - "2004-gen2", - "23.04.24" - ), - CustomScriptExtensionConfig( - "vm-custom-script-extension", - "Microsoft.Azure.Extensions", - "CustomScript", - "2.1", - true, - List( - "https://raw.githubusercontent.com/DataBiosphere/leonardo/4ae6ec54e73d3fb20e8c3a142488bd09db814160/http/src/main/resources/init-resources/azure_vm_init_script.sh" - ) - ), - // [IA-4997] to support CHIPS by setting partitioned cookies - // "terradevacrpublic.azurecr.io/terra-azure-relay-listeners:474f157", - "terradevacrpublic.azurecr.io/terra-azure-relay-listeners:76d982c", - VMCredential(username = "username", password = "password") - ), - PollMonitorConfig(1 seconds, 10, 1 seconds), - PollMonitorConfig(1 seconds, 10, 1 seconds) - ), HttpWsmDaoConfig(Uri.unsafeFromString("https://localhost:8000")), BpmConfig(Uri.unsafeFromString("https://localhost:8000")), AzureAppRegistrationConfig(ClientId(""), ClientSecret(""), ManagedAppTenantId("")), diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala index 410f6e13c5..accd973c88 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala @@ -10,11 +10,7 @@ import org.broadinstitute.dsde.workbench.google2.{DiskName, MachineTypeName, Net import org.broadinstitute.dsde.workbench.leonardo.AppType.Galaxy import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubCodec._ -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAppMessage, - CreateAzureRuntimeMessage, - CreateRuntimeMessage -} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, CreateRuntimeMessage} import org.broadinstitute.dsde.workbench.model.google.{GcsBucketName, GoogleProject} import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} import org.scalatest.flatspec.AnyFlatSpec @@ -126,21 +122,6 @@ class LeoPubsubCodecSpec extends AnyFlatSpec with Matchers { res shouldBe Right(originalMessage) } - it should "encode/decode CreateAzureRuntimeMessage properly" in { - val originalMessage = - CreateAzureRuntimeMessage(1, - WorkspaceId(UUID.randomUUID()), - false, - None, - "WorkspaceName", - BillingProfileId("spend-profile") - ) - - val res = decode[CreateAzureRuntimeMessage](originalMessage.asJson.printWith(Printer.noSpaces)) - - res shouldBe Right(originalMessage) - } - val landingZoneResources = LandingZoneResources( UUID.randomUUID(), AKSCluster("cluster-name", Map.empty[String, Boolean]), From 75c7b9b5f29bcf7e71990fdb17da4fcb22c00957 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 14:43:30 -0400 Subject: [PATCH 14/43] Remove WsmDao (legacy WSM interface) --- .../workbench/leonardo/dao/HttpWsmDao.scala | 323 ------------------ .../dsde/workbench/leonardo/dao/WsmDao.scala | 18 +- .../http/BaselineDependenciesBuilder.scala | 4 - .../leoPubsubMessageSubscriberModels.scala | 4 - .../leonardo/dao/HttpWsmDaoSpec.scala | 170 --------- .../workbench/leonardo/dao/MockWsmDAO.scala | 23 -- .../http/AzureDependenciesBuilderSpec.scala | 3 +- .../http/GcpDependenciesBuilderSpec.scala | 1 - .../leonardo/http/api/TestLeoRoutes.scala | 1 - .../http/service/AppServiceInterpSpec.scala | 14 +- 10 files changed, 7 insertions(+), 554 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDao.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDaoSpec.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmDAO.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDao.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDao.scala deleted file mode 100644 index bc1e1f55fb..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDao.scala +++ /dev/null @@ -1,323 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package dao - -import cats.data.OptionT -import cats.effect.Async -import cats.implicits._ -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.azure.{ApplicationInsightsName, BatchAccountName, RelayNamespace} -import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} -import org.broadinstitute.dsde.workbench.leonardo.config.HttpWsmDaoConfig -import org.broadinstitute.dsde.workbench.leonardo.dao.LandingZoneResourcePurpose.{ - AKS_NODE_POOL_SUBNET, - LandingZoneResourcePurpose, - SHARED_RESOURCE, - WORKSPACE_BATCH_SUBNET -} -import org.broadinstitute.dsde.workbench.leonardo.dao.WsmDecoders._ -import org.broadinstitute.dsde.workbench.leonardo.db.WsmResourceType -import org.broadinstitute.dsde.workbench.leonardo.util.AppCreationException -import org.broadinstitute.dsde.workbench.model.TraceId -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s._ -import org.http4s.circe.CirceEntityDecoder._ -import org.http4s.client.Client -import org.http4s.client.dsl.Http4sClientDsl -import org.http4s.headers.{`Content-Type`, Authorization} -import org.typelevel.ci.CIString -import org.typelevel.log4cats.StructuredLogger - -import java.util.UUID - -/** - * This is the legacy WsmDAO. It remains because there is some specific logic around models retrieved from WSM - * It SHOULD NOT be added to. Favor usage of WsmClientProvider, the auto-generated client. - */ -class HttpWsmDao[F[_]](httpClient: Client[F], config: HttpWsmDaoConfig)(implicit - logger: StructuredLogger[F], - F: Async[F], - metrics: OpenTelemetryMetrics[F] -) extends WsmDao[F] - with Http4sClientDsl[F] { - - val defaultMediaType = `Content-Type`(MediaType.application.json) - - // This remains in the legacy dao because of the custom logic around landing zones - // We should migrate to the generated client when possible - override def getLandingZoneResources(billingProfileId: BillingProfileId, userToken: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[LandingZoneResources] = - for { - // Step 1: call LZ for LZ id - landingZoneOpt <- getLandingZone(billingProfileId, userToken) - landingZone <- F.fromOption( - landingZoneOpt, - AppCreationException(s"Landing zone not found for billing profile ${billingProfileId}") - ) - landingZoneId = landingZone.landingZoneId - - // Step 2: call LZ for LZ resources - lzResourcesByPurpose <- listLandingZoneResourcesByType(landingZoneId, userToken) - region <- lzResourcesByPurpose - .flatMap(_.deployedResources) - .headOption match { // All LZ resources live in a same region. Hence we can grab any resource and find out the region - case Some(lzResource) => - F.pure( - com.azure.core.management.Region - .fromName(lzResource.region) - ) - case None => - F.raiseError(new Exception(s"This should never happen. No resource found for LZ(${landingZoneId})")) - } - groupedLzResources = lzResourcesByPurpose.foldMap(a => - a.deployedResources.groupBy(b => (a.purpose, b.resourceType.toLowerCase)) - ) - aksResource <- getLandingZoneResource(groupedLzResources, - "Microsoft.ContainerService/managedClusters", - SHARED_RESOURCE - ) - aksCluster <- getLandingZoneResourceName(aksResource, useParent = false).map { aksName => - val tagValue = getLandingZoneResourceTagValue(aksResource, "aks-cost-vpa-enabled") - val tags: Map[String, Boolean] = - Map("aks-cost-vpa-enabled" -> java.lang.Boolean.parseBoolean(tagValue.getOrElse("false"))) - AKSCluster(aksName, tags) - } - _ <- logger.info(s"Retrieved Landing Zone AKS cluster: ${aksCluster}") - batchAccountName <- getLandingZoneResourceName(groupedLzResources, - "Microsoft.Batch/batchAccounts", - SHARED_RESOURCE, - false - ) - relayNamespace <- getLandingZoneResourceName(groupedLzResources, - "Microsoft.Relay/namespaces", - SHARED_RESOURCE, - false - ) - storageAccountName <- getLandingZoneResourceName(groupedLzResources, - "Microsoft.Storage/storageAccounts", - SHARED_RESOURCE, - false - ) - applicationInsightsName <- getLandingZoneResourceName(groupedLzResources, - "Microsoft.Insights/components", - SHARED_RESOURCE, - false - ) - vnetName <- getLandingZoneResourceName(groupedLzResources, "DeployedSubnet", AKS_NODE_POOL_SUBNET, true) - batchNodesSubnetName <- getLandingZoneResourceId(groupedLzResources, "DeployedSubnet", WORKSPACE_BATCH_SUBNET) - aksSubnetName <- getLandingZoneResourceName(groupedLzResources, "DeployedSubnet", AKS_NODE_POOL_SUBNET, false) - postgresResource <- getLandingZoneResource(groupedLzResources, - "Microsoft.DBforPostgreSQL/flexibleServers", - SHARED_RESOURCE - ).attempt // use attempt here because older Landing Zones do not have a Postgres server - postgresServer <- postgresResource.toOption.traverse { resource => - getLandingZoneResourceName(resource, useParent = false).map { pgName => - val tagValue = getLandingZoneResourceTagValue(resource, "pgbouncer-enabled") - val pgBouncerEnabled: Boolean = java.lang.Boolean.parseBoolean(tagValue.getOrElse("false")) - logger.info( - s"Landing Zone Postgres server has 'pgbouncer-enabled' tag $tagValue; setting pgBouncerEnabled to $pgBouncerEnabled." - ) - PostgresServer(pgName, pgBouncerEnabled) - } - } - } yield LandingZoneResources( - landingZoneId, - aksCluster, - BatchAccountName(batchAccountName), - RelayNamespace(relayNamespace), - StorageAccountName(storageAccountName), - NetworkName(vnetName), - SubnetworkName(batchNodesSubnetName), - SubnetworkName(aksSubnetName), - region, - ApplicationInsightsName(applicationInsightsName), - postgresServer - ) - - /** - * Given a LandingZoneResource, retrieve the value of a specific tag on that resource. - * - * @param resource the LZ resource to inspect - * @param tagName name of the tag whose value to return - * @return the tag's value, or None if the tag is not present on the resource - */ - private def getLandingZoneResourceTagValue(resource: LandingZoneResource, tagName: String): Option[String] = - resource.tags.flatMap(_.get(tagName)) - - /** - * Given a collection of landing zone resources by purpose, return the single resource that - * matches a given resource type and purpose. Throws an error if the resource is not found. - * - * @param landingZoneResourcesByPurpose the collection in which to search - * @param resourceType type of resource to return - * @param purpose purpose of resource to return - * @return the LandingZoneResource - */ - private def getLandingZoneResource( - landingZoneResourcesByPurpose: Map[(LandingZoneResourcePurpose, String), List[LandingZoneResource]], - resourceType: String, - purpose: LandingZoneResourcePurpose - ): F[LandingZoneResource] = - landingZoneResourcesByPurpose - .get((purpose, resourceType.toLowerCase)) - .flatMap(_.headOption) - .fold( - F.raiseError[LandingZoneResource]( - AppCreationException(s"${resourceType} resource with purpose ${purpose} not found in landing zone") - ) - )(F.pure) - - /** - * Given a collection of landing zone resources by purpose, return the name of the single resource that - * matches a given resource type and purpose. Throws an error if the resource is not found. - * - * @param landingZoneResourcesByPurpose the collection in which to search - * @param resourceType type of resource to return - * @param purpose purpose of resource to return - * @param useParent whether to return the resource's name or its parent's name - * @return name of the specified resource - */ - private def getLandingZoneResourceName( - landingZoneResourcesByPurpose: Map[(LandingZoneResourcePurpose, String), List[LandingZoneResource]], - resourceType: String, - purpose: LandingZoneResourcePurpose, - useParent: Boolean - ): F[String] = - for { - resource <- getLandingZoneResource(landingZoneResourcesByPurpose, resourceType, purpose) - name <- getLandingZoneResourceName(resource, useParent) - } yield name - - /** - * Given a landing zone resource, return that resource's name. Throws an error if the name is not found. - * - * @param resource the resource whose name to return - * @param useParent whether to return the resource's name or its parent's name - * @return name of the resource - */ - private def getLandingZoneResourceName(resource: LandingZoneResource, useParent: Boolean): F[String] = - OptionT - .fromOption[F]( - if (useParent) resource.resourceParentId.flatMap(_.split('/').lastOption) - else resource.resourceName.orElse(resource.resourceId.flatMap(_.split('/').lastOption)) - ) - .getOrRaise( - AppCreationException(s"could not determine name for resource $resource") - ) - - private def getLandingZoneResourceId( - landingZoneResourcesByPurpose: Map[(LandingZoneResourcePurpose, String), List[LandingZoneResource]], - resourceType: String, - purpose: LandingZoneResourcePurpose - ): F[String] = - for { - resource <- getLandingZoneResource(landingZoneResourcesByPurpose, resourceType, purpose) - id <- OptionT - .fromOption[F]( - resource.resourceId - ) - .getOrRaise( - AppCreationException(s"could not determine id for resource $resource") - ) - } yield id - - private def getLandingZone(billingProfileId: BillingProfileId, authorization: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Option[LandingZone]] = - for { - ctx <- ev.ask - res <- httpClient.expectOptionOr[ListLandingZonesResult]( - Request[F]( - method = Method.GET, - uri = config.uri - .withPath(Uri.Path.unsafeFromString("/api/landingzones/v1/azure")) - .withQueryParam("billingProfileId", billingProfileId.value), - headers = headers(authorization, ctx.traceId, withBody = false) - ) - )(onError) - landingZoneOption = res.flatMap(listLandingZoneResult => listLandingZoneResult.landingzones.headOption) - } yield landingZoneOption - - private def listLandingZoneResourcesByType(landingZoneId: UUID, authorization: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[List[LandingZoneResourcesByPurpose]] = - for { - ctx <- ev.ask - resOpt <- httpClient.expectOptionOr[ListLandingZoneResourcesResult]( - Request[F]( - method = Method.GET, - uri = config.uri - .withPath( - Uri.Path - .unsafeFromString(s"/api/landingzones/v1/azure/${landingZoneId}/resources") - ), - headers = headers(authorization, ctx.traceId, withBody = false) - ) - )(onError) - } yield resOpt.fold(List.empty[LandingZoneResourcesByPurpose])(res => res.resources) - - // This remains in the legacy dao because of the custom logic around storage containers - // We should migrate to the generated client when possible - override def getWorkspaceStorageContainer(workspaceId: WorkspaceId, authorization: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Option[StorageContainerResponse]] = for { - resp <- getWorkspaceResourceHelper(workspaceId, authorization, WsmResourceType.AzureStorageContainer) - res = resp.resources.collect { - case r - if r.resourceAttributes - .isInstanceOf[ResourceAttributes.StorageContainerResourceAttributes] && r.resourceAttributes - .asInstanceOf[ResourceAttributes.StorageContainerResourceAttributes] - .name - .value - .startsWith("sc-") => // by WSM workspace storage container naming convention - val rr = r.resourceAttributes - .asInstanceOf[ResourceAttributes.StorageContainerResourceAttributes] - StorageContainerResponse(rr.name, r.metadata.resourceId) - }.headOption - } yield res - - private def getWorkspaceResourceHelper(workspaceId: WorkspaceId, - authorization: Authorization, - wsmResourceType: WsmResourceType - )(implicit - ev: Ask[F, AppContext] - ): F[GetWsmResourceResponse] = for { - ctx <- ev.ask - resp <- httpClient.expectOr[GetWsmResourceResponse]( - Request[F]( - method = Method.GET, - uri = config.uri - .withPath( - Uri.Path - .unsafeFromString( - s"/api/workspaces/v1/${workspaceId.value}/resources" - ) - ) - .withMultiValueQueryParams( - Map( - "resource" -> List(wsmResourceType.toString), - "stewardship" -> List("CONTROLLED"), - "limit" -> List("500") - ) - ), - headers = headers(authorization, ctx.traceId, false) - ) - )(onError) - } yield resp - - private def onError(response: Response[F])(implicit ev: Ask[F, AppContext]): F[Throwable] = - for { - context <- ev.ask - body <- response.bodyText.compile.foldMonoid - _ <- logger.error(context.loggingCtx)(s"WSM call failed: $body") - _ <- metrics.incrementCounter("wsm/errorResponse") - } yield WsmException(context.traceId, body) - - def headers(authorization: Authorization, traceId: TraceId, withBody: Boolean): Headers = { - val requestId = Header.Raw(CIString("X-Request-ID"), traceId.asString) - if (withBody) - Headers(authorization, defaultMediaType, requestId) - else - Headers(authorization, requestId) - } -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala index c8897d1a5c..aaeb51b9dd 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala @@ -3,17 +3,11 @@ package dao import _root_.io.circe._ import ca.mrvisser.sealerate -import cats.mtl.Ask import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.leonardo.JsonCodec.{ - googleProjectDecoder, - storageContainerNameDecoder, - wsmControlledResourceIdDecoder -} +import org.broadinstitute.dsde.workbench.leonardo.JsonCodec.{googleProjectDecoder, storageContainerNameDecoder, wsmControlledResourceIdDecoder} import org.broadinstitute.dsde.workbench.leonardo.dao.LandingZoneResourcePurpose.LandingZoneResourcePurpose import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} -import org.http4s.headers.Authorization import java.util.UUID @@ -21,16 +15,6 @@ import java.util.UUID * This is the legacy WsmDAO. It remains because there is some specific logic around models retrieved from WSM * It SHOULD NOT be added to. Favor usage of WsmClientProvider, the auto-generated client. */ -trait WsmDao[F[_]] { - - def getLandingZoneResources(billingProfileId: BillingProfileId, userToken: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[LandingZoneResources] - - def getWorkspaceStorageContainer(workspaceId: WorkspaceId, authorization: Authorization)(implicit - ev: Ask[F, AppContext] - ): F[Option[StorageContainerResponse]] -} final case class WorkspaceDescription(id: WorkspaceId, displayName: String, spendProfile: String, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index bee9240448..6bb36e835f 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -153,8 +153,6 @@ class BaselineDependenciesBuilder { dockerDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, None, true).map(client => HttpDockerDAO[F](client) ) - wsmDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_wsm_client"), true) - .map(client => new HttpWsmDao[F](client, ConfigReader.appConfig.azure.wsm)) wsmClientProvider = new HttpWsmClientProvider(ConfigReader.appConfig.azure.wsm.uri) @@ -282,7 +280,6 @@ class BaselineDependenciesBuilder { jupyterDao, rstudioDAO, welderDao, - wsmDao, authProvider, leoPublisher, publisherQueue, @@ -414,7 +411,6 @@ final case class BaselineDependencies[F[_]]( jupyterDAO: HttpJupyterDAO[F], rstudioDAO: HttpRStudioDAO[F], welderDAO: HttpWelderDAO[F], - wsmDAO: HttpWsmDao[F], authProvider: SamAuthProvider[F], leoPublisher: LeoPublisher[F], publisherQueue: Queue[F, LeoPubsubMessage], diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala index 79d41ecfea..ce07b98904 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala @@ -12,7 +12,6 @@ import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.Name import org.broadinstitute.dsde.workbench.google2.{DiskName, MachineTypeName, RegionName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ import org.broadinstitute.dsde.workbench.leonardo.config.GalaxyDiskConfig -import org.broadinstitute.dsde.workbench.leonardo.dao.StorageContainerResponse import org.broadinstitute.dsde.workbench.leonardo.http.{ dataprocInCreateRuntimeMsgToDataprocRuntime, RuntimeConfigRequest @@ -506,9 +505,6 @@ object LeoPubsubCodec { Either.catchNonFatal(LeoPubsubMessageType.withName(x)).leftMap(_.getMessage) } - implicit val storageContainerResponseDecoder: Decoder[StorageContainerResponse] = - Decoder.forProduct2("name", "resourceId")(StorageContainerResponse.apply) - implicit val leoPubsubMessageDecoder: Decoder[LeoPubsubMessage] = Decoder.instance { message => for { messageType <- message.downField("messageType").as[LeoPubsubMessageType] diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDaoSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDaoSpec.scala deleted file mode 100644 index 3306289473..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpWsmDaoSpec.scala +++ /dev/null @@ -1,170 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import io.circe.syntax.EncoderOps -import io.circe.{Encoder, Printer} -import org.broadinstitute.dsde.workbench.azure.{ApplicationInsightsName, BatchAccountName, RelayNamespace} -import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.HttpWsmDaoConfig -import org.broadinstitute.dsde.workbench.leonardo.dao.LandingZoneResourcePurpose.{ - AKS_NODE_POOL_SUBNET, - LandingZoneResourcePurpose, - POSTGRESQL_SUBNET, - SHARED_RESOURCE, - WORKSPACE_BATCH_SUBNET, - WORKSPACE_COMPUTE_SUBNET -} -import org.broadinstitute.dsde.workbench.leonardo.{ - AKSCluster, - BillingProfileId, - LandingZoneResources, - LeonardoTestSuite, - PostgresServer, - StorageAccountName -} -import org.http4s._ -import org.http4s.client.Client -import org.http4s.headers.Authorization -import org.scalatest.BeforeAndAfterAll -import org.scalatest.flatspec.AnyFlatSpec - -import java.util.UUID - -class HttpWsmDaoSpec extends AnyFlatSpec with LeonardoTestSuite with BeforeAndAfterAll { - val config = HttpWsmDaoConfig(Uri.unsafeFromString("127.0.0.1")) - - val testCases: Map[Option[Map[String, String]], Boolean] = Map( - None -> false, - Option(Map.empty[String, String]) -> false, - Option(Map("not-pgbouncer" -> "true")) -> false, - Option(Map("pgbouncer-enabled" -> "false", "another-tag" -> "true")) -> false, - Option(Map("pgbouncer-enabled" -> "not-a-boolean", "another-tag" -> "true")) -> false, - Option(Map("pgbouncer-enabled" -> "true", "not-pgbouncer" -> "false")) -> true - ) - - testCases.foreach { case (tags, expectedPgBouncer) => - it should s"correctly get landing zone resources and detect PgBouncer for tags: $tags" in { - val billingId = UUID.randomUUID() - val landingZoneId = UUID.randomUUID() - - val originalLandingZone = LandingZone(landingZoneId, billingId, "def", "v1", "2022-11-11") - val landingZoneResponse = ListLandingZonesResult(List(originalLandingZone)) - val landingZoneStringResponse = landingZoneResponse.asJson.printWith(Printer.noSpaces) - - val originalLandingZoneResourcesByPurpose = List( - LandingZoneResourcesByPurpose( - SHARED_RESOURCE, - List( - buildMockLandingZoneResource("Microsoft.ContainerService/managedClusters", "lzcluster"), - buildMockLandingZoneResource("Microsoft.Batch/batchAccounts", "lzbatch"), - buildMockLandingZoneResource("Microsoft.Relay/namespaces", "lznamespace"), - buildMockLandingZoneResource("Microsoft.Storage/storageAccounts", "lzstorage"), - buildMockLandingZoneResource("microsoft.dbforpostgresql/flexibleservers", "lzpostgres", tags = tags), - buildMockLandingZoneResource("microsoft.operationalinsights/workspaces", "lzloganalytics"), - buildMockLandingZoneResource("Microsoft.Insights/components", "lzappinsights") - ) - ), - LandingZoneResourcesByPurpose( - WORKSPACE_BATCH_SUBNET, - List( - buildMockLandingZoneResource("DeployedSubnet", "batchsub") - ) - ), - LandingZoneResourcesByPurpose( - AKS_NODE_POOL_SUBNET, - List( - buildMockLandingZoneResource("DeployedSubnet", "akssub") - ) - ), - LandingZoneResourcesByPurpose( - WORKSPACE_COMPUTE_SUBNET, - List( - buildMockLandingZoneResource("DeployedSubnet", "computesub") - ) - ), - LandingZoneResourcesByPurpose( - POSTGRESQL_SUBNET, - List(buildMockLandingZoneResource("DeployedSubnet", "postgressub")) - ) - ) - val landingZoneResourcesResult = - ListLandingZoneResourcesResult(landingZoneId, originalLandingZoneResourcesByPurpose) - val landingZoneResourcesStringResponse = landingZoneResourcesResult.asJson.printWith(Printer.noSpaces) - - val wsmClient = Client.fromHttpApp[IO]( - HttpApp { request => - val landingZoneRequestString = s"/api/landingzones/v1/azure?billingProfileId=${billingId}" - val resourceRequestString = s"/api/landingzones/v1/azure/${landingZoneId}/resources" - request.uri.renderString match { - case `landingZoneRequestString` => - IO(Response(status = Status.Ok).withEntity(landingZoneStringResponse)) - case `resourceRequestString` => - IO(Response(status = Status.Ok).withEntity(landingZoneResourcesStringResponse)) - } - } - ) - - val wsmDao = new HttpWsmDao[IO](wsmClient, config) - val res = wsmDao - .getLandingZoneResources( - BillingProfileId(billingId.toString), - Authorization(Credentials.Token(AuthScheme.Bearer, "dummy")) - ) - .attempt - .unsafeRunSync() - - res.isRight shouldBe true - - val expectedLandingZoneResources = LandingZoneResources( - landingZoneId, - AKSCluster("lzcluster", Map("aks-cost-vpa-enabled" -> false)), - BatchAccountName("lzbatch"), - RelayNamespace("lznamespace"), - StorageAccountName("lzstorage"), - NetworkName("lzvnet"), - SubnetworkName("id-prefix/batchsub"), - SubnetworkName("akssub"), - com.azure.core.management.Region.US_EAST, - ApplicationInsightsName("lzappinsights"), - Some(PostgresServer("lzpostgres", expectedPgBouncer)) - ) - - val landingZoneResources = res.toOption.get - landingZoneResources shouldBe expectedLandingZoneResources - } - } - - private def buildMockLandingZoneResource(resourceType: String, - resourceName: String, - useId: Boolean = true, - tags: Option[Map[String, String]] = None - ) = - LandingZoneResource( - resourceId = if (useId) Some(s"id-prefix/${resourceName}") else None, - resourceType, - resourceName = if (resourceType.equalsIgnoreCase("DeployedSubnet")) Some(resourceName) else None, - resourceParentId = if (resourceType.equalsIgnoreCase("DeployedSubnet")) Some("lzvnet") else None, - region = com.azure.core.management.Region.US_EAST.toString, - tags - ) - - implicit val landingZoneEncoder: Encoder[LandingZone] = - Encoder.forProduct5("landingZoneId", "billingProfileId", "definition", "version", "createdDate")(x => - (x.landingZoneId, x.billingProfileId, x.definition, x.version, x.createdDate) - ) - implicit val listLandingZonesResultEncoder: Encoder[ListLandingZonesResult] = - Encoder.forProduct1("landingzones")(x => x.landingzones) - - implicit val landingZoneResourceEncoder: Encoder[LandingZoneResource] = - Encoder.forProduct6("resourceId", "resourceType", "resourceName", "resourceParentId", "region", "tags")(x => - (x.resourceId, x.resourceType, x.resourceName, x.resourceParentId, x.region, x.tags) - ) - implicit val landingZoneResourcePurposeEncoder: Encoder[LandingZoneResourcePurpose] = - Encoder.encodeString.contramap(_.toString) - implicit val landingZoneResourcesByPurposeEncoder: Encoder[LandingZoneResourcesByPurpose] = - Encoder.forProduct2("purpose", "deployedResources")(x => (x.purpose, x.deployedResources)) - implicit val listLandingZoneResourcesResultEncoder: Encoder[ListLandingZoneResourcesResult] = - Encoder.forProduct2("id", "resources")(x => (x.id, x.resources)) -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmDAO.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmDAO.scala deleted file mode 100644 index 16da5bb075..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmDAO.scala +++ /dev/null @@ -1,23 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package dao - -import cats.effect.IO -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.azure._ -import org.http4s.headers.Authorization - -import java.util.UUID - -class MockWsmDAO(jobStatus: WsmJobStatus = WsmJobStatus.Succeeded) extends WsmDao[IO] { - override def getLandingZoneResources(billingProfileId: BillingProfileId, userToken: Authorization)(implicit - ev: Ask[IO, AppContext] - ): IO[LandingZoneResources] = - IO.pure( - CommonTestData.landingZoneResources - ) - - override def getWorkspaceStorageContainer(workspaceId: WorkspaceId, authorization: Authorization)(implicit - ev: Ask[IO, AppContext] - ): IO[Option[StorageContainerResponse]] = - IO.pure(Some(StorageContainerResponse(ContainerName("dummy"), WsmControlledResourceId(UUID.randomUUID())))) -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilderSpec.scala index 1a022a5330..c3f5a6f575 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilderSpec.scala @@ -5,7 +5,7 @@ import akka.testkit.TestKit import cats.effect.IO import cats.effect.std.Queue import cats.effect.unsafe.implicits.global -import org.broadinstitute.dsde.workbench.leonardo.dao.{HttpSamDAO, HttpWsmDao} +import org.broadinstitute.dsde.workbench.leonardo.dao.HttpSamDAO import org.broadinstitute.dsde.workbench.leonardo.db.DbReference import org.broadinstitute.dsde.workbench.leonardo.http.service.LeoAppServiceInterp import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoPubsubMessage, RuntimeToMonitor} @@ -73,7 +73,6 @@ class AzureDependenciesBuilderSpec Queue.bounded[IO, LeoPubsubMessage](10).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) when(baselineDependencies.publisherQueue).thenReturn(publisherQueue) when(baselineDependencies.samDAO).thenReturn(mock[HttpSamDAO[IO]]) - when(baselineDependencies.wsmDAO).thenReturn(mock[HttpWsmDao[IO]]) baselineDependencies } } diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index fbe4f0a0a1..31720864fb 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -121,7 +121,6 @@ class GcpDependenciesBuilderSpec mock[HttpJupyterDAO[IO]], mock[HttpRStudioDAO[IO]], mock[HttpWelderDAO[IO]], - mock[HttpWsmDao[IO]], mock[SamAuthProvider[IO]], mock[LeoPublisher[IO]], mock[Queue[IO, LeoPubsubMessage]], diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala index 7c2b707281..cbcbd0491a 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala @@ -62,7 +62,6 @@ trait TestLeoRoutes { } val mockGoogleIamDAO = new MockGoogleIamDAO - val wsmDao = new MockWsmDAO val wsmClientProvider = mock[HttpWsmClientProvider[IO]] val mockPetGoogleStorageDAO: String => GoogleStorageDAO = _ => { val petMock = new MockGoogleStorageDAO diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala index 2f7135c6b9..44a345b57f 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala @@ -45,7 +45,6 @@ trait AppServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestC val appServiceConfig = Config.appServiceConfig val gkeCustomAppConfig = Config.gkeCustomAppConfig - val wsmDao = new MockWsmDAO val (wsmClientProvider, _, _, workspaceApi) = AzureTestUtils.setUpMockWsmApiClientProvider() val (googleWsmClientProvider, _, _, googleWorkspaceApi) = AzureTestUtils.setUpMockWsmApiClientProvider(googleProject = Some(GoogleProject(workspaceId.toString))) @@ -58,12 +57,10 @@ trait AppServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestC googleWorkspaceApi.getWorkspace(ArgumentMatchers.eq(workspaceId2.value), any()) } thenAnswer (_ => throw new Exception("workspace not found")) - val gcpWsmDao = new MockWsmDAO - val appServiceInterp = makeInterp(QueueFactory.makePublisherQueue()) val appServiceInterp2 = makeInterp(QueueFactory.makePublisherQueue(), authProvider = allowListAuthProvider2) val gcpWorkspaceAppServiceInterp = - makeInterp(QueueFactory.makePublisherQueue(), wsmDao = gcpWsmDao, wsmClientProvider = googleWsmClientProvider) + makeInterp(QueueFactory.makePublisherQueue(), wsmClientProvider = googleWsmClientProvider) def withLeoPublisher( publisherQueue: Queue[IO, LeoPubsubMessage] @@ -81,7 +78,6 @@ trait AppServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestC // used when we care about queue state def makeInterp(queue: Queue[IO, LeoPubsubMessage], authProvider: LeoAuthProvider[IO] = allowListAuthProvider, - wsmDao: WsmDao[IO] = wsmDao, enableCustomAppCheckFlag: Boolean = true, enableSasApp: Boolean = true, googleResourceService: GoogleResourceService[IO] = FakeGoogleResourceService, @@ -1753,7 +1749,7 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le it should "V1 GCP - deleteAppRecords delete an app, nodepool and cluster in DB and records AppUsage stopTime" in isolatedDbTest { val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao) + val kubeServiceInterp = makeInterp(publisherQueue) val appName = AppName("app1") val diskConfig = PersistentDiskRequest(DiskName("disk1"), None, None, Map.empty) @@ -1829,7 +1825,7 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le it should "V1 GCP - deleteAllAppsRecords, update all status appropriately, and not queue messages" in isolatedDbTest { val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao) + val kubeServiceInterp = makeInterp(publisherQueue) val appName = AppName("app1") val appName2 = AppName("app2") @@ -1997,7 +1993,7 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le it should "V1 GCP - deleteAllApp, update all status appropriately, and queue multiple messages" in isolatedDbTest { val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao) + val kubeServiceInterp = makeInterp(publisherQueue) val appName = AppName("app1") val appName2 = AppName("app2") @@ -2077,7 +2073,7 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le it should "V1 GCP - deleteAllApp, should fail and not update anything if there is a non-deletable app status" in isolatedDbTest { val publisherQueue = QueueFactory.makePublisherQueue() - val kubeServiceInterp = makeInterp(publisherQueue, wsmDao = gcpWsmDao) + val kubeServiceInterp = makeInterp(publisherQueue) val appName = AppName("app1") val appName2 = AppName("app2") val diskConfig1 = PersistentDiskRequest(DiskName("disk1"), None, None, Map.empty) From 52e602f96794aa31b0804de448e18dbe5e6b2dfe Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 15:00:48 -0400 Subject: [PATCH 15/43] Remove WsmClientProvider (modern WSM interface) --- .../leonardo/dao/WsmApiClientProvider.scala | 235 ---------------- .../http/AzureDependenciesBuilder.scala | 4 +- .../http/BaselineDependenciesBuilder.scala | 4 - .../http/GcpDependenciesBuilder.scala | 4 +- .../http/service/LeoAppServiceInterp.scala | 28 -- .../leonardo/monitor/MonitorAtBoot.scala | 5 +- .../dao/MockWsmApiClientProvider.scala | 87 ------ .../dao/WsmApiClientProviderSpec.scala | 256 ------------------ .../http/GcpDependenciesBuilderSpec.scala | 1 - .../leonardo/http/api/TestLeoRoutes.scala | 2 - .../http/service/AppServiceInterpSpec.scala | 34 +-- .../LeoPubsubMessageSubscriberSpec.scala | 2 - .../leonardo/monitor/MonitorAtBootSpec.scala | 4 +- .../leonardo/util/AzureTestUtils.scala | 63 +---- 14 files changed, 17 insertions(+), 712 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProvider.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmApiClientProvider.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProviderSpec.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProvider.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProvider.scala deleted file mode 100644 index ec8ce77654..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProvider.scala +++ /dev/null @@ -1,235 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import bio.terra.workspace.api.{ControlledAzureResourceApi, ResourceApi, WorkspaceApi} -import bio.terra.workspace.client.ApiClient -import bio.terra.workspace.model.{IamRole, ResourceMetadata, State} -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, ManagedResourceGroupName, SubscriptionId, TenantId} -import org.broadinstitute.dsde.workbench.leonardo.db.WsmResourceType -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, WorkspaceId, WsmControlledResourceId, WsmState} -import org.broadinstitute.dsde.workbench.leonardo.util.WithSpanFilter -import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.glassfish.jersey.client.ClientConfig -import org.http4s.Uri -import org.typelevel.log4cats.StructuredLogger - -/** - * Represents a way to get a client for interacting with workspace manager controlled resources. - * Additional WSM clients can be added here if needed. - * - * Based on `org/broadinstitute/dsde/rawls/dataaccess/workspacemanager/WorkspaceManagerApiClientProvider.scala` - * - */ -trait WsmApiClientProvider[F[_]] { - - // WSM state can be BROKEN, CREATING, READY, UPDATING or NONE, (deleted or doesn't exist) - val possibleStatuses: Array[WsmState] = - State.values().map(_.toString).map(Some(_)).map(WsmState(_)) :+ WsmState(Some("NONE")) - - def getWorkspace(token: String, workspaceId: WorkspaceId, iamRole: IamRole = IamRole.READER)(implicit - ev: Ask[F, AppContext] - ): F[Option[WorkspaceDescription]] - - def getControlledAzureResourceApi(token: String)(implicit ev: Ask[F, AppContext]): F[ControlledAzureResourceApi] - def getResourceApi(token: String)(implicit ev: Ask[F, AppContext]): F[ResourceApi] - - def getWorkspaceApi(token: String)(implicit ev: Ask[F, AppContext]): F[WorkspaceApi] - def getDisk(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] - - def getVm(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] - - def getDatabase(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] - - def getNamespace(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] - - def getIdentity(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] - - def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - resourceType: WsmResourceType - )(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[WsmState] -} - -class HttpWsmClientProvider[F[_]](baseWorkspaceManagerUrl: Uri)(implicit F: Async[F]) extends WsmApiClientProvider[F] { - - /** - * This function wraps the wsm generated client getWorkspace - * The purpose of it is to sanitize the output into the same model our original custom DAO used to reduce the surface area of its removal - */ - override def getWorkspace(token: String, workspaceId: WorkspaceId, iamRole: IamRole = IamRole.READER)(implicit - ev: Ask[F, AppContext] - ): F[Option[WorkspaceDescription]] = - for { - workspaceApi <- getWorkspaceApi(token) - workspaceDescAttempt <- F.delay(workspaceApi.getWorkspace(workspaceId.value, iamRole)).attempt - workspaceDescUnchecked <- F.fromEither(workspaceDescAttempt) - workspaceDescOpt = workspaceDescUnchecked match { - case emptyWorkspace if emptyWorkspace == null => None - case workspace => Some(workspace) - } - workspaceDescResult = workspaceDescOpt.map(workspaceDesc => - WorkspaceDescription( - workspaceId, - workspaceDesc.getDisplayName, - workspaceDesc.getSpendProfile, - workspaceDesc.getAzureContext match { - case emptyContext if emptyContext == null => None - case context => - Some( - AzureCloudContext(TenantId(context.getTenantId), - SubscriptionId(context.getSubscriptionId), - ManagedResourceGroupName(context.getResourceGroupId) - ) - ) - }, - workspaceDesc.getGcpContext match { - case emptyContext if emptyContext == null => None - case context => Some(GoogleProject(context.getProjectId)) - } - ) - ) - } yield workspaceDescResult - - private def getApiClient(token: String)(implicit ev: Ask[F, AppContext]): F[ApiClient] = - for { - ctx <- ev.ask - client = new ApiClient() { - override def performAdditionalClientConfiguration(clientConfig: ClientConfig): Unit = { - super.performAdditionalClientConfiguration(clientConfig) - ctx.span.foreach { span => - clientConfig.register(new WithSpanFilter(span)) - } - } - } - _ = client.setBasePath(baseWorkspaceManagerUrl.renderString) - _ = client.setAccessToken(token) - } yield client - - private def toWsmStatus( - state: Option[String] - )(implicit logger: StructuredLogger[F]): WsmState = { - val wsmState = WsmState(state) - if (!possibleStatuses.contains(wsmState)) logger.warn("Invalid Wsm status") - wsmState - } - override def getResourceApi(token: String)(implicit ev: Ask[F, AppContext]): F[ResourceApi] = - getApiClient(token).map(apiClient => new ResourceApi(apiClient)) - - override def getControlledAzureResourceApi(token: String)(implicit - ev: Ask[F, AppContext] - ): F[ControlledAzureResourceApi] = - getApiClient(token).map(apiClient => new ControlledAzureResourceApi(apiClient)) - - override def getWorkspaceApi(token: String)(implicit ev: Ask[F, AppContext]): F[WorkspaceApi] = - getApiClient(token).map(apiClient => new WorkspaceApi(apiClient)) - - override def getVm(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] = for { - wsmApi <- getControlledAzureResourceApi(token) - attempt <- F.delay(wsmApi.getAzureVm(workspaceId.value, wsmResourceId.value)).attempt - vm = attempt match { - case Right(result) => Some(result.getMetadata) - case Left(_) => None - } - } yield vm - - override def getDisk(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] = for { - wsmApi <- getControlledAzureResourceApi(token) - attempt <- F.delay(wsmApi.getAzureDisk(workspaceId.value, wsmResourceId.value)).attempt - disk = attempt match { - case Right(result) => Some(result.getMetadata) - case Left(_) => None - } - } yield disk - - override def getDatabase(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] = for { - wsmApi <- getControlledAzureResourceApi(token) - attempt <- F.delay(wsmApi.getAzureDatabase(workspaceId.value, wsmResourceId.value)).attempt - db = attempt match { - case Right(result) => Some(result.getMetadata) - case Left(_) => None - } - } yield db - - override def getNamespace(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] = for { - wsmApi <- getControlledAzureResourceApi(token) - attempt <- F.delay(wsmApi.getAzureKubernetesNamespace(workspaceId.value, wsmResourceId.value)).attempt - namespace = attempt match { - case Right(result) => Some(result.getMetadata) - case Left(_) => None - } - } yield namespace - - override def getIdentity(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[Option[ResourceMetadata]] = for { - wsmApi <- getControlledAzureResourceApi(token) - attempt <- F.delay(wsmApi.getAzureManagedIdentity(workspaceId.value, wsmResourceId.value)).attempt - id = attempt match { - case Right(result) => Some(result.getMetadata) - case Left(_) => None - } - } yield id - - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - resourceType: WsmResourceType - )(implicit - ev: Ask[F, AppContext], - log: StructuredLogger[F] - ): F[WsmState] = for { - resource <- resourceType match { - case WsmResourceType.AzureVm => - getVm(token, workspaceId, wsmResourceId) - case WsmResourceType.AzureDatabase => - getDatabase(token, workspaceId, wsmResourceId) - case WsmResourceType.AzureKubernetesNamespace => - getNamespace(token, workspaceId, wsmResourceId) - case WsmResourceType.AzureManagedIdentity => - getIdentity(token, workspaceId, wsmResourceId) - case WsmResourceType.AzureDisk => - getDisk(token, workspaceId, wsmResourceId) - case WsmResourceType.AzureStorageContainer => - F.pure(None) // TODO: no get endpoint for a storage container in WSM yet - } - state = resource match { - case Some(rs) => Some(rs.getState.getValue) - case None => None - } - } yield toWsmStatus(state) - -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilder.scala index 19038d6938..e2206c4662 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AzureDependenciesBuilder.scala @@ -51,8 +51,7 @@ class AzureDependenciesBuilder extends CloudDependenciesBuilder { new MonitorAtBoot[IO]( baselineDependencies.publisherQueue, None, // no GCP dependency - baselineDependencies.samDAO, - baselineDependencies.wsmClientProvider + baselineDependencies.samDAO ) List(monitorAtBoot.process) @@ -86,7 +85,6 @@ class AzureDependenciesBuilder extends CloudDependenciesBuilder { None, None, gkeCustomAppConfig, - baselineDependencies.wsmClientProvider, baselineDependencies.samService ) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index 6bb36e835f..d71ed8fe9e 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -154,8 +154,6 @@ class BaselineDependenciesBuilder { HttpDockerDAO[F](client) ) - wsmClientProvider = new HttpWsmClientProvider(ConfigReader.appConfig.azure.wsm.uri) - bpmClientProvider = new HttpBpmClientProvider(ConfigReader.appConfig.azure.bpm.uri) azureRelay <- AzureRelayService.fromAzureAppRegistrationConfig(ConfigReader.appConfig.azure.appRegistration) @@ -294,7 +292,6 @@ class BaselineDependenciesBuilder { oidcConfig, appDAO, listenerDao, - wsmClientProvider, bpmClientProvider, azureContainerService, runtimeServiceConfig, @@ -425,7 +422,6 @@ final case class BaselineDependencies[F[_]]( openIDConnectConfiguration: OpenIDConnectConfiguration, appDAO: AppDAO[F], listenerDAO: ListenerDAO[F], - wsmClientProvider: HttpWsmClientProvider[F], bpmClientProvider: HttpBpmClientProvider[F], azureContainerService: AzureContainerService[F], runtimeServicesConfig: RuntimeServiceConfig, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilder.scala index ce501eb289..019d712c0a 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilder.scala @@ -88,8 +88,7 @@ class GcpDependencyBuilder extends CloudDependenciesBuilder { new MonitorAtBoot[IO]( baselineDependencies.publisherQueue, Some(gcpDependencies.googleComputeService), - baselineDependencies.samDAO, - baselineDependencies.wsmClientProvider + baselineDependencies.samDAO ) val nonLeoMessageSubscriber = @@ -320,7 +319,6 @@ class GcpDependencyBuilder extends CloudDependenciesBuilder { Some(gcpDependencies.googleComputeService), Some(gcpDependencies.googleResourceService), gkeCustomAppConfig, - baselineDependencies.wsmClientProvider, baselineDependencies.samService ) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala index 3a8477afcb..3edc875990 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala @@ -19,7 +19,6 @@ import org.broadinstitute.dsde.workbench.leonardo.AppType._ import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ import org.broadinstitute.dsde.workbench.leonardo.SamResourceId._ import org.broadinstitute.dsde.workbench.leonardo.config._ -import org.broadinstitute.dsde.workbench.leonardo.dao.WsmApiClientProvider import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService import org.broadinstitute.dsde.workbench.leonardo.db.DBIOInstances.dbioInstance import org.broadinstitute.dsde.workbench.leonardo.db._ @@ -46,7 +45,6 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, computeService: Option[GoogleComputeService[F]], googleResourceService: Option[GoogleResourceService[F]], customAppConfig: CustomAppConfig, - wsmClientProvider: WsmApiClientProvider[F], samService: SamService[F] )(implicit F: Async[F], @@ -673,32 +671,6 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, _ <- getUpdateAppTransaction(appResult.app.id, req) } yield () - private def checkIfSubresourcesDeletable(appId: AppId, - resourceType: WsmResourceType, - userInfo: UserInfo, - workspaceId: WorkspaceId - )(implicit - ev: Ask[F, AppContext] - ): F[Unit] = for { - ctx <- ev.ask - wsmResources <- appControlledResourceQuery - .getAllForAppByType(appId.id, resourceType) - .transaction - _ <- wsmResources.traverse { resource => - for { - wsmState <- wsmClientProvider.getWsmState(userInfo.accessToken.token, - workspaceId, - resource.resourceId, - resourceType - ) - _ <- F - .raiseUnless(wsmState.isDeletable)( - AppResourceCannotBeDeletedException(resource.resourceId, appId, wsmState.value, resourceType, ctx.traceId) - ) - } yield () - } - } yield () - private[service] def getSavableCluster( userEmail: WorkbenchEmail, cloudContext: CloudContext, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala index 6ea5d7028c..ba58f747b6 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBoot.scala @@ -7,7 +7,7 @@ import cats.mtl.Ask import cats.syntax.all._ import fs2.Stream import org.broadinstitute.dsde.workbench.google2.{GoogleComputeService, ZoneName} -import org.broadinstitute.dsde.workbench.leonardo.dao.{SamDAO, WsmApiClientProvider} +import org.broadinstitute.dsde.workbench.leonardo.dao.SamDAO import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.http._ import org.broadinstitute.dsde.workbench.leonardo.model.LeoException @@ -20,8 +20,7 @@ import scala.concurrent.ExecutionContext class MonitorAtBoot[F[_]](publisherQueue: Queue[F, LeoPubsubMessage], computeService: Option[GoogleComputeService[F]], - samDAO: SamDAO[F], - wsmClientProvider: WsmApiClientProvider[F] + samDAO: SamDAO[F] )(implicit F: Async[F], dbRef: DbReference[F], diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmApiClientProvider.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmApiClientProvider.scala deleted file mode 100644 index fb270d8930..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/MockWsmApiClientProvider.scala +++ /dev/null @@ -1,87 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import bio.terra.workspace.api._ -import bio.terra.workspace.model.{IamRole, ResourceMetadata} -import cats.effect.IO -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, ManagedResourceGroupName, SubscriptionId, TenantId} -import org.broadinstitute.dsde.workbench.leonardo.db.WsmResourceType -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, WorkspaceId, WsmControlledResourceId, WsmState} -import org.scalatestplus.mockito.MockitoSugar.mock -import org.typelevel.log4cats.StructuredLogger - -class MockWsmClientProvider(controlledAzureResourceApi: ControlledAzureResourceApi = mock[ControlledAzureResourceApi], - resourceApi: ResourceApi = mock[ResourceApi], - workspaceApi: WorkspaceApi = mock[WorkspaceApi] -) extends WsmApiClientProvider[IO] { - - override def getWorkspace(token: String, workspaceId: WorkspaceId, iamRole: IamRole)(implicit - ev: Ask[IO, AppContext] - ): IO[Option[WorkspaceDescription]] = IO.pure( - Some( - WorkspaceDescription( - workspaceId, - "workspaceName" + workspaceId.value.toString, - "spend-profile", - Some( - AzureCloudContext(TenantId(workspaceId.value.toString), - SubscriptionId(workspaceId.value.toString), - ManagedResourceGroupName(workspaceId.value.toString) - ) - ), - None - ) - ) - ) - - override def getControlledAzureResourceApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[ControlledAzureResourceApi] = - IO.pure(controlledAzureResourceApi) - - override def getResourceApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[ResourceApi] = - IO.pure(resourceApi) - - override def getWorkspaceApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[WorkspaceApi] = - IO.pure(workspaceApi) - - override def getIdentity(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[IO, AppContext], - log: StructuredLogger[IO] - ): IO[Option[ResourceMetadata]] = IO.pure(None) - - override def getVm(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[IO, AppContext], - log: StructuredLogger[IO] - ): IO[Option[ResourceMetadata]] = IO.pure(None) - - override def getDatabase(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[IO, AppContext], - log: StructuredLogger[IO] - ): IO[Option[ResourceMetadata]] = IO.pure(None) - - override def getNamespace(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[IO, AppContext], - log: StructuredLogger[IO] - ): IO[Option[ResourceMetadata]] = IO.pure(None) - - override def getDisk(token: String, workspaceId: WorkspaceId, wsmResourceId: WsmControlledResourceId)(implicit - ev: Ask[IO, AppContext], - log: StructuredLogger[IO] - ): IO[Option[ResourceMetadata]] = IO.pure(None) - - override def getWsmState(token: String, - workspaceId: WorkspaceId, - wsmResourceId: WsmControlledResourceId, - resourceType: WsmResourceType - )(implicit - ev: Ask[IO, AppContext], - log: StructuredLogger[IO] - ): IO[WsmState] = - IO.pure(WsmState(Some("READY"))) - -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProviderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProviderSpec.scala deleted file mode 100644 index a79dffacbe..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmApiClientProviderSpec.scala +++ /dev/null @@ -1,256 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import bio.terra.workspace.api.{ControlledAzureResourceApi, WorkspaceApi} -import bio.terra.workspace.model.{ - AzureDatabaseResource, - AzureDiskResource, - AzureKubernetesNamespaceResource, - AzureManagedIdentityResource, - AzureVmResource, - IamRole, - ResourceMetadata, - ResourceType, - State -} -import cats.effect.IO -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, ManagedResourceGroupName, SubscriptionId, TenantId} -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{ - tokenValue, - workspaceId, - workspaceId2, - wsmResourceId, - wsmWorkspaceDesc -} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.db.WsmResourceType -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, LeonardoTestSuite} -import org.http4s._ -import org.mockito.Mockito.{times, verify, when} -import org.mockito.ArgumentMatchers.{any, eq => mockitoEq} -import org.scalatestplus.mockito.MockitoSugar -import org.scalatest.BeforeAndAfterAll -import org.scalatest.flatspec.AnyFlatSpec - -import java.util.UUID - -class WsmApiClientProviderSpec extends AnyFlatSpec with LeonardoTestSuite with BeforeAndAfterAll with MockitoSugar { - - def newWsmProvider() = - new HttpWsmClientProvider[IO](baseWorkspaceManagerUrl = Uri.unsafeFromString("test")) { - override def getControlledAzureResourceApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[ControlledAzureResourceApi] = IO.pure(setUpMockResourceApi) - } - - val wsmProvider = newWsmProvider() - - it should "return disk metadata" in { - val res = for { - md <- wsmProvider.getDisk(tokenValue, workspaceId, wsmResourceId) - } yield md.get.getResourceType shouldBe ResourceType.AZURE_DISK - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "not return disk metadata if disk doesn't exist" in { - val res = for { - md <- wsmProvider.getDisk(tokenValue, workspaceId2, wsmResourceId) - } yield md shouldBe None - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "return vm metadata" in { - val res = for { - md <- wsmProvider.getVm(tokenValue, workspaceId, wsmResourceId) - } yield md.get.getResourceType shouldBe ResourceType.AZURE_VM - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "return database metadata" in { - val res = for { - md <- wsmProvider.getDatabase(tokenValue, workspaceId, wsmResourceId) - } yield md.get.getResourceType shouldBe ResourceType.AZURE_DATABASE - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "return namespace metadata" in { - val res = for { - md <- wsmProvider.getNamespace(tokenValue, workspaceId, wsmResourceId) - } yield md.get.getResourceType shouldBe ResourceType.AZURE_KUBERNETES_NAMESPACE - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - it should "return managed identity metadata" in { - val res = for { - md <- wsmProvider.getIdentity(tokenValue, workspaceId, wsmResourceId) - } yield md.get.getResourceType shouldBe ResourceType.AZURE_MANAGED_IDENTITY - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - for ( - resourceType <- List( - WsmResourceType.AzureDisk, - WsmResourceType.AzureKubernetesNamespace, - WsmResourceType.AzureManagedIdentity, - WsmResourceType.AzureDatabase, - WsmResourceType.AzureVm - ) - ) - it should s"return a WsmState for a ${resourceType.toString} state" in { - val res = for { - md <- wsmProvider.getWsmState(tokenValue, workspaceId, wsmResourceId, resourceType) - } yield md.value shouldBe "CREATING" - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "a NONE state if an Azure resource doesn't exist" in { - val res = for { - md <- wsmProvider.getWsmState(tokenValue, workspaceId2, wsmResourceId, WsmResourceType.AzureDisk) - } yield md.value shouldBe "NONE" - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "get a workspace" in { - val workspaceApi = mock[WorkspaceApi] - val wsmProvider = new HttpWsmClientProvider[IO](baseWorkspaceManagerUrl = Uri.unsafeFromString("test")) { - override def getWorkspaceApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[WorkspaceApi] = IO.pure(workspaceApi) - } - - when { - workspaceApi.getWorkspace(any(), any()) - } thenAnswer { invocation => - val workspaceId = invocation.getArgument[UUID](0) - wsmWorkspaceDesc.id(workspaceId) - } - - val res = for { - workspace <- wsmProvider.getWorkspace(tokenValue, workspaceId, IamRole.WRITER) - } yield { - workspace.isDefined shouldBe true - workspace.map(_.spendProfile) shouldBe Some(wsmWorkspaceDesc.getSpendProfile) - workspace.map(_.id) shouldBe Some(workspaceId) - workspace.flatMap(_.azureContext) shouldBe Some( - AzureCloudContext( - TenantId(wsmWorkspaceDesc.getAzureContext.getTenantId), - SubscriptionId(wsmWorkspaceDesc.getAzureContext.getSubscriptionId), - ManagedResourceGroupName(wsmWorkspaceDesc.getAzureContext.getResourceGroupId) - ) - ) - workspace.flatMap(_.gcpContext.map(_.value)) shouldBe Some(wsmWorkspaceDesc.getGcpContext.getProjectId) - workspace.map(_.displayName) shouldBe Some(wsmWorkspaceDesc.getDisplayName) - verify(workspaceApi, times(1)).getWorkspace(mockitoEq(workspaceId.value), mockitoEq(IamRole.WRITER)) - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle null contexts in get workspace" in { - val workspaceApi = mock[WorkspaceApi] - val wsmProvider = new HttpWsmClientProvider[IO](baseWorkspaceManagerUrl = Uri.unsafeFromString("test")) { - override def getWorkspaceApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[WorkspaceApi] = IO.pure(workspaceApi) - } - - when { - workspaceApi.getWorkspace(any(), any()) - } thenAnswer { invocation => - val workspaceId = invocation.getArgument[UUID](0) - wsmWorkspaceDesc.id(workspaceId).azureContext(null).gcpContext(null) - } - - val res = for { - workspace <- wsmProvider.getWorkspace(tokenValue, workspaceId, IamRole.WRITER) - } yield { - workspace.isDefined shouldBe true - workspace.map(_.spendProfile) shouldBe Some(wsmWorkspaceDesc.getSpendProfile) - workspace.map(_.id) shouldBe Some(workspaceId) - workspace.flatMap(_.azureContext) shouldBe None - workspace.flatMap(_.gcpContext) shouldBe None - workspace.map(_.displayName) shouldBe Some(wsmWorkspaceDesc.getDisplayName) - verify(workspaceApi, times(1)).getWorkspace(mockitoEq(workspaceId.value), mockitoEq(IamRole.WRITER)) - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - it should "handle null for get workspace" in { - val workspaceApi = mock[WorkspaceApi] - val wsmProvider = new HttpWsmClientProvider[IO](baseWorkspaceManagerUrl = Uri.unsafeFromString("test")) { - override def getWorkspaceApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[WorkspaceApi] = IO.pure(workspaceApi) - } - - when { - workspaceApi.getWorkspace(any(), any()) - } thenAnswer { _ => - null - } - - val res = for { - workspace <- wsmProvider.getWorkspace(tokenValue, workspaceId, IamRole.WRITER) - } yield { - verify(workspaceApi, times(1)).getWorkspace(mockitoEq(workspaceId.value), mockitoEq(IamRole.WRITER)) - workspace.isDefined shouldBe false - } - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } - - private def setUpMockResourceApi: ControlledAzureResourceApi = { - val api = mock[ControlledAzureResourceApi] - when { - api.getAzureDisk(workspaceId2.value, wsmResourceId.value) - } thenAnswer { _ => - throw new Exception("Resource not found") - } - when { - api.getAzureDisk(workspaceId.value, wsmResourceId.value) - } thenAnswer { _ => - new AzureDiskResource().metadata( - new ResourceMetadata() - .resourceId(wsmResourceId.value) - .resourceType(ResourceType.AZURE_DISK) - .state(State.CREATING) - ) - } - when { - api.getAzureVm(workspaceId.value, wsmResourceId.value) - } thenAnswer { _ => - new AzureVmResource().metadata( - new ResourceMetadata() - .resourceId(wsmResourceId.value) - .resourceType(ResourceType.AZURE_VM) - .state(State.CREATING) - ) - } - when { - api.getAzureDatabase(workspaceId.value, wsmResourceId.value) - } thenAnswer { _ => - new AzureDatabaseResource().metadata( - new ResourceMetadata() - .resourceId(wsmResourceId.value) - .resourceType(ResourceType.AZURE_DATABASE) - .state(State.CREATING) - ) - } - when { - api.getAzureKubernetesNamespace(workspaceId.value, wsmResourceId.value) - } thenAnswer { _ => - new AzureKubernetesNamespaceResource().metadata( - new ResourceMetadata() - .resourceId(wsmResourceId.value) - .resourceType(ResourceType.AZURE_KUBERNETES_NAMESPACE) - .state(State.CREATING) - ) - } - when { - api.getAzureManagedIdentity(workspaceId.value, wsmResourceId.value) - } thenAnswer { _ => - new AzureManagedIdentityResource().metadata( - new ResourceMetadata() - .resourceId(wsmResourceId.value) - .resourceType(ResourceType.AZURE_MANAGED_IDENTITY) - .state(State.CREATING) - ) - } - api - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index 31720864fb..5ece4d8b77 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -135,7 +135,6 @@ class GcpDependenciesBuilderSpec mock[OpenIDConnectConfiguration], mock[AppDAO[IO]], mock[ListenerDAO[IO]], - mock[HttpWsmClientProvider[IO]], mock[HttpBpmClientProvider[IO]], mock[AzureContainerService[IO]], mock[RuntimeServiceConfig], diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala index cbcbd0491a..de208d6658 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala @@ -62,7 +62,6 @@ trait TestLeoRoutes { } val mockGoogleIamDAO = new MockGoogleIamDAO - val wsmClientProvider = mock[HttpWsmClientProvider[IO]] val mockPetGoogleStorageDAO: String => GoogleStorageDAO = _ => { val petMock = new MockGoogleStorageDAO petMock.buckets += userScriptBucketName -> Set( @@ -113,7 +112,6 @@ trait TestLeoRoutes { Some(FakeGoogleComputeService), Some(FakeGoogleResourceService), Config.gkeCustomAppConfig, - wsmClientProvider, MockSamService ) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala index 44a345b57f..e2cf5495f0 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala @@ -18,19 +18,17 @@ import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config.Config.leoKubernetesConfig import org.broadinstitute.dsde.workbench.leonardo.config.{Config, CustomAppConfig, CustomApplicationAllowListConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.model._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, DeleteAppMessage} import org.broadinstitute.dsde.workbench.leonardo.monitor.{ClusterNodepoolAction, LeoPubsubMessage, LeoPubsubMessageType} -import org.broadinstitute.dsde.workbench.leonardo.util.{AzureTestUtils, QueueFactory} +import org.broadinstitute.dsde.workbench.leonardo.util.QueueFactory import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} import org.broadinstitute.dsde.workbench.util2.messaging.CloudPublisher import org.broadinstitute.dsp.{ChartName, ChartVersion} import org.http4s.Uri -import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.{verify, when} import org.scalatest.Assertion @@ -45,22 +43,10 @@ trait AppServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestC val appServiceConfig = Config.appServiceConfig val gkeCustomAppConfig = Config.gkeCustomAppConfig - val (wsmClientProvider, _, _, workspaceApi) = AzureTestUtils.setUpMockWsmApiClientProvider() - val (googleWsmClientProvider, _, _, googleWorkspaceApi) = - AzureTestUtils.setUpMockWsmApiClientProvider(googleProject = Some(GoogleProject(workspaceId.toString))) - - when { - workspaceApi.getWorkspace(ArgumentMatchers.eq(workspaceId2.value), any()) - } thenAnswer (_ => throw new Exception("workspace not found")) - - when { - googleWorkspaceApi.getWorkspace(ArgumentMatchers.eq(workspaceId2.value), any()) - } thenAnswer (_ => throw new Exception("workspace not found")) - val appServiceInterp = makeInterp(QueueFactory.makePublisherQueue()) val appServiceInterp2 = makeInterp(QueueFactory.makePublisherQueue(), authProvider = allowListAuthProvider2) val gcpWorkspaceAppServiceInterp = - makeInterp(QueueFactory.makePublisherQueue(), wsmClientProvider = googleWsmClientProvider) + makeInterp(QueueFactory.makePublisherQueue()) def withLeoPublisher( publisherQueue: Queue[IO, LeoPubsubMessage] @@ -82,7 +68,6 @@ trait AppServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestC enableSasApp: Boolean = true, googleResourceService: GoogleResourceService[IO] = FakeGoogleResourceService, customAppConfig: CustomAppConfig = gkeCustomAppConfig, - wsmClientProvider: WsmApiClientProvider[IO] = wsmClientProvider, samService: SamService[IO] = MockSamService ) = { val appConfig = appServiceConfig.copy(enableCustomAppCheck = enableCustomAppCheckFlag, enableSasApp = enableSasApp) @@ -94,7 +79,6 @@ trait AppServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestC Some(FakeGoogleComputeService), Some(googleResourceService), customAppConfig, - wsmClientProvider, samService ) } @@ -144,7 +128,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le Some(passComputeService), Some(FakeGoogleResourceService), gkeCustomAppConfig, - wsmClientProvider, MockSamService ) val notEnoughMemoryAppService = new LeoAppServiceInterp[IO]( @@ -154,7 +137,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le Some(notEnoughMemoryComputeService), Some(FakeGoogleResourceService), gkeCustomAppConfig, - wsmClientProvider, MockSamService ) val notEnoughCpuAppService = new LeoAppServiceInterp[IO]( @@ -164,7 +146,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le Some(notEnoughCpuComputeService), Some(FakeGoogleResourceService), gkeCustomAppConfig, - wsmClientProvider, MockSamService ) @@ -195,7 +176,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le Some(FakeGoogleComputeService), Some(noLabelsGoogleResourceService), gkeCustomAppConfig, - wsmClientProvider, MockSamService ) @@ -225,7 +205,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le Some(FakeGoogleComputeService), Some(FakeGoogleResourceService), gkeCustomAppConfig, - wsmClientProvider, MockSamService ) val res = interp @@ -1369,7 +1348,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le true, List() ), - wsmClientProvider, MockSamService ) val appReq = createAppRequest.copy( @@ -1519,7 +1497,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le true, List() ), - wsmClientProvider, MockSamService ) val appReq = createAppRequest.copy( @@ -1562,7 +1539,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le true, List() ), - wsmClientProvider, MockSamService ) val appReq = createAppRequest.copy( @@ -1604,7 +1580,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le true, List() ), - wsmClientProvider, MockSamService ) val appReq = createAppRequest.copy( @@ -1643,7 +1618,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le true, List() ), - wsmClientProvider, MockSamService ) val appReq = createAppRequest.copy( @@ -1688,7 +1662,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le true, List() ), - wsmClientProvider, MockSamService ) val appReq = createAppRequest.copy( @@ -1733,7 +1706,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le true, List() ), - wsmClientProvider, MockSamService ) val appReq = createAppRequest.copy( @@ -1925,7 +1897,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le Some(FakeGoogleComputeService), Some(FakeGoogleResourceService), gkeCustomAppConfig, - wsmClientProvider, mockSamService ) @@ -1982,7 +1953,6 @@ class AppServiceInterpTest extends AnyFlatSpec with AppServiceInterpSpec with Le Some(FakeGoogleComputeService), Some(FakeGoogleResourceService), gkeCustomAppConfig, - wsmClientProvider, mockSamService ) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala index a51703cc5a..aa3fb404b4 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala @@ -2064,8 +2064,6 @@ class LeoPubsubMessageSubscriberSpec MockSamService ) } - val (mockWsm, mockControlledResourceApi, mockResourceApi, workspaceApi) = - AzureTestUtils.setUpMockWsmApiClientProvider() def makeTaskQueue(): Queue[IO, Task[IO]] = Queue.bounded[IO, Task[IO]](10).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala index f667095b69..537e683f57 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala @@ -7,7 +7,7 @@ import cats.syntax.all._ import org.broadinstitute.dsde.workbench.google2.mock.FakeGoogleComputeService import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData._ -import org.broadinstitute.dsde.workbench.leonardo.dao.{MockSamDAO, MockWsmClientProvider} +import org.broadinstitute.dsde.workbench.leonardo.dao.MockSamDAO import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent import org.broadinstitute.dsde.workbench.leonardo.monitor.ClusterNodepoolAction.{ CreateClusterAndNodepool, @@ -254,5 +254,5 @@ class MonitorAtBootSpec extends AnyFlatSpec with TestComponent with LeonardoTest queue: Queue[IO, LeoPubsubMessage] = Queue.bounded[IO, LeoPubsubMessage](10).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) ): MonitorAtBoot[IO] = - new MonitorAtBoot[IO](queue, Some(FakeGoogleComputeService), new MockSamDAO(), new MockWsmClientProvider()) + new MonitorAtBoot[IO](queue, Some(FakeGoogleComputeService), new MockSamDAO()) } diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala index 93d0008a86..ccd6f6c7b7 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala @@ -1,41 +1,24 @@ package org.broadinstitute.dsde.workbench.leonardo.util -import scala.collection.mutable -import cats.effect.IO -import org.mockito.Mockito.when -import org.mockito.ArgumentMatchers.any import bio.terra.workspace.api.{ControlledAzureResourceApi, ResourceApi, WorkspaceApi} -import bio.terra.workspace.model.{ - AzureVmAttributes, - AzureVmResource, - CreateControlledAzureDiskRequestBody, - CreateControlledAzureDiskRequestV2Body, - CreateControlledAzureResourceResult, - CreatedControlledAzureStorageContainer, - CreatedControlledAzureVmResult, - DeleteControlledAzureResourceResult, - ErrorReport, - IamRole, - JobReport -} +import bio.terra.workspace.model._ +import cats.effect.IO import cats.mtl.Ask import com.azure.resourcemanager.compute.models.{PowerState, VirtualMachine} -import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, ManagedResourceGroupName, SubscriptionId, TenantId} +import org.broadinstitute.dsde.workbench.azure.AzureCloudContext import org.broadinstitute.dsde.workbench.azure.mock.FakeAzureVmService +import org.broadinstitute.dsde.workbench.leonardo.AppContext import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.wsmWorkspaceDesc -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, WorkspaceId} -import org.broadinstitute.dsde.workbench.leonardo.dao.{ - MockWsmClientProvider, - WorkspaceDescription, - WsmApiClientProvider -} import org.broadinstitute.dsde.workbench.model.TraceId import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.util2.InstanceName +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.when import org.scalatestplus.mockito.MockitoSugar import reactor.core.publisher.Mono import java.util.UUID +import scala.collection.mutable object AzureTestUtils extends MockitoSugar { implicit val appContext: Ask[IO, AppContext] = AppContext @@ -47,7 +30,7 @@ object AzureTestUtils extends MockitoSugar { vmJobStatus: JobReport.StatusEnum = JobReport.StatusEnum.SUCCEEDED, storageContainerJobStatus: JobReport.StatusEnum = JobReport.StatusEnum.SUCCEEDED, googleProject: Option[GoogleProject] = None - ): (WsmApiClientProvider[IO], ControlledAzureResourceApi, ResourceApi, WorkspaceApi) = { + ): (ControlledAzureResourceApi, ResourceApi, WorkspaceApi) = { val api = mock[ControlledAzureResourceApi] val workspaceApi = mock[WorkspaceApi] val resourceApi = mock[ResourceApi] @@ -195,35 +178,7 @@ object AzureTestUtils extends MockitoSugar { wsmWorkspaceDesc.id(workspaceId) } - val wsm = new MockWsmClientProvider(api, resourceApi, workspaceApi) { - override def getWorkspace(token: String, workspaceId: WorkspaceId, iamRole: IamRole)(implicit - ev: Ask[IO, AppContext] - ): IO[Option[WorkspaceDescription]] = { - val azureContext = - if (googleProject.isDefined) None - else - Some( - AzureCloudContext(TenantId(workspaceId.value.toString), - SubscriptionId(workspaceId.value.toString), - ManagedResourceGroupName(workspaceId.value.toString) - ) - ) - val googleContext = if (googleProject.isDefined) googleProject else None - IO.pure( - Some( - WorkspaceDescription( - workspaceId, - "workspaceName" + workspaceId.value.toString, - "spend-profile", - azureContext, - googleContext - ) - ) - ) - } - } - - (wsm, api, resourceApi, workspaceApi) + (api, resourceApi, workspaceApi) } def setupFakeAzureVmService(startVm: Boolean = true, From 3e093f929b884f770f0855e3a25aefb4c1f97064 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 15:05:43 -0400 Subject: [PATCH 16/43] Remove BpmClientProvider --- .../leonardo/dao/BpmApiClientProvider.scala | 56 ------------------- .../http/BaselineDependenciesBuilder.scala | 4 -- .../dao/BpmApiClientProviderSpec.scala | 48 ---------------- .../http/GcpDependenciesBuilderSpec.scala | 1 - 4 files changed, 109 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProvider.scala delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProviderSpec.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProvider.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProvider.scala deleted file mode 100644 index 462d60daa4..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProvider.scala +++ /dev/null @@ -1,56 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import bio.terra.profile.api.ProfileApi -import bio.terra.profile.model.ProfileModel -import bio.terra.profile.client.ApiClient -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.http4s.Uri -import org.glassfish.jersey.client.ClientConfig -import org.broadinstitute.dsde.workbench.leonardo.util.WithSpanFilter - -import java.util.UUID - -trait BpmApiClientProvider[F[_]] { - - def getProfileApi(token: String)(implicit ev: Ask[F, AppContext]): F[ProfileApi] - - def getProfile(token: String, profileId: UUID)(implicit - ev: Ask[F, AppContext] - ): F[Option[ProfileModel]] - -} - -class HttpBpmClientProvider[F[_]](baseBpmUrl: Uri)(implicit F: Async[F]) extends BpmApiClientProvider[F] { - private def getApiClient(token: String)(implicit ev: Ask[F, AppContext]): F[ApiClient] = - for { - ctx <- ev.ask - client = new ApiClient() { - override def performAdditionalClientConfiguration(clientConfig: ClientConfig): Unit = { - super.performAdditionalClientConfiguration(clientConfig) - ctx.span.foreach { span => - clientConfig.register(new WithSpanFilter(span)) - } - } - } - _ = client.setBasePath(baseBpmUrl.renderString) - _ = client.setAccessToken(token) - } yield client - - override def getProfileApi(token: String)(implicit ev: Ask[F, AppContext]): F[ProfileApi] = - getApiClient(token).map(apiClient => new ProfileApi(apiClient)) - - override def getProfile(token: String, profileId: UUID)(implicit - ev: Ask[F, AppContext] - ): F[Option[ProfileModel]] = for { - bpmApi <- getProfileApi(token) - attempt <- F.delay(bpmApi.getProfile(profileId)).attempt - profile = attempt match { - case Right(result) => Some(result) - case Left(_) => None - } - } yield profile - -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index d71ed8fe9e..ee62c0cac3 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -154,8 +154,6 @@ class BaselineDependenciesBuilder { HttpDockerDAO[F](client) ) - bpmClientProvider = new HttpBpmClientProvider(ConfigReader.appConfig.azure.bpm.uri) - azureRelay <- AzureRelayService.fromAzureAppRegistrationConfig(ConfigReader.appConfig.azure.appRegistration) azureVmService <- AzureVmService.fromAzureAppRegistrationConfig(ConfigReader.appConfig.azure.appRegistration) @@ -292,7 +290,6 @@ class BaselineDependenciesBuilder { oidcConfig, appDAO, listenerDao, - bpmClientProvider, azureContainerService, runtimeServiceConfig, kubernetesDnsCache, @@ -422,7 +419,6 @@ final case class BaselineDependencies[F[_]]( openIDConnectConfiguration: OpenIDConnectConfiguration, appDAO: AppDAO[F], listenerDAO: ListenerDAO[F], - bpmClientProvider: HttpBpmClientProvider[F], azureContainerService: AzureContainerService[F], runtimeServicesConfig: RuntimeServiceConfig, kubernetesDnsCache: KubernetesDnsCache[F], diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProviderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProviderSpec.scala deleted file mode 100644 index 86f2d88678..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/dao/BpmApiClientProviderSpec.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import bio.terra.profile.api.ProfileApi -import bio.terra.profile.model.{Organization, ProfileModel} -import cats.effect.IO -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.tokenValue -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.{AppContext, LeonardoTestSuite} -import org.http4s._ -import org.mockito.Mockito.when -import org.scalatestplus.mockito.MockitoSugar -import org.scalatest.BeforeAndAfterAll -import org.scalatest.flatspec.AnyFlatSpec -import scala.jdk.CollectionConverters._ -import cats.effect.unsafe.implicits.global -import java.util.UUID -import cats.mtl.Ask - -class BpmApiClientProviderSpec extends AnyFlatSpec with LeonardoTestSuite with BeforeAndAfterAll with MockitoSugar { - - val profileId = UUID.randomUUID() - - def newBpmProvider() = - new HttpBpmClientProvider[IO](baseBpmUrl = Uri.unsafeFromString("test")) { - override def getProfileApi(token: String)(implicit - ev: Ask[IO, AppContext] - ): IO[ProfileApi] = IO.pure(setUpMockProfileApi) - } - - val bpmProvider = newBpmProvider() - - it should "return a profile" in { - val resIO = bpmProvider.getProfile(tokenValue, profileId) - val res = resIO.unsafeRunSync() - res.get.getId shouldBe profileId - res.get.getOrganization.getLimits shouldBe Map("autopause" -> "30").asJava - } - - private def setUpMockProfileApi: ProfileApi = { - val api = mock[ProfileApi] - when { - api.getProfile(profileId) - } thenAnswer { _ => - new ProfileModel().id(profileId).organization(new Organization().limits(Map("autopause" -> "30").asJava)) - } - api - } -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index 5ece4d8b77..2ad50fb08a 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -135,7 +135,6 @@ class GcpDependenciesBuilderSpec mock[OpenIDConnectConfiguration], mock[AppDAO[IO]], mock[ListenerDAO[IO]], - mock[HttpBpmClientProvider[IO]], mock[AzureContainerService[IO]], mock[RuntimeServiceConfig], mock[KubernetesDnsCache[IO]], From ae6c629c1c78fca5a59070a480a316789378a11b Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 15:08:27 -0400 Subject: [PATCH 17/43] Remove WSM and BPM config --- http/src/main/resources/leo.conf | 8 -------- http/src/main/resources/reference.conf | 8 -------- .../dsde/workbench/leonardo/config/BpmConfig.scala | 5 ----- .../dsde/workbench/leonardo/config/HttpWsmDaoConfig.scala | 5 ----- .../dsde/workbench/leonardo/http/ConfigReader.scala | 2 -- .../dsde/workbench/leonardo/http/ConfigReaderSpec.scala | 2 -- 6 files changed, 30 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/BpmConfig.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/HttpWsmDaoConfig.scala diff --git a/http/src/main/resources/leo.conf b/http/src/main/resources/leo.conf index 7debf83fbc..6170a83548 100644 --- a/http/src/main/resources/leo.conf +++ b/http/src/main/resources/leo.conf @@ -191,14 +191,6 @@ azure { } } - wsm { - uri = ${?WSM_URL} - } - - bpm { - uri = ${?BPM_URL} - } - tdr { url = ${?DATA_REPO_URL} } diff --git a/http/src/main/resources/reference.conf b/http/src/main/resources/reference.conf index 9df4207f0f..d0c597fe2b 100644 --- a/http/src/main/resources/reference.conf +++ b/http/src/main/resources/reference.conf @@ -185,14 +185,6 @@ azure { } } - wsm { - uri = "https://localhost:8000" - } - - bpm { - uri = "https://localhost:8000" - } - tdr { url = "https://jade.datarepo-dev.broadinstitute.org" } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/BpmConfig.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/BpmConfig.scala deleted file mode 100644 index 2f40c9fbf7..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/BpmConfig.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.config - -import org.http4s.Uri - -final case class BpmConfig(uri: Uri) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/HttpWsmDaoConfig.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/HttpWsmDaoConfig.scala deleted file mode 100644 index 107ef3d0de..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/HttpWsmDaoConfig.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.config - -import org.http4s.Uri - -final case class HttpWsmDaoConfig(uri: Uri) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala index 84f96cae56..83fef20ea6 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala @@ -19,8 +19,6 @@ object ConfigReader { .loadOrThrow[AppConfig] } final case class AzureConfig( - wsm: HttpWsmDaoConfig, - bpm: BpmConfig, appRegistration: AzureAppRegistrationConfig, allowedSharedApps: List[AppType], tdr: TdrConfig, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala index df21d4d276..108f78f128 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala @@ -28,8 +28,6 @@ class ConfigReaderSpec extends AnyFlatSpec with Matchers { Vector("bogus") ), AzureConfig( - HttpWsmDaoConfig(Uri.unsafeFromString("https://localhost:8000")), - BpmConfig(Uri.unsafeFromString("https://localhost:8000")), AzureAppRegistrationConfig(ClientId(""), ClientSecret(""), ManagedAppTenantId("")), List(), TdrConfig("https://jade.datarepo-dev.broadinstitute.org"), From 5dd85e2ae9c2289a3d079f22dd2ef9ca67352025 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 15:19:19 -0400 Subject: [PATCH 18/43] Remove ListenerDao --- .../leonardo/dao/HttpListenerDAO.scala | 28 ----------- .../workbench/leonardo/dao/ListenerDAO.scala | 11 ----- .../http/AppDependenciesBuilder.scala | 2 - .../http/BaselineDependenciesBuilder.scala | 5 -- .../leonardo/monitor/LeoMetricsMonitor.scala | 48 ++++--------------- .../http/GcpDependenciesBuilderSpec.scala | 1 - .../monitor/LeoMetricsMonitorSpec.scala | 12 ----- 7 files changed, 9 insertions(+), 98 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpListenerDAO.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/ListenerDAO.scala diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpListenerDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpListenerDAO.scala deleted file mode 100644 index eeade1dd7c..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/HttpListenerDAO.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s.client.Client -import org.http4s.client.dsl.Http4sClientDsl -import org.http4s.{Method, Request, Uri} - -class HttpListenerDAO[F[_]](httpClient: Client[F])(implicit - F: Async[F], - metrics: OpenTelemetryMetrics[F] -) extends ListenerDAO[F] - with Http4sClientDsl[F] { - override def getStatus(baseUri: Uri)(implicit ev: Ask[F, AppContext]): F[Boolean] = - for { - _ <- metrics.incrementCounter("listener/status") - listenerStatusUri = baseUri / "listenerstatus" - res <- httpClient.status( - Request[F]( - method = Method.GET, - uri = listenerStatusUri - ) - ) - } yield res.isSuccess -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/ListenerDAO.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/ListenerDAO.scala deleted file mode 100644 index f99727ef5a..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/ListenerDAO.scala +++ /dev/null @@ -1,11 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.dao - -import cats.mtl.Ask -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.http4s.Uri - -trait ListenerDAO[F[_]] { - def getStatus(baseUri: Uri)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 074d0c4266..1a6735af60 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -141,8 +141,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu val metricsMonitor = new LeoMetricsMonitor( ConfigReader.appConfig.metrics, baselineDependencies.appDAO, - baselineDependencies.listenerDAO, - baselineDependencies.samDAO, kubeAlg, baselineDependencies.azureContainerService ) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index ee62c0cac3..93f7302f5c 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -132,9 +132,6 @@ class BaselineDependenciesBuilder { cloudAuthTokenProvider ) ) - listenerDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_listener_client"), false).map( - client => new HttpListenerDAO[F](client) - ) jupyterDao <- buildHttpClient(sslContext, proxyResolver.resolveHttp4s, Some("leo_jupyter_client"), false).map( client => new HttpJupyterDAO[F](runtimeDnsCache, client, samDao) ) @@ -289,7 +286,6 @@ class BaselineDependenciesBuilder { samResourceCache, oidcConfig, appDAO, - listenerDao, azureContainerService, runtimeServiceConfig, kubernetesDnsCache, @@ -418,7 +414,6 @@ final case class BaselineDependencies[F[_]]( samResourceCache: scalacache.Cache[F, SamResourceCacheKey, (Option[String], Option[AppAccessScope])], openIDConnectConfiguration: OpenIDConnectConfiguration, appDAO: AppDAO[F], - listenerDAO: ListenerDAO[F], azureContainerService: AzureContainerService[F], runtimeServicesConfig: RuntimeServiceConfig, kubernetesDnsCache: KubernetesDnsCache[F], diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala index abc9c3e05b..5265acb55a 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala @@ -11,15 +11,13 @@ import org.broadinstitute.dsde.workbench.azure.{AKSClusterName, AzureCloudContex import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName import org.broadinstitute.dsde.workbench.leonardo.LeoLenses.cloudContextToManagedResourceGroup import org.broadinstitute.dsde.workbench.leonardo.config.{Config, KubernetesAppConfig} -import org.broadinstitute.dsde.workbench.leonardo.dao.{ToolDAO, _} -import org.broadinstitute.dsde.workbench.leonardo.db.{clusterQuery, DbReference, KubernetesServiceDbQueries} +import org.broadinstitute.dsde.workbench.leonardo.dao._ +import org.broadinstitute.dsde.workbench.leonardo.db.{DbReference, KubernetesServiceDbQueries, clusterQuery} import org.broadinstitute.dsde.workbench.leonardo.http.{dbioToIO, _} import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ -import org.broadinstitute.dsde.workbench.leonardo.util.{AppCreationException, KubernetesAlgebra} +import org.broadinstitute.dsde.workbench.leonardo.util.KubernetesAlgebra import org.broadinstitute.dsde.workbench.model.TraceId import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.http4s.headers.Authorization -import org.http4s.{AuthScheme, Credentials, Uri} import org.typelevel.log4cats.StructuredLogger import scala.concurrent.ExecutionContext @@ -29,8 +27,6 @@ import scala.jdk.CollectionConverters._ /** Collects metrics about active Leo runtimes and apps. */ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, appDAO: AppDAO[F], - listenerDAO: ListenerDAO[F], - samDAO: SamDAO[F], kubeAlg: KubernetesAlgebra[F], azureContainerService: AzureContainerService[F] )(implicit @@ -150,10 +146,10 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, // Only care about Running apps for health check metrics a <- n.apps if a.status == AppStatus.Running s <- a.appResources.services - } yield (c.cloudContext, c.asyncFields.get.loadBalancerIp, a, s.config.name) + } yield (c.cloudContext, a, s.config.name) allServices - .parTraverseN(parallelism) { case (cloudContext, baseUri, app, serviceName) => + .parTraverseN(parallelism) { case (cloudContext, app, serviceName) => for { ctx <- ev.ask // For GCP just test if the app is available through the Leo proxy. @@ -161,36 +157,10 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, isUp <- cloudContext match { case CloudContext.Gcp(project) => appDAO.isProxyAvailable(project, app.appName, serviceName, ctx.traceId) - case CloudContext.Azure(_) => - for { - token <- ConfigReader.appConfig.azure.hostingModeConfig.enabled match { - case false => - for { - tokenOpt <- samDAO.getCachedArbitraryPetAccessToken(app.auditInfo.creator) - token <- F.fromOption( - tokenOpt, - AppCreationException(s"Pet not found for user ${app.auditInfo.creator}", Some(ctx.traceId)) - ) - } yield token - case true => - for { - leoAuth <- samDAO.getLeoAuthToken - token = leoAuth.credentials.toString().split(" ")(1) - } yield token - } - - authHeader = Authorization(Credentials.Token(AuthScheme.Bearer, token)) - relayPath = Uri - .unsafeFromString(baseUri.asString) / s"${app.appName.value}-${app.workspaceId.map(_.value.toString).getOrElse("")}" - isUp <- serviceName match { - case s if s == ConfigReader.appConfig.azure.listenerChartConfig.service.config.name => - listenerDAO.getStatus(relayPath).handleError(_ => false) - case s => - logger.warn(ctx.loggingCtx)( - s"Unexpected app service encountered during health checks: ${s.value}" - ) >> F.pure(false) - } - } yield isUp + case _ => + logger.warn(ctx.loggingCtx)( + s"Unexpected cloud context encountered during health checks" + ) >> F.pure(false) } // In addition to collecting aggregate metrics, log a warning for any app that is down. _ <- diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index 2ad50fb08a..b03a73dead 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -134,7 +134,6 @@ class GcpDependenciesBuilderSpec mock[Cache[IO, SamResourceCacheKey, (Option[String], Option[AppAccessScope])]], mock[OpenIDConnectConfiguration], mock[AppDAO[IO]], - mock[ListenerDAO[IO]], mock[AzureContainerService[IO]], mock[RuntimeServiceConfig], mock[KubernetesDnsCache[IO]], diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index f59c0af1ec..e80a900c32 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -73,11 +73,9 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test // Mocks val appDAO = setUpMockAppDAO - val samDAO = setUpMockSamDAO val jupyterDAO = setUpMockJupyterDAO val rstudioDAO = setUpMockRStudioDAO val welderDAO = setUpMockWelderDAO - val relayListenerDAO = setUpMockRelayListenerDAO val kube = setUpMockKubeDAO val containerService = setUpMockAzureContainerService @@ -89,8 +87,6 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test val leoMetricsMonitor = new LeoMetricsMonitor[IO]( config, appDAO, - relayListenerDAO, - samDAO, kube, containerService ) @@ -608,14 +604,6 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test welder } - private def setUpMockRelayListenerDAO: ListenerDAO[IO] = { - val listener = mock[ListenerDAO[IO]] - when { - listener.getStatus(any)(any) - } thenReturn IO.pure(true) - listener - } - private def setUpMockKubeDAO: KubernetesAlgebra[IO] = { val client = mock[CoreV1Api] val podList = mock[V1PodList] From 2dac410437344c1ecf94c24324f673b02bcb6336 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 15:25:41 -0400 Subject: [PATCH 19/43] Remove remaining Listener references and config --- .../workbench/leonardo/runtimeModels.scala | 1 - http/src/main/resources/reference.conf | 5 -- .../leonardo/http/ConfigReader.scala | 10 ---- .../http/service/LeoAppServiceInterp.scala | 5 +- .../monitor/LeoPubsubMessageSubscriber.scala | 2 +- .../leonardo/util/BuildHelmChartValues.scala | 59 +------------------ .../leonardo/http/ConfigReaderSpec.scala | 1 - .../util/BuildHelmChartValuesSpec.scala | 40 ------------- 8 files changed, 5 insertions(+), 118 deletions(-) diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala index ea900c578c..bb7b438e37 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala @@ -384,7 +384,6 @@ object RuntimeImageType extends Enum[RuntimeImageType] { case object Jupyter extends RuntimeImageType case object RStudio extends RuntimeImageType case object Welder extends RuntimeImageType - case object Listener extends RuntimeImageType // This is not strictly an image type. It can either be a custom VM image for dataproc, // or boot disk snapshot for GCE VMs case object BootSource extends RuntimeImageType diff --git a/http/src/main/resources/reference.conf b/http/src/main/resources/reference.conf index d0c597fe2b..d9b2d8e543 100644 --- a/http/src/main/resources/reference.conf +++ b/http/src/main/resources/reference.conf @@ -198,11 +198,6 @@ azure { # App types which are allowed to launch with WORKSPACE_SHARED access scope. allowed-shared-apps = [] - - listener-chart-config { - chart-name = "terra-helm/listener" - chart-version = "0.3.0" - } } dateAccessedUpdater { diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala index 83fef20ea6..9935529e7e 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReader.scala @@ -3,12 +3,10 @@ package http import _root_.pureconfig.generic.auto._ import org.broadinstitute.dsde.workbench.azure.AzureAppRegistrationConfig -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName import org.broadinstitute.dsde.workbench.leonardo.ConfigImplicits._ import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetricsMonitorConfig import org.broadinstitute.dsde.workbench.leonardo.util.TerraAppSetupChartConfig -import org.broadinstitute.dsp.{ChartName, ChartVersion} import org.http4s.Uri import pureconfig.ConfigSource @@ -22,7 +20,6 @@ final case class AzureConfig( appRegistration: AzureAppRegistrationConfig, allowedSharedApps: List[AppType], tdr: TdrConfig, - listenerChartConfig: ListenerChartConfig, hostingModeConfig: AzureHostingModeConfig ) @@ -35,13 +32,6 @@ final case class DrsConfig(url: String) final case class TdrConfig(url: String) -final case class ListenerChartConfig(chartName: ChartName, chartVersion: ChartVersion) { - def service = KubernetesService( - ServiceId(-1), - ServiceConfig(ServiceName("listener"), KubernetesServiceKindName("ClusterIP")) - ) -} - // Note: pureconfig supports reading kebab case into camel case in code by default // More docs see https://pureconfig.github.io/docs/index.html final case class AppConfig( diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala index 3edc875990..7b678ae178 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala @@ -1103,10 +1103,7 @@ final class LeoAppServiceInterp[F[_]: Parallel](config: AppServiceConfig, Release.apply ) )(app => app.release.asRight[Throwable]) - services = - if (cloudContext.cloudProvider == CloudProvider.Azure) { - gkeAppConfig.kubernetesServices.appended(ConfigReader.appConfig.azure.listenerChartConfig.service) - } else gkeAppConfig.kubernetesServices + services = gkeAppConfig.kubernetesServices numOfReplicas = if (req.appType == AppType.Allowed) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala index fcb50c6a7f..420e728445 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala @@ -1509,7 +1509,7 @@ class LeoPubsubMessageSubscriber[F[_]]( // This would provide more cases in which an app is left in a usable state // Note that an app can also emit this error if the liveness probe fails before an update is triggered, so rolling back may not have an effect case _: AppUpdatePollingException => appQuery.updateStatus(msg.appId, AppStatus.Error).transaction - // Fatal case, helm call failed for either listener or app charts + // Fatal case, helm call failed for app chart case _: HelmException => appQuery.updateStatus(msg.appId, AppStatus.Error).transaction // Non fatal catch-all case, set app status back to running but append whatever error occurred in db for traceability case _ => appQuery.updateStatus(msg.appId, AppStatus.Running).transaction diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala index dfdbcef13b..7fa725c8cf 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValues.scala @@ -1,21 +1,17 @@ package org.broadinstitute.dsde.workbench.leonardo package util -import org.broadinstitute.dsde.workbench.leonardo.Autopilot -import org.broadinstitute.dsde.workbench.azure.{PrimaryKey, RelayHybridConnectionName, RelayNamespace} import org.broadinstitute.dsde.workbench.google2.DiskName import org.broadinstitute.dsde.workbench.google2.GKEModels.NodepoolName import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.{NamespaceName, ServiceAccountName} import org.broadinstitute.dsde.workbench.leonardo.AppRestore.GalaxyRestore -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.AppSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.config.{AzureEnvironmentConverter, SamConfig} +import org.broadinstitute.dsde.workbench.leonardo.Autopilot import org.broadinstitute.dsde.workbench.leonardo.dao.CustomAppService -import org.broadinstitute.dsde.workbench.leonardo.http.{kubernetesProxyHost, ConfigReader} +import org.broadinstitute.dsde.workbench.leonardo.http.kubernetesProxyHost import org.broadinstitute.dsde.workbench.model.WorkbenchEmail import org.broadinstitute.dsde.workbench.model.google.GcsBucketName -import org.broadinstitute.dsp.{Release, Values} +import org.broadinstitute.dsp.Release -import java.net.URL import java.nio.charset.StandardCharsets private[leonardo] object BuildHelmChartValues { @@ -242,55 +238,6 @@ private[leonardo] object BuildHelmChartValues { ) ++ command ++ args ++ configs ++ ingress ++ nodepool).mkString(",") } - def buildListenerChartOverrideValuesString(release: Release, - samResourceId: AppSamResourceId, - relayNamespace: RelayNamespace, - relayHcName: RelayHybridConnectionName, - relayPrimaryKey: PrimaryKey, - appType: AppType, - workspaceId: WorkspaceId, - appName: AppName, - validHosts: Set[String], - samConfig: SamConfig, - listenerImage: String, - leoUrlBase: URL - ): Values = { - val relayTargetHost = appType match { - case AppType.Cromwell => s"http://coa-${release.asString}-reverse-proxy-service:8000/" - case _ => "unknown" - } - - // Some apps may serve requests on endpoints like /{appName}/batch and use relative redirects, - // requiring that we don't strip the entity path. For all current app types we do strip the entity path. - val removeEntityPathFromHttpUrl = true - - // validHosts can have a different number of hosts, this pre-processes the list as separate chart values - val validHostValues = validHosts.zipWithIndex.map { case (elem, idx) => - raw"connection.validHosts[$idx]=$elem" - } - - Values( - List( - raw"""connection.removeEntityPathFromHttpUrl="${removeEntityPathFromHttpUrl.toString}"""", - raw"connection.connectionString=Endpoint=sb://${relayNamespace.value}${AzureEnvironmentConverter.relaySuffixFromString( - ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment - )}/;SharedAccessKeyName=listener;SharedAccessKey=${relayPrimaryKey.value};EntityPath=${relayHcName.value}", - raw"connection.connectionName=${relayHcName.value}", - raw"connection.endpoint=https://${relayNamespace.value}${AzureEnvironmentConverter - .relaySuffixFromString(ConfigReader.appConfig.azure.hostingModeConfig.azureEnvironment)}", - raw"connection.targetHost=$relayTargetHost", - raw"sam.url=${samConfig.server}", - raw"sam.resourceId=${samResourceId.resourceId}", - raw"sam.resourceType=${samResourceId.resourceType.asString}", - raw"sam.action=connect", - raw"leonardo.url=${leoUrlBase}", - raw"general.workspaceId=${workspaceId.value.toString}", - raw"general.appName=${appName.value}", - raw"listener.image=${listenerImage}" - ).concat(validHostValues).mkString(",") - ) - } - def buildAllowedAppChartOverrideValuesString(config: GKEInterpreterConfig, allowedChartName: AllowedChartName, appName: AppName, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala index 108f78f128..c4d562b895 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala @@ -31,7 +31,6 @@ class ConfigReaderSpec extends AnyFlatSpec with Matchers { AzureAppRegistrationConfig(ClientId(""), ClientSecret(""), ManagedAppTenantId("")), List(), TdrConfig("https://jade.datarepo-dev.broadinstitute.org"), - ListenerChartConfig(ChartName("terra-helm/listener"), ChartVersion("0.3.0")), AzureHostingModeConfig( false, "AZURE", diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala index ca4bcbacba..6ea504a907 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/BuildHelmChartValuesSpec.scala @@ -1,14 +1,12 @@ package org.broadinstitute.dsde.workbench.leonardo package util -import org.broadinstitute.dsde.workbench.azure.{PrimaryKey, RelayHybridConnectionName, RelayNamespace} import org.broadinstitute.dsde.workbench.google2.DiskName import org.broadinstitute.dsde.workbench.google2.GKEModels.NodepoolName import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.{NamespaceName, ServiceAccountName} import org.broadinstitute.dsde.workbench.leonardo.AppRestore.GalaxyRestore import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{makePersistentDisk, userEmail, userEmail2} import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{makeCustomAppService, makeKubeCluster} -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.AppSamResourceId import org.broadinstitute.dsde.workbench.leonardo.config.Config import org.broadinstitute.dsde.workbench.leonardo.util.BuildHelmChartValues._ import org.broadinstitute.dsde.workbench.model.WorkbenchEmail @@ -16,9 +14,6 @@ import org.broadinstitute.dsde.workbench.model.google.GcsBucketName import org.broadinstitute.dsp.Release import org.scalatest.flatspec.AnyFlatSpecLike -import java.net.URL -import java.util.UUID - class BuildHelmChartValuesSpec extends AnyFlatSpecLike with LeonardoTestSuite { it should "build Galaxy override values string" in { @@ -542,39 +537,4 @@ class BuildHelmChartValuesSpec extends AnyFlatSpecLike with LeonardoTestSuite { """gcsfuse.enabled=true,""" + """gcsfuse.bucket=fc-bucket""".stripMargin } - - it should "build relay listener override values string" in { - val workspaceId = WorkspaceId(UUID.randomUUID) - val res = buildListenerChartOverrideValuesString( - Release("rl-rls"), - AppSamResourceId("sam-id", Some(AppAccessScope.WorkspaceShared)), - RelayNamespace("relay-ns"), - RelayHybridConnectionName("hc-name"), - PrimaryKey("hc-name"), - AppType.Cromwell, - workspaceId, - AppName("app1"), - Set("example.com", "foo.com", "bar.org"), - Config.samConfig, - "acr/listener:1", - new URL("https://leo.com") - ) - res.asString shouldBe - "connection.removeEntityPathFromHttpUrl=\"true\"," + - "connection.connectionString=Endpoint=sb://relay-ns.servicebus.windows.net/;SharedAccessKeyName=listener;SharedAccessKey=hc-name;EntityPath=hc-name," + - "connection.connectionName=hc-name," + - "connection.endpoint=https://relay-ns.servicebus.windows.net," + - "connection.targetHost=http://wds-rl-rls-wds-svc:8080," + - "sam.url=https://sam.test.org:443," + - "sam.resourceId=sam-id," + - "sam.resourceType=kubernetes-app-shared," + - "sam.action=connect," + - "leonardo.url=https://leo.com," + - s"general.workspaceId=${workspaceId.value.toString}," + - "general.appName=app1," + - "listener.image=acr/listener:1," + - "connection.validHosts[0]=example.com," + - "connection.validHosts[1]=foo.com," + - "connection.validHosts[2]=bar.org" - } } From 0ee47ecc37c03a777ac4f2f496556118b889bc94 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 15:41:14 -0400 Subject: [PATCH 20/43] Delete more Azure models --- .../dsde/workbench/leonardo/JsonCodec.scala | 61 ----- .../dsde/workbench/leonardo/azureModels.scala | 46 ---- .../dsde/workbench/leonardo/dao/WsmDao.scala | 231 ------------------ .../workbench/leonardo/CommonTestData.scala | 31 +-- .../leonardo/monitor/LeoPubsubCodecSpec.scala | 20 -- 5 files changed, 1 insertion(+), 388 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala index a287d6f969..f1e8c4fd05 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala @@ -83,8 +83,6 @@ object JsonCodec { implicit val runtimeSamResourceIdEncoder: Encoder[RuntimeSamResourceId] = Encoder.encodeString.contramap(_.resourceId) implicit val storageContainerNameEncoder: Encoder[org.broadinstitute.dsde.workbench.azure.ContainerName] = Encoder.encodeString.contramap(_.value) - implicit val storageAccountNameEncoder: Encoder[StorageAccountName] = - Encoder.encodeString.contramap(_.value) implicit val urlEncoder: Encoder[URL] = Encoder.encodeString.contramap(_.toString) implicit val zoneNameEncoder: Encoder[ZoneName] = Encoder.encodeString.contramap(_.value) implicit val regionNameEncoder: Encoder[RegionName] = Encoder.encodeString.contramap(_.value) @@ -306,7 +304,6 @@ object JsonCodec { implicit val networkNameEncoder: Encoder[NetworkName] = Encoder.encodeString.contramap(_.value) implicit val subNetworkNameEncoder: Encoder[SubnetworkName] = Encoder.encodeString.contramap(_.value) implicit val ipRangeEncoder: Encoder[IpRange] = Encoder.encodeString.contramap(_.value) - implicit val wsmJobIdEncoder: Encoder[WsmJobId] = Encoder.encodeString.contramap(_.value.toString) implicit val batchAccountNameDecoder: Decoder[BatchAccountName] = Decoder.decodeString.map(BatchAccountName) implicit val batchAccountNameEncoder: Encoder[BatchAccountName] = Encoder.encodeString.contramap(_.value) @@ -363,8 +360,6 @@ object JsonCodec { implicit val workbenchUserIdDecoder: Decoder[WorkbenchUserId] = Decoder.decodeString.map(WorkbenchUserId) implicit val storageContainerNameDecoder: Decoder[org.broadinstitute.dsde.workbench.azure.ContainerName] = Decoder.decodeString.map(org.broadinstitute.dsde.workbench.azure.ContainerName) - implicit val storageAccountNameDecoder: Decoder[StorageAccountName] = - Decoder.decodeString.map(StorageAccountName) implicit val pathDecoder: Decoder[Path] = Decoder.decodeString.map(s => Paths.get(s)) implicit val runtimeImageTypeDecoder: Decoder[RuntimeImageType] = Decoder.decodeString.emap(s => RuntimeImageType.stringToRuntimeImageType.get(s).toRight(s"invalid RuntimeImageType ${s}") @@ -699,10 +694,6 @@ object JsonCodec { implicit val chartNameDecoder: Decoder[ChartName] = Decoder.decodeString.map(ChartName) implicit val allowedChartNameDecoder: Decoder[AllowedChartName] = Decoder.decodeString.emap(x => AllowedChartName.stringToObject.get(x).toRight("chart name not allowed")) - implicit val aksClusterNameDecoder: Decoder[AKSClusterName] = Decoder.decodeString.map(AKSClusterName) - implicit val aksClusterDecoder: Decoder[AKSCluster] = Decoder.forProduct2("name", "tags")(AKSCluster) - implicit val postgresServerDecoder: Decoder[PostgresServer] = - Decoder.forProduct2("name", "pgBouncerEnabled")(PostgresServer) implicit val apiServerIpDecoder: Decoder[KubernetesApiServerIp] = Decoder.decodeString.map(KubernetesApiServerIp) implicit val networkNameDecoder: Decoder[NetworkName] = Decoder.decodeString.map(NetworkName) @@ -750,9 +741,6 @@ object JsonCodec { implicit val uuidDecoder: Decoder[UUID] = Decoder.decodeString.map(s => UUID.fromString(s)) - implicit val wsmJobIdDecoder: Decoder[WsmJobId] = - Decoder.decodeString.map(s => WsmJobId(s)) - implicit val workspaceIdEncoder: Encoder[WorkspaceId] = Encoder.encodeString.contramap(_.value.toString) @@ -772,11 +760,6 @@ object JsonCodec { implicit val azureMachineTypeEncoder: Encoder[VirtualMachineSizeTypes] = Encoder.encodeString.contramap(_.toString) implicit val azureDiskNameEncoder: Encoder[AzureDiskName] = Encoder.encodeString.contramap(_.value) implicit val relayNamespaceEncoder: Encoder[RelayNamespace] = Encoder.encodeString.contramap(_.value) - implicit val aksClusterNameEncoder: Encoder[AKSClusterName] = Encoder.encodeString.contramap(_.value) - implicit val aksClusterEncoder: Encoder[AKSCluster] = - Encoder.forProduct2("name", "tags")(x => (x.name, x.tags)) - implicit val postgresServerEncoder: Encoder[PostgresServer] = - Encoder.forProduct2("name", "pgBouncerEnabled")(x => (x.name, x.pgBouncerEnabled)) implicit val azureImageEncoder: Encoder[AzureImage] = Encoder.forProduct4( "publisher", @@ -785,50 +768,6 @@ object JsonCodec { "version" )(x => (x.publisher, x.offer, x.sku, x.version)) - implicit val landingZoneResourcesDecoder: Decoder[LandingZoneResources] = - Decoder.forProduct11( - "landingZoneId", - "clusterName", - "batchAccountName", - "relayNamespace", - "storageAccountName", - "vnetName", - "batchNodesSubnetName", - "aksSubnetName", - "region", - "applicationInsightsName", - "postgresName" - )( - LandingZoneResources.apply - ) - - implicit val landingZoneResourcesEncoder: Encoder[LandingZoneResources] = Encoder.forProduct11( - "landingZoneId", - "clusterName", - "batchAccountName", - "relayNamespace", - "storageAccountName", - "vnetName", - "batchNodesSubnetName", - "aksSubnetName", - "region", - "applicationInsightsName", - "postgresName" - )(x => - (x.landingZoneId, - x.aksCluster, - x.batchAccountName, - x.relayNamespace, - x.storageAccountName, - x.vnetName, - x.batchNodesSubnetName, - x.aksSubnetName, - x.region, - x.applicationInsightsName, - x.postgresServer - ) - ) - implicit val autodeleteThresholdEncoder: Encoder[AutodeleteThreshold] = Encoder.encodeInt.contramap(_.value) implicit val autodeleteThresholdDecoder: Decoder[AutodeleteThreshold] = Decoder.decodeInt.emap { diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala index 020fe483af..452b19004e 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala @@ -1,14 +1,5 @@ package org.broadinstitute.dsde.workbench.leonardo -import org.broadinstitute.dsde.workbench.azure.{ - AKSClusterName, - ApplicationInsightsName, - BatchAccountName, - RelayNamespace -} -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.{NamespaceName, ServiceAccountName} -import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} - import java.util.UUID final case class WsmControlledResourceId(value: UUID) extends AnyVal @@ -17,41 +8,4 @@ final case class AzureUnimplementedException(message: String) extends Exception override def getMessage: String = message } -final case class StorageAccountName(value: String) extends AnyVal - -final case class WsmJobId(value: String) extends AnyVal - -final case class ManagedIdentityName(value: String) extends AnyVal - -final case class BatchAccountKey(value: String) extends AnyVal - -final case class PostgresServer(name: String, pgBouncerEnabled: Boolean) - -final case class AKSCluster(name: String, tags: Map[String, Boolean]) { - def asClusterName: AKSClusterName = AKSClusterName(name) -} - -final case class WsmManagedAzureIdentity(wsmResourceName: String, managedIdentityName: String) - -final case class WsmControlledDatabaseResource(wsmDatabaseName: String, - azureDatabaseName: String, - controlledResourceId: UUID = null -) - -final case class WsmControlledKubernetesNamespaceResource(name: NamespaceName, - wsmResourceId: WsmControlledResourceId, - serviceAccountName: ServiceAccountName -) -final case class LandingZoneResources(landingZoneId: UUID, - aksCluster: AKSCluster, - batchAccountName: BatchAccountName, - relayNamespace: RelayNamespace, - storageAccountName: StorageAccountName, - vnetName: NetworkName, - batchNodesSubnetName: SubnetworkName, - aksSubnetName: SubnetworkName, - region: com.azure.core.management.Region, - applicationInsightsName: ApplicationInsightsName, - postgresServer: Option[PostgresServer] -) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala deleted file mode 100644 index aaeb51b9dd..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/dao/WsmDao.scala +++ /dev/null @@ -1,231 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo -package dao - -import _root_.io.circe._ -import ca.mrvisser.sealerate -import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.leonardo.JsonCodec.{googleProjectDecoder, storageContainerNameDecoder, wsmControlledResourceIdDecoder} -import org.broadinstitute.dsde.workbench.leonardo.dao.LandingZoneResourcePurpose.LandingZoneResourcePurpose -import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} - -import java.util.UUID - -/** - * This is the legacy WsmDAO. It remains because there is some specific logic around models retrieved from WSM - * It SHOULD NOT be added to. Favor usage of WsmClientProvider, the auto-generated client. - */ -final case class WorkspaceDescription(id: WorkspaceId, - displayName: String, - spendProfile: String, - azureContext: Option[AzureCloudContext], - gcpContext: Option[GoogleProject] -) - -//Landing Zone models -final case class LandingZone(landingZoneId: UUID, - billingProfileId: UUID, - definition: String, - version: String, - createdDate: String -) -final case class ListLandingZonesResult(landingzones: List[LandingZone]) - -// A LandingZoneResource will have either a resourceId or a resourceName + resourceParentId -final case class LandingZoneResource(resourceId: Option[String], - resourceType: String, - resourceName: Option[String], - resourceParentId: Option[String], - region: String, - tags: Option[Map[String, String]] -) - -object LandingZoneResourcePurpose extends Enumeration { - type LandingZoneResourcePurpose = Value - val SHARED_RESOURCE, WLZ_RESOURCE = Value - val WORKSPACE_COMPUTE_SUBNET, WORKSPACE_STORAGE_SUBNET, AKS_NODE_POOL_SUBNET, POSTGRESQL_SUBNET, POSTGRES_ADMIN, - WORKSPACE_BATCH_SUBNET = Value -} - -final case class LandingZoneResourcesByPurpose(purpose: LandingZoneResourcePurpose, - deployedResources: List[LandingZoneResource] -) -final case class ListLandingZoneResourcesResult(id: UUID, resources: List[LandingZoneResourcesByPurpose]) -final case class StorageContainerResponse(name: ContainerName, resourceId: WsmControlledResourceId) - -sealed trait ResourceAttributes extends Serializable with Product -object ResourceAttributes { - final case class StorageContainerResourceAttributes(name: ContainerName) extends ResourceAttributes -} - -final case class WsmResourceMetadata(resourceId: WsmControlledResourceId) -final case class WsmResource(metadata: WsmResourceMetadata, resourceAttributes: ResourceAttributes) -final case class GetWsmResourceResponse(resources: List[WsmResource]) - -// Azure Disk models -final case class PollDiskParams(workspaceId: WorkspaceId, - jobId: WsmJobId, - diskId: DiskId, - runtime: Runtime, - wsmResourceId: WsmControlledResourceId - ) - -final case class CreateDiskForRuntimeResult(resourceId: WsmControlledResourceId, pollParams: Option[PollDiskParams]) - -// Common Controlled resource models -final case class InternalDaoControlledResourceCommonFields(name: ControlledResourceName, - description: ControlledResourceDescription, - cloningInstructions: CloningInstructions, - accessScope: AccessScope, - managedBy: ManagedBy, - privateResourceUser: Option[PrivateResourceUser], - resourceId: Option[WsmControlledResourceId] -) - -final case class ControlledResourceName(value: String) extends AnyVal -final case class ControlledResourceDescription(value: String) extends AnyVal -final case class PrivateResourceUser(userName: WorkbenchEmail, privateResourceIamRoles: ControlledResourceIamRole) - -final case class WsmGcpContext(projectId: GoogleProject) - -sealed abstract class WsmJobStatus -object WsmJobStatus { - case object Running extends WsmJobStatus { - override def toString: String = "RUNNING" - } - case object Succeeded extends WsmJobStatus { - override def toString: String = "SUCCEEDED" - } - case object Failed extends WsmJobStatus { - override def toString: String = "FAILED" - } - - def values: Set[WsmJobStatus] = sealerate.values[WsmJobStatus] - - def stringToObject: Map[String, WsmJobStatus] = values.map(v => v.toString -> v).toMap -} - -sealed abstract class ControlledResourceIamRole -object ControlledResourceIamRole { - case object Reader extends ControlledResourceIamRole { - override def toString: String = "READER" - } - case object Writer extends ControlledResourceIamRole { - override def toString: String = "WRITER" - } - case object Editor extends ControlledResourceIamRole { - override def toString: String = "EDITOR" - } - - def values: Set[ControlledResourceIamRole] = sealerate.values[ControlledResourceIamRole] - - def stringToObject: Map[String, ControlledResourceIamRole] = values.map(v => v.toString -> v).toMap -} - -sealed abstract class CloningInstructions -object CloningInstructions { - case object Nothing extends CloningInstructions { - override def toString: String = "COPY_NOTHING" - } - case object Definition extends CloningInstructions { - override def toString: String = "COPY_DEFINITION" - } - case object Resource extends CloningInstructions { - override def toString: String = "COPY_RESOURCE" - } - case object Reference extends CloningInstructions { - override def toString: String = "COPY_REFERENCE" - } - - def values: Set[CloningInstructions] = sealerate.values[CloningInstructions] - - def stringToObject: Map[String, CloningInstructions] = values.map(v => v.toString -> v).toMap -} - -sealed abstract class AccessScope - -object AccessScope { - case object SharedAccess extends AccessScope { - override def toString: String = "SHARED_ACCESS" - } - - case object PrivateAccess extends AccessScope { - override def toString: String = "PRIVATE_ACCESS" - } - - def values: Set[AccessScope] = sealerate.values[AccessScope] - - def stringToObject: Map[String, AccessScope] = values.map(v => v.toString -> v).toMap -} - -sealed abstract class ManagedBy - -object ManagedBy { - case object User extends ManagedBy { - override def toString: String = "USER" - } - - case object Application extends ManagedBy { - override def toString: String = "APPLICATION" - } - - def values: Set[ManagedBy] = sealerate.values[ManagedBy] - - def stringToObject: Map[String, ManagedBy] = values.map(v => v.toString -> v).toMap -} -// End Common Controlled resource models - -object WsmDecoders { - - implicit val azureContextDecoder: Decoder[AzureCloudContext] = Decoder.instance { c => - for { - tenantId <- c.downField("tenantId").as[String] - subscriptionId <- c.downField("subscriptionId").as[String] - resourceGroupId <- c.downField("resourceGroupId").as[String] - } yield AzureCloudContext(TenantId(tenantId), - SubscriptionId(subscriptionId), - ManagedResourceGroupName(resourceGroupId) - ) - } - - implicit val landingZoneDecoder: Decoder[LandingZone] = - Decoder.forProduct5("landingZoneId", "billingProfileId", "definition", "version", "createdDate")(LandingZone.apply) - implicit val listLandingZonesResultDecoder: Decoder[ListLandingZonesResult] = - Decoder.forProduct1("landingzones")(ListLandingZonesResult.apply) - - implicit val landingZoneResourceDecoder: Decoder[LandingZoneResource] = - Decoder.forProduct6("resourceId", "resourceType", "resourceName", "resourceParentId", "region", "tags")( - LandingZoneResource.apply - ) - - implicit val landingZoneResourcePurposeDecoder: Decoder[LandingZoneResourcePurpose] = - Decoder.decodeString.emap(s => - LandingZoneResourcePurpose.values.find(_.toString == s).toRight(s"Invalid LandingZoneResourcePurpose found: ${s}") - ) - implicit val landingZoneResourcesByPurposeDecoder: Decoder[LandingZoneResourcesByPurpose] = - Decoder.forProduct2("purpose", "deployedResources")(LandingZoneResourcesByPurpose.apply) - implicit val listLandingZoneResourcesResultDecoder: Decoder[ListLandingZoneResourcesResult] = - Decoder.forProduct2("id", "resources")(ListLandingZoneResourcesResult.apply) - - implicit val wsmGcpContextDecoder: Decoder[WsmGcpContext] = - Decoder.forProduct1("gcpContext")(WsmGcpContext.apply) - - implicit val wsmJobStatusDecoder: Decoder[WsmJobStatus] = - Decoder.decodeString.emap(s => WsmJobStatus.stringToObject.get(s).toRight(s"Invalid WsmJobStatus found: $s")) - - implicit val storageContainerResourceAttributesDecoder - : Decoder[ResourceAttributes.StorageContainerResourceAttributes] = - Decoder.forProduct1("storageContainerName")(ResourceAttributes.StorageContainerResourceAttributes.apply) - implicit val resourceAttributesDecoder: Decoder[ResourceAttributes] = - Decoder.instance { x => - x.downField("azureStorageContainer").as[ResourceAttributes.StorageContainerResourceAttributes] - } - implicit val wsmResourceMetadataDecoder: Decoder[WsmResourceMetadata] = - Decoder.forProduct1("resourceId")(WsmResourceMetadata.apply) - implicit val wsmResourceeDecoder: Decoder[WsmResource] = - Decoder.forProduct2("metadata", "resourceAttributes")(WsmResource.apply) - implicit val getRelayNamespaceDecoder: Decoder[GetWsmResourceResponse] = - Decoder.forProduct1("resources")(GetWsmResourceResponse.apply) -} - -final case class WsmException(traceId: TraceId, message: String) extends Exception(message) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index 60e518d753..7165da91e7 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -21,7 +21,7 @@ import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{BootSource, import org.broadinstitute.dsde.workbench.leonardo.SamResourceId._ import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config._ -import org.broadinstitute.dsde.workbench.leonardo.dao.{AccessScope, CloningInstructions, ControlledResourceDescription, ControlledResourceIamRole, ControlledResourceName, InternalDaoControlledResourceCommonFields, ManagedBy, MockSamDAO, PrivateResourceUser} +import org.broadinstitute.dsde.workbench.leonardo.dao.MockSamDAO import org.broadinstitute.dsde.workbench.leonardo.db.ClusterRecord import org.broadinstitute.dsde.workbench.leonardo.http.{CreateRuntimeRequest, RuntimeConfigRequest, userScriptStartupOutputUriMetadataKey} import org.broadinstitute.dsde.workbench.model._ @@ -504,21 +504,6 @@ object CommonTestData { ) .gcpContext(new GcpContext().projectId("googleProject")) - val testCommonControlledResourceFields = InternalDaoControlledResourceCommonFields( - ControlledResourceName("name"), - ControlledResourceDescription("desc"), - CloningInstructions.Nothing, - AccessScope.PrivateAccess, - ManagedBy.User, - Some( - PrivateResourceUser( - userEmail, - ControlledResourceIamRole.Editor - ) - ), - None - ) - val defaultCreateAzureRuntimeReq = CreateAzureRuntimeRequest( Map.empty, VirtualMachineSizeTypes.STANDARD_A1, @@ -532,20 +517,6 @@ object CommonTestData { Some(0) ) - val landingZoneResources = LandingZoneResources( - UUID.randomUUID(), - AKSCluster("lzcluster", Map.empty[String, Boolean]), - BatchAccountName("lzbatch"), - RelayNamespace("lznamespace"), - StorageAccountName("lzstorage"), - NetworkName("lzvnet"), - SubnetworkName("batchsub"), - SubnetworkName("akssub"), - azureRegion, - ApplicationInsightsName("lzappinsights"), - Some(PostgresServer("postgres", false)) - ) - def modifyInstance(instance: DataprocInstance): DataprocInstance = instance.copy(key = modifyInstanceKey(instance.key), googleId = instance.googleId + 1) def modifyInstanceKey(instanceKey: DataprocInstanceKey): DataprocInstanceKey = diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala index accd973c88..5e7b4066fc 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala @@ -121,24 +121,4 @@ class LeoPubsubCodecSpec extends AnyFlatSpec with Matchers { res shouldBe Right(originalMessage) } - - val landingZoneResources = LandingZoneResources( - UUID.randomUUID(), - AKSCluster("cluster-name", Map.empty[String, Boolean]), - BatchAccountName("batch-account"), - RelayNamespace("relay-ns"), - StorageAccountName("storage-account"), - NetworkName("vnet"), - SubnetworkName("batch-subnet"), - SubnetworkName("aks-subnet"), - com.azure.core.management.Region.US_EAST, - ApplicationInsightsName("lzappinsights"), - Some(PostgresServer("postgres", false)) - ) - - it should "encode/decode LandingZoneResources properly" in { - val res = decode[LandingZoneResources](landingZoneResources.asJson.printWith(Printer.noSpaces)) - - res shouldBe Right(landingZoneResources) - } } From c176698e60d552b553bb7418c6a2f01122f30483 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 16:15:54 -0400 Subject: [PATCH 21/43] Remove Azure VM scripts --- .../init-resources/azure_vm_init_script.sh | 314 ------------------ .../init-resources/jupyter_server_config.py | 11 - 2 files changed, 325 deletions(-) delete mode 100644 http/src/main/resources/init-resources/azure_vm_init_script.sh delete mode 100644 http/src/main/resources/init-resources/jupyter_server_config.py diff --git a/http/src/main/resources/init-resources/azure_vm_init_script.sh b/http/src/main/resources/init-resources/azure_vm_init_script.sh deleted file mode 100644 index 0b6c3851bc..0000000000 --- a/http/src/main/resources/init-resources/azure_vm_init_script.sh +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env bash -set -e -# Log output is saved at /var/log/azure_vm_init_script.log - -# If you update this file, please update azure.custom-script-extension.file-uris in reference.conf so that Leonardo can adopt the new script - -# This is to avoid the error Ref BioC -# 'debconf: unable to initialize frontend: Dialog' -export DEBIAN_FRONTEND=noninteractive - -#create user to run jupyter -VM_JUP_USER=jupyter - -sudo useradd -m -c "Jupyter User" $VM_JUP_USER -sudo usermod -a -G $VM_JUP_USER,adm,dialout,cdrom,floppy,audio,dip,video,plugdev,lxd,netdev $VM_JUP_USER - -## Change ownership for the new user - -sudo chgrp $VM_JUP_USER /anaconda/bin/* - -sudo chown $VM_JUP_USER /anaconda/bin/* - -sudo chgrp $VM_JUP_USER /anaconda/envs/py38_default/bin/* - -sudo chown $VM_JUP_USER /anaconda/envs/py38_default/bin/* - -sudo systemctl disable --now jupyterhub.service - - -# Formatting and mounting persistent disk -WORK_DIRECTORY="/home/$VM_JUP_USER/persistent_disk" -## Create the PD working directory -mkdir -p ${WORK_DIRECTORY} - -## The PD should be the only `sd` disk that is not mounted yet -AllsdDisks=($(lsblk --nodeps --noheadings --output NAME --paths | grep -i "sd")) -FreesdDisks=() -for Disk in "${AllsdDisks[@]}"; do - Mounts="$(lsblk -no MOUNTPOINT "${Disk}")" - if [ -z "$Mounts" ]; then - echo "Found our unmounted persistent disk!" - FreesdDisks="${Disk}" - else - echo "Not our persistent disk!" - fi -done -DISK_DEVICE_PATH=${FreesdDisks} - -## Only format disk is it hasn't already been formatted -## It the disk has previously been in use, then it should have a partition that we can mount -EXIT_CODE=0 -lsblk -no NAME --paths "${DISK_DEVICE_PATH}1" || EXIT_CODE=$? -if [ $EXIT_CODE -eq 0 ]; then - ## From https://learn.microsoft.com/en-us/azure/virtual-machines/linux/attach-disk-portal?tabs=ubuntu - ## Use the partprobe utility to make sure the kernel is aware of the new partition and filesystem. - ## Failure to use partprobe can cause the blkid or lslbk commands to not return the UUID for the new filesystem immediately. - sudo partprobe "${DISK_DEVICE_PATH}1" - # There is a pre-existing partition that we should try to directly mount - sudo mount -t ext4 "${DISK_DEVICE_PATH}1" ${WORK_DIRECTORY} - echo "Existing PD successfully remounted" -else - ## Create one partition on the PD - ( - echo o #create a new empty DOS partition table - echo n #add a new partition - echo p #print the partition table - echo - echo - echo - echo w #write table to disk and exit - ) | sudo fdisk ${DISK_DEVICE_PATH} - echo "successful partitioning" - ## Format the partition - # It's likely that the persistent disk was previously mounted on another VM and wasn't properly unmounted - # Passing -F -F to mkfs ext4 forces the tool to ignore the state of the partition. - # Note that there should be two instances command-line switch (-F -F) to override this check - echo y | sudo mkfs.ext4 "${DISK_DEVICE_PATH}1" -F -F - echo "successful formatting" - ## From https://learn.microsoft.com/en-us/azure/virtual-machines/linux/attach-disk-portal?tabs=ubuntu - ## Use the partprobe utility to make sure the kernel is aware of the new partition and filesystem. - ## Failure to use partprobe can cause the blkid or lslbk commands to not return the UUID for the new filesystem immediately. - sudo partprobe "${DISK_DEVICE_PATH}1" - ## Mount the PD partition to the working directory - sudo mount -t ext4 "${DISK_DEVICE_PATH}1" ${WORK_DIRECTORY} - echo "successful mount" -fi - -## Add the PD UUID to fstab to ensure that the drive is remounted automatically after a reboot -OUTPUT="$(lsblk -no UUID --paths "${DISK_DEVICE_PATH}1")" -echo "UUID="$OUTPUT" ${WORK_DIRECTORY} ext4 defaults 0 1" | sudo tee -a /etc/fstab -echo "successful write of PD UUID to fstab" - -## Change ownership of the mounted drive to the user -sudo chown -R $VM_JUP_USER:$VM_JUP_USER ${WORK_DIRECTORY} - - -# Read script arguments -echo $# arguments -if [ $# -ne 13 ]; - then echo "illegal number of parameters" -fi - -RELAY_NAME=$1 -RELAY_CONNECTION_NAME=$2 -RELAY_TARGET_HOST=$3 -RELAY_CONNECTION_POLICY_KEY=$4 -LISTENER_DOCKER_IMAGE=$5 -SAMURL=$6 -SAMRESOURCEID=$7 -CONTENTSECURITYPOLICY_FILE=$8 - -RELAY_SUFFIX=${21:-".servicebus.windows.net"} -AZURE_MANAGEMENT_URL="${22:-"https://management.azure.com/"}" - -# Envs for welder -WELDER_WSM_URL=${9:-localhost} -WORKSPACE_ID="${10:-dummy}" # Additionally used for welder -WORKSPACE_STORAGE_CONTAINER_ID="${11:-dummy}" # Additionally used for welder -WELDER_WELDER_DOCKER_IMAGE="${12:-dummy}" -WELDER_OWNER_EMAIL="${13:-dummy}" -WELDER_STAGING_BUCKET="${14:-dummy}" -WELDER_STAGING_STORAGE_CONTAINER_RESOURCE_ID="${15:-dummy}" - -# Envs for Jupyter -WORKSPACE_NAME="${16:-dummy}" -WORKSPACE_STORAGE_CONTAINER_URL="${17:-dummy}" - -# Jupyter variables for listener -SERVER_APP_BASE_URL="/${RELAY_CONNECTION_NAME}/" -SERVER_APP_ALLOW_ORIGIN="*" -HCVAR='\$hc' -SERVER_APP_WEBSOCKET_URL="wss://${RELAY_NAME}${RELAY_SUFFIX}/${HCVAR}/${RELAY_CONNECTION_NAME}" -SERVER_APP_WEBSOCKET_HOST="${RELAY_NAME}${RELAY_SUFFIX}" - -# Relay listener configuration -RELAY_CONNECTIONSTRING="Endpoint=sb://${RELAY_NAME}${RELAY_SUFFIX}/;SharedAccessKeyName=listener;SharedAccessKey=${RELAY_CONNECTION_POLICY_KEY};EntityPath=${RELAY_CONNECTION_NAME}" - -# Relay listener configuration - setDateAccessed listener -LEONARDO_URL="${18:-dummy}" -RUNTIME_NAME="${19:-dummy}" -VALID_HOSTS="${20:-dummy}" -DATEACCESSED_SLEEP_SECONDS=60 # supercedes default defined in terra-azure-relay-listeners/service/src/main/resources/application.yml - -# Log in script output for debugging purposes. -echo "RELAY_NAME = ${RELAY_NAME}" -echo "RELAY_CONNECTION_NAME = ${RELAY_CONNECTION_NAME}" -echo "RELAY_TARGET_HOST = ${RELAY_TARGET_HOST}" -echo "RELAY_CONNECTION_POLICY_KEY = ${RELAY_CONNECTION_POLICY_KEY}" -echo "RELAY_SUFFIX = ${RELAY_SUFFIX}" -echo "LISTENER_DOCKER_IMAGE = ${LISTENER_DOCKER_IMAGE}" -echo "SAMURL = ${SAMURL}" -echo "SAMRESOURCEID = ${SAMRESOURCEID}" -echo "CONTENTSECURITYPOLICY_FILE = ${CONTENTSECURITYPOLICY_FILE}" -echo "WELDER_WSM_URL = ${WELDER_WSM_URL}" -echo "WORKSPACE_ID = ${WORKSPACE_ID}" -echo "WORKSPACE_STORAGE_CONTAINER_ID = ${WORKSPACE_STORAGE_CONTAINER_ID}" -echo "WELDER_WELDER_DOCKER_IMAGE = ${WELDER_WELDER_DOCKER_IMAGE}" -echo "WELDER_OWNER_EMAIL = ${WELDER_OWNER_EMAIL}" -echo "WELDER_STAGING_BUCKET = ${WELDER_STAGING_BUCKET}" -echo "WELDER_STAGING_STORAGE_CONTAINER_RESOURCE_ID = ${WELDER_STAGING_STORAGE_CONTAINER_RESOURCE_ID}" -echo "WORKSPACE_NAME = ${WORKSPACE_NAME}" -echo "WORKSPACE_STORAGE_CONTAINER_URL = ${WORKSPACE_STORAGE_CONTAINER_URL}" -echo "SERVER_APP_BASE_URL = ${SERVER_APP_BASE_URL}" -echo "SERVER_APP_ALLOW_ORIGIN = ${SERVER_APP_ALLOW_ORIGIN}" -echo "SERVER_APP_WEBSOCKET_URL = ${SERVER_APP_WEBSOCKET_URL}" -echo "RELAY_CONNECTIONSTRING = ${RELAY_CONNECTIONSTRING}" -echo "LEONARDO_URL = ${LEONARDO_URL}" -echo "RUNTIME_NAME = ${RUNTIME_NAME}" -echo "VALID_HOSTS = ${VALID_HOSTS}" - -# Wait for lock to resolve before any installs, to resolve this error: https://broadworkbench.atlassian.net/browse/IA-4645 - -while sudo fuser /var/lib/dpkg/lock-frontend > /dev/null 2>&1 - do - echo "Waiting to get lock /var/lib/dpkg/lock-frontend..." - sleep 5 - done - -# Install updated R version -echo "Installing R" -# Update package list -sudo apt-get update -# Install most recent R version -sudo apt-get install --no-install-recommends -y r-base - -#Update kernel list - -echo "Y"| /anaconda/bin/jupyter kernelspec remove sparkkernel - -echo "Y"| /anaconda/bin/jupyter kernelspec remove sparkrkernel - -echo "Y"| /anaconda/bin/jupyter kernelspec remove pysparkkernel - -echo "Y"| /anaconda/bin/jupyter kernelspec remove spark-3-python - -#echo "Y"| /anaconda/bin/jupyter kernelspec remove julia-1.6 - -echo "Y"| /anaconda/envs/py38_default/bin/pip3 install ipykernel pydevd - -echo "Y"| /anaconda/envs/py38_default/bin/python3 -m ipykernel install - -# Start Jupyter server with custom parameters -sudo runuser -l $VM_JUP_USER -c "mkdir -p /home/$VM_JUP_USER/.jupyter" -sudo runuser -l $VM_JUP_USER -c "wget -qP /home/$VM_JUP_USER/.jupyter https://raw.githubusercontent.com/DataBiosphere/leonardo/ea519ef899de28e27e2a37ba368433da9fd03b7f/http/src/main/resources/init-resources/jupyter_server_config.py" -# We pull the jupyter_delocalize.py file from the base terra-docker python image, but it was designed for notebooks and we need to make a couple of changes to make it work with server instead -sudo runuser -l $VM_JUP_USER -c "wget -qP /anaconda/lib/python3.10/site-packages https://raw.githubusercontent.com/DataBiosphere/terra-docker/0ea6d2ebd7fcae7072e01e1c2f2d178390a276b0/terra-jupyter-base/custom/jupyter_delocalize.py" -sudo runuser -l $VM_JUP_USER -c "sed -i 's/notebook.services/jupyter_server.services/g' /anaconda/lib/python3.10/site-packages/jupyter_delocalize.py" -sudo runuser -l $VM_JUP_USER -c "sed -i 's/http:\/\/welder:8080/http:\/\/127.0.0.1:8081/g' /anaconda/lib/python3.10/site-packages/jupyter_delocalize.py" - -echo "------ Jupyter ------" -echo "Starting Jupyter with command..." - -echo "sudo runuser -l $VM_JUP_USER -c \"/anaconda/bin/jupyter server --ServerApp.base_url=$SERVER_APP_BASE_URL --ServerApp.websocket_url=$SERVER_APP_WEBSOCKET_URL --ServerApp.contents_manager_class=jupyter_delocalize.WelderContentsManager --autoreload &> /home/$VM_JUP_USER/jupyter.log\"" >/dev/null 2>&1& - -sudo runuser -l $VM_JUP_USER -c "/anaconda/bin/jupyter server --ServerApp.base_url=$SERVER_APP_BASE_URL --ServerApp.websocket_url=$SERVER_APP_WEBSOCKET_URL --ServerApp.contents_manager_class=jupyter_delocalize.WelderContentsManager --autoreload &> /home/$VM_JUP_USER/jupyter.log" >/dev/null 2>&1& - -# Store Jupyter Server parameters for reboot processes -sudo crontab -l 2>/dev/null| cat - <(echo "@reboot sudo runuser -l $VM_JUP_USER -c '/anaconda/bin/jupyter server --ServerApp.base_url=$SERVER_APP_BASE_URL --ServerApp.websocket_url=$SERVER_APP_WEBSOCKET_URL --ServerApp.contents_manager_class=jupyter_delocalize.WelderContentsManager --autoreload &> /home/$VM_JUP_USER/jupyter.log' >/dev/null 2>&1&") | crontab - - -echo "------ Listener version: ${LISTENER_DOCKER_IMAGE} ------" -echo " Starting listener with command..." - -echo "docker run -d --restart always --network host --name listener \ --e LISTENER_RELAYCONNECTIONSTRING=\"$RELAY_CONNECTIONSTRING\" \ --e LISTENER_RELAYCONNECTIONNAME=\"$RELAY_CONNECTION_NAME\" \ --e LISTENER_REQUESTINSPECTORS_0=\"samChecker\" \ --e LISTENER_REQUESTINSPECTORS_1=\"setDateAccessed\" \ --e LISTENER_SAMINSPECTORPROPERTIES_SAMRESOURCEID=\"$SAMRESOURCEID\" \ --e LISTENER_SAMINSPECTORPROPERTIES_SAMURL=\"$SAMURL\" \ --e LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_SERVICEHOST=\"$LEONARDO_URL\" \ --e LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_WORKSPACEID=\"$WORKSPACE_ID\" \ --e LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_CALLWINDOWINSECONDS=\"$DATEACCESSED_SLEEP_SECONDS\" \ --e LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_RUNTIMENAME=\"$RUNTIME_NAME\" \ --e LISTENER_CORSSUPPORTPROPERTIES_CONTENTSECURITYPOLICY=\"$(cat $CONTENTSECURITYPOLICY_FILE)\" \ --e LISTENER_CORSSUPPORTPROPERTIES_VALIDHOSTS=\"${VALID_HOSTS},${SERVER_APP_WEBSOCKET_HOST}\" \ --e LISTENER_TARGETPROPERTIES_TARGETHOST=\"http://$RELAY_TARGET_HOST:8888\" \ --e LISTENER_TARGETPROPERTIES_TARGETROUTINGRULES_0_PATHCONTAINS=welder \ --e LISTENER_TARGETPROPERTIES_TARGETROUTINGRULES_0_TARGETHOST=http://$RELAY_TARGET_HOST:8081 \ --e LISTENER_TARGETPROPERTIES_TARGETROUTINGRULES_0_REMOVEFROMPATH=\"\$hc-name/welder\" \ --e LOGGING_LEVEL_ROOT=INFO \ -$LISTENER_DOCKER_IMAGE" - -#Run docker container with Relay Listener -docker run -d --restart always --network host --name listener \ ---env LISTENER_RELAYCONNECTIONSTRING=$RELAY_CONNECTIONSTRING \ ---env LISTENER_RELAYCONNECTIONNAME=$RELAY_CONNECTION_NAME \ ---env LISTENER_REQUESTINSPECTORS_0=samChecker \ ---env LISTENER_REQUESTINSPECTORS_1=setDateAccessed \ ---env LISTENER_SAMINSPECTORPROPERTIES_SAMRESOURCEID=$SAMRESOURCEID \ ---env LISTENER_SAMINSPECTORPROPERTIES_SAMURL=$SAMURL \ ---env LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_SERVICEHOST=$LEONARDO_URL \ ---env LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_WORKSPACEID=$WORKSPACE_ID \ ---env LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_CALLWINDOWINSECONDS=$DATEACCESSED_SLEEP_SECONDS \ ---env LISTENER_SETDATEACCESSEDINSPECTORPROPERTIES_RUNTIMENAME=$RUNTIME_NAME \ ---env LISTENER_CORSSUPPORTPROPERTIES_CONTENTSECURITYPOLICY="$(cat $CONTENTSECURITYPOLICY_FILE)" \ ---env LISTENER_CORSSUPPORTPROPERTIES_VALIDHOSTS="${VALID_HOSTS},${SERVER_APP_WEBSOCKET_HOST}" \ ---env LISTENER_TARGETPROPERTIES_TARGETHOST="http://${RELAY_TARGET_HOST}:8888" \ ---env LISTENER_TARGETPROPERTIES_TARGETROUTINGRULES_0_PATHCONTAINS="welder" \ ---env LISTENER_TARGETPROPERTIES_TARGETROUTINGRULES_0_TARGETHOST="http://${RELAY_TARGET_HOST}:8081" \ ---env LISTENER_TARGETPROPERTIES_TARGETROUTINGRULES_0_REMOVEFROMPATH="\$hc-name/welder" \ ---env LOGGING_LEVEL_ROOT=INFO \ -$LISTENER_DOCKER_IMAGE - -echo "------ Listener done ------" - -echo "------ Welder version: ${WELDER_WELDER_DOCKER_IMAGE} ------" -echo " Starting Welder with command...." - -echo "docker run -d --restart always --network host --name welder \ - --volume \"/home/${VM_JUP_USER}\":\"/work\" \ - -e WSM_URL=$WELDER_WSM_URL \ - -e PORT=8081 \ - -e WORKSPACE_ID=$WORKSPACE_ID \ - -e STORAGE_CONTAINER_RESOURCE_ID=$WORKSPACE_STORAGE_CONTAINER_ID \ - -e STAGING_STORAGE_CONTAINER_RESOURCE_ID=$WELDER_STAGING_STORAGE_CONTAINER_RESOURCE_ID \ - -e OWNER_EMAIL=\"$WELDER_OWNER_EMAIL\" \ - -e CLOUD_PROVIDER=\"azure\" \ - -e LOCKING_ENABLED=false \ - -e STAGING_BUCKET=\"$WELDER_STAGING_BUCKET\" \ - -e SHOULD_BACKGROUND_SYNC=\"false\" \ - -e AZURE_MANAGEMENT_URL=$AZURE_MANAGEMENT_URL \ - $WELDER_WELDER_DOCKER_IMAGE" - -docker run -d --restart always --network host --name welder \ ---volume "/home/${VM_JUP_USER}":"/work" \ ---env WSM_URL=$WELDER_WSM_URL \ ---env PORT=8081 \ ---env WORKSPACE_ID=$WORKSPACE_ID \ ---env STORAGE_CONTAINER_RESOURCE_ID=$WORKSPACE_STORAGE_CONTAINER_ID \ ---env STAGING_STORAGE_CONTAINER_RESOURCE_ID=$WELDER_STAGING_STORAGE_CONTAINER_RESOURCE_ID \ ---env OWNER_EMAIL=$WELDER_OWNER_EMAIL \ ---env CLOUD_PROVIDER="azure" \ ---env LOCKING_ENABLED=false \ ---env STAGING_BUCKET=$WELDER_STAGING_BUCKET \ ---env SHOULD_BACKGROUND_SYNC="false" \ ---env AZURE_MANAGEMENT_URL=$AZURE_MANAGEMENT_URL \ -$WELDER_WELDER_DOCKER_IMAGE - -echo "------ Welder done ------" - -# This next command creates a json file which contains the "env" variables to be added to the kernel.json files. -jq --null-input \ ---arg workspace_id "${WORKSPACE_ID}" \ ---arg workspace_storage_container_id "${WORKSPACE_STORAGE_CONTAINER_ID}" \ ---arg workspace_name "${WORKSPACE_NAME}" \ ---arg workspace_storage_container_url "${WORKSPACE_STORAGE_CONTAINER_URL}" \ -'{ "env": { "WORKSPACE_ID": $workspace_id, "WORKSPACE_STORAGE_CONTAINER_ID": $workspace_storage_container_id, "WORKSPACE_NAME": $workspace_name, "WORKSPACE_STORAGE_CONTAINER_URL": $workspace_storage_container_url }}' \ -> wsenv.json - -# This next commands iterate through the available kernels, and uses jq to include the env variables from the previous step -/anaconda/bin/jupyter kernelspec list | awk 'NR>1 {print $2}' | while read line; do jq -s add $line"/kernel.json" wsenv.json > tmpkernel.json && mv tmpkernel.json $line"/kernel.json"; done -/anaconda/envs/py38_default/bin/jupyter kernelspec list | awk 'NR>1 {print $2}' | while read line; do jq -s add $line"/kernel.json" wsenv.json > tmpkernel.json && mv tmpkernel.json $line"/kernel.json"; done -/anaconda/envs/azureml_py38/bin/jupyter kernelspec list | awk 'NR>1 {print $2}' | while read line; do jq -s add $line"/kernel.json" wsenv.json > tmpkernel.json && mv tmpkernel.json $line"/kernel.json"; done -/anaconda/envs/azureml_py38_PT_and_TF/bin/jupyter kernelspec list | awk 'NR>1 {print $2}' | while read line; do jq -s add $line"/kernel.json" wsenv.json > tmpkernel.json && mv tmpkernel.json $line"/kernel.json"; done diff --git a/http/src/main/resources/init-resources/jupyter_server_config.py b/http/src/main/resources/init-resources/jupyter_server_config.py deleted file mode 100644 index cca0ae367c..0000000000 --- a/http/src/main/resources/init-resources/jupyter_server_config.py +++ /dev/null @@ -1,11 +0,0 @@ -# Jupyter server config file for Azure VMs only! -# Need to update the link in the azure_vm_init_script.sh with your commit hash to update this config -c.ServerApp.quit_button=False -c.ServerApp.certfile='' -c.ServerApp.keyfile='' -c.ServerApp.port=8888 -c.ServerApp.token='' -c.ServerApp.ip='' -c.ServerApp.allow_origin="*" -c.ServerApp.disable_check_xsrf=True # to prevent 'xsrf missing from POST' error https://broadworkbench.atlassian.net/browse/IA-4284 -# c.ServerApp.contents_manager_class=jupyter_delocalize.WelderContentsManager From f600fd844068212f77fe22c64440ef59a41f698b Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 16:27:09 -0400 Subject: [PATCH 22/43] Clean up LeoMetricsMonitor and related code (tests TBD) --- .../dsde/workbench/leonardo/JsonCodec.scala | 1 - .../workbench/leonardo/runtimeModels.scala | 4 +- http/src/main/resources/reference.conf | 4 - .../http/AppDependenciesBuilder.scala | 7 +- .../http/BaselineDependenciesBuilder.scala | 6 - .../leonardo/monitor/LeoMetricsMonitor.scala | 219 +-- .../leonardo/util/KubernetesAlgebra.scala | 36 - .../leonardo/util/KubernetesInterpreter.scala | 183 --- .../leonardo/http/ConfigReaderSpec.scala | 2 +- .../http/GcpDependenciesBuilderSpec.scala | 1 - .../monitor/LeoMetricsMonitorSpec.scala | 1300 ++++++++--------- 11 files changed, 664 insertions(+), 1099 deletions(-) delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesAlgebra.scala delete mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesInterpreter.scala diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala index f1e8c4fd05..776051f684 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala @@ -6,7 +6,6 @@ import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes import io.circe.syntax._ import io.circe.{Decoder, DecodingFailure, Encoder, Json} import org.broadinstitute.dsde.workbench.azure.{ - AKSClusterName, ApplicationInsightsName, AzureCloudContext, BatchAccountName, diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala index bb7b438e37..adde889fbe 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/runtimeModels.scala @@ -390,8 +390,6 @@ object RuntimeImageType extends Enum[RuntimeImageType] { case object Proxy extends RuntimeImageType case object CryptoDetector extends RuntimeImageType - case object Azure extends RuntimeImageType - def stringToRuntimeImageType: Map[String, RuntimeImageType] = values.map(c => c.toString -> c).toMap } @@ -403,7 +401,7 @@ sealed trait RuntimeContainerServiceType extends EnumEntry with Serializable wit object RuntimeContainerServiceType extends Enum[RuntimeContainerServiceType] { val values = findValues val imageTypeToRuntimeContainerServiceType: Map[RuntimeImageType, RuntimeContainerServiceType] = - values.toList.map(v => v.imageType -> v).toMap ++ Map(RuntimeImageType.Azure -> JupyterService) + values.toList.map(v => v.imageType -> v).toMap case object JupyterService extends RuntimeContainerServiceType { override def imageType: RuntimeImageType = Jupyter override def proxySegment: String = "jupyter" diff --git a/http/src/main/resources/reference.conf b/http/src/main/resources/reference.conf index d9b2d8e543..06b796c21d 100644 --- a/http/src/main/resources/reference.conf +++ b/http/src/main/resources/reference.conf @@ -1058,8 +1058,4 @@ drs { metrics { enabled = true check-interval = 5 minutes - # If true, will include the AzureCloudContext as a metric tag for Azure runtimes/apps. - # Normally it's best to avoid tags for high-cardinality things (like workspaceId). - # But MRGs are fairly low cardinality, and useful to incude for public preview launch. - include-azure-cloud-context = true } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 1a6735af60..5a3ba49902 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -134,15 +134,10 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu baselineDependencies.welderDAO, baselineDependencies.rstudioDAO ) - val kubeAlg = new KubernetesInterpreter[IO]( - baselineDependencies.azureContainerService - ) val metricsMonitor = new LeoMetricsMonitor( ConfigReader.appConfig.metrics, - baselineDependencies.appDAO, - kubeAlg, - baselineDependencies.azureContainerService + baselineDependencies.appDAO ) val pubsubSubscriber = new LeoPubsubMessageSubscriber[IO]( diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index 93f7302f5c..633f7d3339 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -155,10 +155,6 @@ class BaselineDependenciesBuilder { azureVmService <- AzureVmService.fromAzureAppRegistrationConfig(ConfigReader.appConfig.azure.appRegistration) - azureContainerService <- AzureContainerService.fromAzureAppRegistrationConfig( - ConfigReader.appConfig.azure.appRegistration - ) - azureBatchService <- AzureBatchService.fromAzureAppRegistrationConfig( ConfigReader.appConfig.azure.appRegistration ) @@ -286,7 +282,6 @@ class BaselineDependenciesBuilder { samResourceCache, oidcConfig, appDAO, - azureContainerService, runtimeServiceConfig, kubernetesDnsCache, appDescriptorDAO, @@ -414,7 +409,6 @@ final case class BaselineDependencies[F[_]]( samResourceCache: scalacache.Cache[F, SamResourceCacheKey, (Option[String], Option[AppAccessScope])], openIDConnectConfiguration: OpenIDConnectConfiguration, appDAO: AppDAO[F], - azureContainerService: AzureContainerService[F], runtimeServicesConfig: RuntimeServiceConfig, kubernetesDnsCache: KubernetesDnsCache[F], appDescriptorDAO: HttpAppDescriptorDAO[F], diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala index 5265acb55a..d70d873e4c 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala @@ -6,29 +6,22 @@ import cats.effect.implicits.concurrentParTraverseOps import cats.mtl.Ask import cats.syntax.all._ import fs2.Stream -import io.kubernetes.client.custom.Quantity -import org.broadinstitute.dsde.workbench.azure.{AKSClusterName, AzureCloudContext, AzureContainerService} import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName -import org.broadinstitute.dsde.workbench.leonardo.LeoLenses.cloudContextToManagedResourceGroup import org.broadinstitute.dsde.workbench.leonardo.config.{Config, KubernetesAppConfig} import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.db.{DbReference, KubernetesServiceDbQueries, clusterQuery} -import org.broadinstitute.dsde.workbench.leonardo.http.{dbioToIO, _} +import org.broadinstitute.dsde.workbench.leonardo.http.dbioToIO import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ -import org.broadinstitute.dsde.workbench.leonardo.util.KubernetesAlgebra import org.broadinstitute.dsde.workbench.model.TraceId import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics import org.typelevel.log4cats.StructuredLogger import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration -import scala.jdk.CollectionConverters._ /** Collects metrics about active Leo runtimes and apps. */ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, - appDAO: AppDAO[F], - kubeAlg: KubernetesAlgebra[F], - azureContainerService: AzureContainerService[F] + appDAO: AppDAO[F] )(implicit F: Async[F], dbRef: DbReference[F], @@ -67,12 +60,6 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, appHealth <- countAppsByHealth(clusters) _ <- recordMetric(appHealth) _ <- logger.info(s"Recorded health metrics for ${appHealth.size} apps") - appResources <- getAppK8sResources(clusters) - _ <- recordMetric(appResources) - _ <- logger.info(s"Recorded ${appResources.size} app k8s resources") - nodepoolSize <- getNodepoolSize(clusters) - _ <- recordMetric(nodepoolSize) - _ <- logger.info(s"Recorded size for ${nodepoolSize.size} Azure nodepools") } yield () /** Queries the DB for all active runtimes and collects metrics */ @@ -102,7 +89,6 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, a.appType, a.status, getRuntimeUI(a.labels), - getAzureCloudContext(c.cloudContext), a.chart, isUpgradeable(a.appType, c.cloudContext.cloudProvider, a.chart) ) -> 1d @@ -117,17 +103,16 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, private[monitor] def countRuntimesByDbStatus(allRuntimes: List[RuntimeMetrics]): Map[RuntimeStatusMetric, Double] = { val allContainers = for { r <- allRuntimes - // Only care about Jupyter, RStudio, or Azure image types. + // Only care about Jupyter or RStudio image types. // Assume every runtime has exactly 1 of these. - imageTypes = Set(RuntimeImageType.Jupyter, RuntimeImageType.RStudio, RuntimeImageType.Azure) + imageTypes = Set(RuntimeImageType.Jupyter, RuntimeImageType.RStudio) c <- r.images.filter(i => imageTypes.contains(i.imageType)).headOption } yield Map( RuntimeStatusMetric(r.cloudContext.cloudProvider, c.imageType, c.imageUrl, r.status, - getRuntimeUI(r.labels), - getAzureCloudContext(r.cloudContext) + getRuntimeUI(r.labels) ) -> 1d ) allContainers.combineAll @@ -153,7 +138,6 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, for { ctx <- ev.ask // For GCP just test if the app is available through the Leo proxy. - // For Azure impersonate the user and call the app's status endpoint via Azure Relay. isUp <- cloudContext match { case CloudContext.Gcp(project) => appDAO.isProxyAvailable(project, app.appName, serviceName, ctx.traceId) @@ -180,7 +164,6 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, app.appType, serviceName, getRuntimeUI(app.labels), - getAzureCloudContext(cloudContext), isUp, app.chart, isUpgradeable(app.appType, cloudContext.cloudProvider, app.chart) @@ -190,7 +173,6 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, app.appType, serviceName, getRuntimeUI(app.labels), - getAzureCloudContext(cloudContext), !isUp, app.chart, isUpgradeable(app.appType, cloudContext.cloudProvider, app.chart) @@ -236,14 +218,12 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, image.imageType, image.imageUrl, getRuntimeUI(runtime.labels), - getAzureCloudContext(runtime.cloudContext), isUp ) -> 1d, RuntimeHealthMetric(runtime.cloudContext.cloudProvider, image.imageType, image.imageUrl, getRuntimeUI(runtime.labels), - getAzureCloudContext(runtime.cloudContext), !isUp ) -> 0d ) @@ -251,120 +231,6 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, .map(_.combineAll) } - /** - * Records the nodepool size per cluster. - * Only AKS supported. - */ - private[monitor] def getNodepoolSize( - allClusters: List[KubernetesCluster] - )(implicit ev: Ask[F, AppContext]): F[Map[NodepoolSizeMetric, Double]] = { - // TODO: handle GCP - val activeClusters = for { - c <- allClusters - // Filter out clusters whose apps have all been deleted - if c.cloudContext.cloudProvider == CloudProvider.Azure && c.nodepools - .flatMap(_.apps) - .exists(a => a.status != AppStatus.Deleted) - } yield List(c) - - activeClusters.combineAll - .parTraverseN(parallelism) { case cluster => - for { - ctx <- ev.ask - azureCloudContext <- F.fromOption( - cloudContextToManagedResourceGroup.get(cluster.cloudContext), - new RuntimeException(s"Azure cloud context not found for cluster ${cluster}: ${ctx.traceId}") - ) - clusterName = AKSClusterName(cluster.clusterName.value) - cluster <- azureContainerService.getCluster(clusterName, azureCloudContext).attempt - res <- cluster match { - case Left(_) => - logger - .warn(ctx.loggingCtx)( - s"Cluster ${azureCloudContext.asString} / ${clusterName} does not exist. Skipping metrics collection." - ) - .as(List.empty[Map[NodepoolSizeMetric, Double]]) - case Right(c) => - F.delay(c.agentPools().asScala.toList.map { case (name, pool) => - Map(NodepoolSizeMetric(azureCloudContext, name) -> pool.count.doubleValue) - }) - } - } yield res.combineAll - } - .map(_.combineAll) - } - - /** - * Records memory/cpu requests/limits by (cloud, appType, service). - * Only Azure apps supported. - */ - private[monitor] def getAppK8sResources(allClusters: List[KubernetesCluster])(implicit - ev: Ask[F, AppContext] - ): F[Map[AppResourcesMetric, Double]] = { - val allServices = for { - // TODO: handle GCP - c <- allClusters if c.cloudContext.cloudProvider == CloudProvider.Azure - n <- c.nodepools - // Only care about Running apps for resource metrics - a <- n.apps if a.status == AppStatus.Running - } yield Map((c.clusterName, c.cloudContext) -> List(a)) - - allServices.combineAll.toList - .parTraverseN(parallelism) { case ((clusterName, cloudContext), apps) => - for { - ctx <- ev.ask - - // Build k8s client - azureCloudContext <- F.fromOption( - cloudContextToManagedResourceGroup.get(cloudContext), - new RuntimeException(s"Azure cloud context not found for cluster ${clusterName}: ${ctx.traceId}") - ) - aksClusterName = AKSClusterName(clusterName.value) - client <- kubeAlg.createAzureClient(azureCloudContext, aksClusterName).attempt - - res <- client match { - case Left(_) => - logger - .warn(ctx.loggingCtx)( - s"Cluster ${azureCloudContext.asString} / ${clusterName} does not exist. Skipping metrics collection." - ) - .as(List.empty[Map[AppResourcesMetric, Double]]) - case Right(client) => - // For each app, query pods by leoAppName label and services by leoServiceName label. - // These labels are required for exposing Leo metrics. - apps.traverse { app => - val namespace = app.appResources.namespace - val labelSelector = s"leoAppName=${app.appName.value}" - for { - pods <- F.blocking( - client - .listNamespacedPod(namespace.value) - .labelSelector(labelSelector) - .execute() - ) - - res = pods.getItems.asScala.flatMap { pod => - pod.getMetadata.getLabels.asScala.get("leoServiceName").toList.flatMap { service => - pod.getSpec.getContainers.asScala.flatMap { container => - val resources = Option(container.getResources) - val requests = resources.flatMap(r => Option(r.getRequests)).map(_.asScala.toList) - val limits = resources.flatMap(r => Option(r.getLimits)).map(_.asScala.toList) - val requestMetrics = buildResourcesMetric(cloudContext, app, service, "request", requests) - val limitMetrics = buildResourcesMetric(cloudContext, app, service, "limit", limits) - requestMetrics ++ limitMetrics - } - } - } - } yield res.toList.combineAll - } - - } - - } yield res.combineAll - } - .map(_.combineAll) - } - /** Records and logs a generic AppMetric */ private[monitor] def recordMetric[T <: LeoMetric]( appMetric: Map[T, Double] @@ -388,42 +254,13 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, else if (labels.contains(Config.uiConfig.allOfUsLabel)) RuntimeUI.AoU else RuntimeUI.Other - private def getAzureCloudContext(cloudContext: CloudContext): Option[AzureCloudContext] = - (config.includeAzureCloudContext, cloudContext) match { - case (true, CloudContext.Azure(cc)) => Some(cc) - case _ => None - } - - private def buildResourcesMetric(cloudContext: CloudContext, - app: App, - service: String, - requestOrLimit: String, - resources: Option[List[(String, Quantity)]] - ): List[Map[AppResourcesMetric, Double]] = - resources - .map(_.map { case (resource, quantity) => - Map( - AppResourcesMetric( - cloudContext.cloudProvider, - app.appType, - ServiceName(service), - getRuntimeUI(app.labels), - getAzureCloudContext(cloudContext), - requestOrLimit, - resource, - app.chart - ) -> quantity.getNumber.doubleValue() // TODO are units consistent? - ) - }) - .getOrElse(List.empty) - private def isUpgradeable(appType: AppType, cloudProvider: CloudProvider, chart: Chart): Boolean = KubernetesAppConfig.configForTypeAndCloud(appType, cloudProvider).exists { config => !config.chartVersionsToExcludeFromUpdates.contains(chart.version) } } -case class LeoMetricsMonitorConfig(enabled: Boolean, checkInterval: FiniteDuration, includeAzureCloudContext: Boolean) +case class LeoMetricsMonitorConfig(enabled: Boolean, checkInterval: FiniteDuration) sealed trait LeoMetric { def name: String @@ -434,7 +271,6 @@ object LeoMetric { appType: AppType, status: AppStatus, runtimeUI: RuntimeUI, - azureCloudContext: Option[AzureCloudContext], chart: Chart, upgradeable: Boolean ) extends LeoMetric { @@ -445,7 +281,7 @@ object LeoMetric { "appType" -> appType.toString, "status" -> status.toString, "uiClient" -> runtimeUI.asString, - "azureCloudContext" -> azureCloudContext.map(_.asString).getOrElse(""), + "azureCloudContext" -> "", // obsolete "chart" -> chart.toString, "upgradeable" -> upgradeable.toString ) @@ -455,7 +291,6 @@ object LeoMetric { appType: AppType, serviceName: ServiceName, runtimeUI: RuntimeUI, - azureCloudContext: Option[AzureCloudContext], isUp: Boolean, chart: Chart, upgradeable: Boolean @@ -467,48 +302,17 @@ object LeoMetric { "serviceName" -> serviceName.value, "uiClient" -> runtimeUI.asString, "isUp" -> isUp.toString, - "azureCloudContext" -> azureCloudContext.map(_.asString).getOrElse(""), + "azureCloudContext" -> "", // obsolete "chart" -> chart.toString, "upgradeable" -> upgradeable.toString ) } - final case class AppResourcesMetric(cloudProvider: CloudProvider, - appType: AppType, - serviceName: ServiceName, - runtimeUI: RuntimeUI, - azureCloudContext: Option[AzureCloudContext], - requestOrLimit: String, - k8sResource: String, - chart: Chart - ) extends LeoMetric { - override def name: String = "leoAppResources" - override def tags: Map[String, String] = Map( - "cloudProvider" -> cloudProvider.asString, - "appType" -> appType.toString, - "serviceName" -> serviceName.value, - "uiClient" -> runtimeUI.asString, - "azureCloudContext" -> azureCloudContext.map(_.asString).getOrElse(""), - "requestOrLimit" -> requestOrLimit, - "k8sResource" -> k8sResource, - "chart" -> chart.toString - ) - } - - final case class NodepoolSizeMetric(azureCloudContext: AzureCloudContext, nodepoolName: String) extends LeoMetric { - override def name: String = "leoNodepoolSize" - override def tags: Map[String, String] = Map( - "azureCloudContext" -> azureCloudContext.asString, - "nodepoolName" -> nodepoolName - ) - } - final case class RuntimeStatusMetric(cloudProvider: CloudProvider, imageType: RuntimeImageType, imageUrl: String, status: RuntimeStatus, - runtimeUI: RuntimeUI, - azureCloudContext: Option[AzureCloudContext] + runtimeUI: RuntimeUI ) extends LeoMetric { override def name: String = "leoRuntimeStatus" override def tags: Map[String, String] = @@ -518,7 +322,7 @@ object LeoMetric { "imageUrl" -> imageUrl, "status" -> status.toString, "uiClient" -> runtimeUI.asString, - "azureCloudContext" -> azureCloudContext.map(_.asString).getOrElse("") + "azureCloudContext" -> "" // obsolete ) } @@ -526,7 +330,6 @@ object LeoMetric { imageType: RuntimeImageType, imageUrl: String, runtimeUI: RuntimeUI, - azureCloudContext: Option[AzureCloudContext], isUp: Boolean ) extends LeoMetric { override def name: String = "leoRuntimeHealth" @@ -537,7 +340,7 @@ object LeoMetric { "imageUrl" -> imageUrl, "uiClient" -> runtimeUI.asString, "isUp" -> isUp.toString, - "azureCloudContext" -> azureCloudContext.map(_.asString).getOrElse("") + "azureCloudContext" -> "" // obsolete ) } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesAlgebra.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesAlgebra.scala deleted file mode 100644 index 147f7bd0ee..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesAlgebra.scala +++ /dev/null @@ -1,36 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.util - -import cats.mtl.Ask -import io.kubernetes.client.openapi.apis.CoreV1Api -import org.broadinstitute.dsde.workbench.azure.{AKSClusterName, AzureCloudContext} -import org.broadinstitute.dsde.workbench.google2.KubernetesModels.{KubernetesNamespace, PodStatus} -import org.broadinstitute.dsde.workbench.leonardo.AppContext - -trait KubernetesAlgebra[F[_]] { - - /** Creates a k8s client given an Azure cloud context and cluster name. */ - def createAzureClient(cloudContext: AzureCloudContext, clusterName: AKSClusterName)(implicit - ev: Ask[F, AppContext] - ): F[CoreV1Api] - - /** Lists pods in a namespace. */ - def listPodStatus(clusterId: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[List[PodStatus]] - - /** Creates a namespace. */ - def createNamespace(client: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - /** Deletes a namespace. */ - def deleteNamespace(client: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[Unit] - - /** Checks whether a namespace exists. */ - def namespaceExists(client: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] - -} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesInterpreter.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesInterpreter.scala deleted file mode 100644 index b88e9c5f60..0000000000 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/util/KubernetesInterpreter.scala +++ /dev/null @@ -1,183 +0,0 @@ -package org.broadinstitute.dsde.workbench -package leonardo -package util - -import cats.Show -import cats.effect.Async -import cats.mtl.Ask -import cats.syntax.all._ -import io.kubernetes.client.openapi.apis.CoreV1Api -import io.kubernetes.client.openapi.models.{V1Namespace, V1NamespaceList, V1ObjectMeta} -import io.kubernetes.client.util.Config -import org.broadinstitute.dsde.workbench.azure.{AKSClusterName, AzureCloudContext, AzureContainerService} -import org.broadinstitute.dsde.workbench.google2.KubernetesModels.{KubernetesNamespace, PodStatus} -import org.broadinstitute.dsde.workbench.google2.util.RetryPredicates.whenStatusCode -import org.broadinstitute.dsde.workbench.google2.{autoClosableResourceF, recoverF} -import org.broadinstitute.dsde.workbench.leonardo.http._ -import org.broadinstitute.dsde.workbench.util2.withLogging -import org.typelevel.log4cats.StructuredLogger - -import java.io.ByteArrayInputStream -import java.util.Base64 -import scala.jdk.CollectionConverters._ - -class KubernetesInterpreter[F[_]](azureContainerService: AzureContainerService[F])(implicit - F: Async[F], - logger: StructuredLogger[F] -) extends KubernetesAlgebra[F] { - - override def createAzureClient(cloudContext: AzureCloudContext, clusterName: AKSClusterName)(implicit - ev: Ask[F, AppContext] - ): F[CoreV1Api] = for { - credentials <- azureContainerService.getClusterCredentials(clusterName, cloudContext) - client <- createClientInternal(credentials.token.value, credentials.certificate.value, credentials.server.value) - } yield client - -// Leave the implementation here in case later we'd like to converge this class with org.broadinstitute.dsde.workbench.google2.KubernetesService -// override def createGcpClient(clusterId: GKEModels.KubernetesClusterId)(implicit -// ev: Ask[F, AppContext] -// ): F[CoreV1Api] = for { -// ctx <- ev.ask -// clusterOpt <- gkeService.getCluster(clusterId) -// cluster <- F.fromEither( -// clusterOpt.toRight( -// KubernetesClusterNotFoundException( -// s"Could not create client for cluster $clusterId because it does not exist in GCP. Trace ID: ${ctx.traceId.asString}" -// ) -// ) -// ) -// _ <- F.blocking(credentials.refreshIfExpired()) -// token = credentials.getAccessToken.getTokenValue -// client <- createClientInternal(token, cluster.getMasterAuth.getClusterCaCertificate, cluster.getEndpoint) -// } yield client - - // The underlying http client for ApiClient claims that it releases idle threads and that shutdown is not necessary - // Here is a guide on how to proactively release resource if this proves to be problematic https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/#shutdown-isnt-necessary - private def createClientInternal(token: String, cert: String, endpoint: String): F[CoreV1Api] = { - val certResource = autoClosableResourceF( - new ByteArrayInputStream(Base64.getDecoder.decode(cert)) - ) - for { - apiClient <- certResource.use { certStream => - F.delay( - Config - .fromToken( - endpoint, - token - ) - .setSslCaCert(certStream) - // appending here a .setDebugging(true) prints out useful API request/response info for development - ) - } - _ <- F.blocking(apiClient.setApiKey(token)) - } yield new CoreV1Api(apiClient) - } - - override def listPodStatus(client: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[List[PodStatus]] = - for { - ctx <- ev.ask - call = - F.blocking( - client - .listNamespacedPod(namespace.name.value) - .pretty("true") - .execute() - ) - - response <- withLogging( - call, - Some(ctx.traceId), - s"io.kubernetes.client.apis.CoreV1Api.listNamespacedPod(${namespace.name.value}).pretty(true).execute()" - ) - - listPodStatus: List[PodStatus] = response.getItems.asScala.toList.flatMap(v1Pod => - PodStatus.stringToPodStatus - .get(v1Pod.getStatus.getPhase) - ) - - } yield listPodStatus - - override def createNamespace(client: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = - for { - ctx <- ev.ask - call = F - .blocking( - client - .createNamespace(new V1Namespace().metadata(new V1ObjectMeta().name(namespace.name.value))) - .pretty("true") - .execute() - ) - .void - _ <- withLogging( - call, - Some(ctx.traceId), - s"io.kubernetes.client.openapi.apis.CoreV1Api.createNamespace(${namespace.name.value}, true, null, null, null)" - ) - } yield () - - override def deleteNamespace(client: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[Unit] = { - val delete = for { - ctx <- ev.ask - call = - recoverF( - F.blocking( - client - .deleteNamespace(namespace.name.value) - .pretty("true") - .execute() - ).void - .recoverWith { - case e: com.google.gson.JsonSyntaxException - if e.getMessage.contains("Expected a string but was BEGIN_OBJECT") => - logger.error(e)("Ignore response parsing error") - } // see https://github.com/kubernetes-client/java/wiki/6.-Known-Issues#1-exception-on-deleting-resources-javalangillegalstateexception-expected-a-string-but-was-begin_object - , - whenStatusCode(404) - ) - _ <- withLogging( - call, - Some(ctx.traceId), - s"io.kubernetes.client.openapi.apis.CoreV1Api.deleteNamespace(${namespace.name.value}, true, null, null, null, null, null)" - ) - } yield () - - // There is a known bug with the client lib json decoding. `com.google.gson.JsonSyntaxException` occurs every time. - // See https://github.com/kubernetes-client/java/issues/86 - delete.handleErrorWith { - case _: com.google.gson.JsonSyntaxException => - F.unit - case e: Throwable => F.raiseError(e) - } - } - - override def namespaceExists(client: CoreV1Api, namespace: KubernetesNamespace)(implicit - ev: Ask[F, AppContext] - ): F[Boolean] = - for { - ctx <- ev.ask - call = - recoverF( - F.blocking( - client.listNamespace.pretty("true").allowWatchBookmarks(false).execute() - ), - whenStatusCode(409) - ) - v1NamespaceList <- withLogging( - call, - Some(ctx.traceId), - s"io.kubernetes.client.apis.CoreV1Api.listNamespace.pretty(true).allowWatchBookmarks(false).execute()", - Show.show[Option[V1NamespaceList]]( - _.fold("No namespace found")(x => x.getItems.asScala.toList.map(_.getMetadata.getName).mkString(",")) - ) - ) - } yield v1NamespaceList - .map(ls => ls.getItems.asScala.toList) - .getOrElse(List.empty) - .exists(x => x.getMetadata.getName == namespace.name.value) -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala index c4d562b895..3a9f2e4145 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/ConfigReaderSpec.scala @@ -46,7 +46,7 @@ class ConfigReaderSpec extends AnyFlatSpec with Matchers { DrsConfig( "https://drshub.dsde-dev.broadinstitute.org/api/v4/drs/resolve" ), - LeoMetricsMonitorConfig(true, 5 minutes, true) + LeoMetricsMonitorConfig(true, 5 minutes) ) config shouldBe expectedConfig diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index b03a73dead..e6835f208b 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -134,7 +134,6 @@ class GcpDependenciesBuilderSpec mock[Cache[IO, SamResourceCacheKey, (Option[String], Option[AppAccessScope])]], mock[OpenIDConnectConfiguration], mock[AppDAO[IO]], - mock[AzureContainerService[IO]], mock[RuntimeServiceConfig], mock[KubernetesDnsCache[IO]], mock[HttpAppDescriptorDAO[IO]], diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index e80a900c32..ca10ea5c20 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -1,682 +1,682 @@ package org.broadinstitute.dsde.workbench.leonardo.monitor -import cats.effect.IO -import cats.effect.unsafe.IORuntime -import com.azure.resourcemanager.containerservice.models.KubernetesClusterAgentPool -import io.kubernetes.client.custom.Quantity -import io.kubernetes.client.openapi.apis.CoreV1Api -import io.kubernetes.client.openapi.models._ -import org.broadinstitute.dsde.workbench.azure._ -import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName -import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} -import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{ - makeApp, - makeAzureCluster, - makeKubeCluster, - makeNodepool -} -import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.config.Config -import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ -import org.broadinstitute.dsde.workbench.leonardo.util.KubernetesAlgebra -import org.broadinstitute.dsde.workbench.leonardo.{ - AppName, - AppStatus, - AppType, - Chart, - CloudContext, - CloudProvider, - IpRange, - KubernetesCluster, - KubernetesClusterAsyncFields, - KubernetesService, - KubernetesServiceKindName, - LeonardoTestSuite, - NetworkFields, - RuntimeContainerServiceType, - RuntimeImage, - RuntimeImageType, - RuntimeMetrics, - RuntimeName, - RuntimeStatus, - RuntimeUI, - ServiceConfig, - ServiceId, - WorkspaceId -} -import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.broadinstitute.dsde.workbench.model.{IP, TraceId} -import org.mockito.ArgumentMatchers.{any, anyString} -import org.mockito.Mockito.when -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatestplus.mockito.MockitoSugar - -import java.time.Instant -import java.util.UUID -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ -import scala.jdk.CollectionConverters._ - -class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { - val azureContext = AzureCloudContext( - TenantId("tenant"), - SubscriptionId("sub"), - ManagedResourceGroupName("mrg") - ) - val azureContext2 = AzureCloudContext( - TenantId("tenant2"), - SubscriptionId("sub2"), - ManagedResourceGroupName("mrg2") - ) - - // Mocks - val appDAO = setUpMockAppDAO - val jupyterDAO = setUpMockJupyterDAO - val rstudioDAO = setUpMockRStudioDAO - val welderDAO = setUpMockWelderDAO - val kube = setUpMockKubeDAO - val containerService = setUpMockAzureContainerService - - // Test object - implicit val clusterToolToToolDao: RuntimeContainerServiceType => ToolDAO[IO, RuntimeContainerServiceType] = - ToolDAO.clusterToolToToolDao(jupyterDAO, welderDAO, rstudioDAO) - implicit val ec: ExecutionContext = cats.effect.unsafe.IORuntime.global.compute - val config = LeoMetricsMonitorConfig(true, 1 minute, true) - val leoMetricsMonitor = new LeoMetricsMonitor[IO]( - config, - appDAO, - kube, - containerService - ) - - "LeoMetricsMonitor" should "count apps by status" in { - val test = leoMetricsMonitor.countAppsByDbStatus(allApps) - // 10 apps - test.size shouldBe 5 - // Cromwell on GCP on Terra - test.get( - AppStatusMetric(CloudProvider.Gcp, - AppType.Cromwell, - AppStatus.Running, - RuntimeUI.Terra, - None, - cromwellChart, - true - ) - ) shouldBe Some(1) - // Galaxy on GCP - test.get( - AppStatusMetric(CloudProvider.Gcp, AppType.Galaxy, AppStatus.Running, RuntimeUI.Terra, None, galaxyChart, true) - ) shouldBe Some(1) - // Custom app on GCP - test.get( - AppStatusMetric(CloudProvider.Gcp, AppType.Custom, AppStatus.Running, RuntimeUI.Terra, None, customChart, true) - ) shouldBe Some(1) - // Cromwell on GCP on AoU - test.get( - AppStatusMetric(CloudProvider.Gcp, AppType.Cromwell, AppStatus.Running, RuntimeUI.AoU, None, cromwellChart, true) - ) shouldBe Some(1) - // RStudio on GCP on AoU - test.get( - AppStatusMetric(CloudProvider.Gcp, AppType.Allowed, AppStatus.Running, RuntimeUI.AoU, None, rstudioChart, true) - ) shouldBe Some(1) - } - - it should "count runtimes by status" in { - val test = leoMetricsMonitor.countRuntimesByDbStatus(allRuntimes) - // 4 runtimes - test.size shouldBe 4 - // Jupyter on GCP on Terra - test.get( - RuntimeStatusMetric(CloudProvider.Gcp, - jupyterImage.imageType, - jupyterImage.imageUrl, - RuntimeStatus.Running, - RuntimeUI.Terra, - None - ) - ) shouldBe Some(1) - // RStudio on GCP on Terra - test.get( - RuntimeStatusMetric(CloudProvider.Gcp, - rstudioImage.imageType, - rstudioImage.imageUrl, - RuntimeStatus.Running, - RuntimeUI.Terra, - None - ) - ) shouldBe Some(1) - // Jupyter on Azure - test.get( - RuntimeStatusMetric(CloudProvider.Azure, - azureImage.imageType, - azureImage.imageUrl, - RuntimeStatus.Running, - RuntimeUI.Terra, - Some(azureContext) - ) - ) shouldBe Some(1) - // Jupyter on GCP on AoU - test.get( - RuntimeStatusMetric(CloudProvider.Gcp, - jupyterImage.imageType, - jupyterImage.imageUrl, - RuntimeStatus.Running, - RuntimeUI.AoU, - None - ) - ) shouldBe Some(1) - } - -// it should "health check apps" in { -// val test = -// leoMetricsMonitor -// .countAppsByHealth(List(galaxyAppGcp)) -// .unsafeRunSync()(IORuntime.global) -// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy -// test.size shouldBe 12 -// List("cromwell", "cbas").foreach { s => -// test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.Cromwell, -// ServiceName(s), -// RuntimeUI.Terra, -// Some(azureContext), -// s != "cbas", -// cromwellOnAzureChart, -// true -// ) -// ) shouldBe Some(1) -// test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.Cromwell, -// ServiceName(s), -// RuntimeUI.Terra, -// Some(azureContext), -// s == "cbas", -// cromwellOnAzureChart, -// true -// ) -// ) shouldBe Some(0) -// } +//import cats.effect.IO +//import cats.effect.unsafe.IORuntime +//import com.azure.resourcemanager.containerservice.models.KubernetesClusterAgentPool +//import io.kubernetes.client.custom.Quantity +//import io.kubernetes.client.openapi.apis.CoreV1Api +//import io.kubernetes.client.openapi.models._ +//import org.broadinstitute.dsde.workbench.azure._ +//import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName +//import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} +//import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{ +// makeApp, +// makeAzureCluster, +// makeKubeCluster, +// makeNodepool +//} +//import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext +//import org.broadinstitute.dsde.workbench.leonardo.config.Config +//import org.broadinstitute.dsde.workbench.leonardo.dao._ +//import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent +//import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ +//import org.broadinstitute.dsde.workbench.leonardo.util.KubernetesAlgebra +//import org.broadinstitute.dsde.workbench.leonardo.{ +// AppName, +// AppStatus, +// AppType, +// Chart, +// CloudContext, +// CloudProvider, +// IpRange, +// KubernetesCluster, +// KubernetesClusterAsyncFields, +// KubernetesService, +// KubernetesServiceKindName, +// LeonardoTestSuite, +// NetworkFields, +// RuntimeContainerServiceType, +// RuntimeImage, +// RuntimeImageType, +// RuntimeMetrics, +// RuntimeName, +// RuntimeStatus, +// RuntimeUI, +// ServiceConfig, +// ServiceId, +// WorkspaceId +//} +//import org.broadinstitute.dsde.workbench.model.google.GoogleProject +//import org.broadinstitute.dsde.workbench.model.{IP, TraceId} +//import org.mockito.ArgumentMatchers.{any, anyString} +//import org.mockito.Mockito.when +//import org.scalatest.flatspec.AnyFlatSpec +//import org.scalatestplus.mockito.MockitoSugar +// +//import java.time.Instant +//import java.util.UUID +//import scala.concurrent.ExecutionContext +//import scala.concurrent.duration._ +//import scala.jdk.CollectionConverters._ +// +//class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { +// val azureContext = AzureCloudContext( +// TenantId("tenant"), +// SubscriptionId("sub"), +// ManagedResourceGroupName("mrg") +// ) +// val azureContext2 = AzureCloudContext( +// TenantId("tenant2"), +// SubscriptionId("sub2"), +// ManagedResourceGroupName("mrg2") +// ) +// +// // Mocks +// val appDAO = setUpMockAppDAO +// val jupyterDAO = setUpMockJupyterDAO +// val rstudioDAO = setUpMockRStudioDAO +// val welderDAO = setUpMockWelderDAO +// val kube = setUpMockKubeDAO +// val containerService = setUpMockAzureContainerService +// +// // Test object +// implicit val clusterToolToToolDao: RuntimeContainerServiceType => ToolDAO[IO, RuntimeContainerServiceType] = +// ToolDAO.clusterToolToToolDao(jupyterDAO, welderDAO, rstudioDAO) +// implicit val ec: ExecutionContext = cats.effect.unsafe.IORuntime.global.compute +// val config = LeoMetricsMonitorConfig(true, 1 minute, true) +// val leoMetricsMonitor = new LeoMetricsMonitor[IO]( +// config, +// appDAO, +// kube, +// containerService +// ) +// +// "LeoMetricsMonitor" should "count apps by status" in { +// val test = leoMetricsMonitor.countAppsByDbStatus(allApps) +// // 10 apps +// test.size shouldBe 5 +// // Cromwell on GCP on Terra // test.get( -// AppHealthMetric(CloudProvider.Gcp, -// AppType.Galaxy, -// ServiceName("galaxy"), +// AppStatusMetric(CloudProvider.Gcp, +// AppType.Cromwell, +// AppStatus.Running, // RuntimeUI.Terra, // None, -// true, -// galaxyChart, +// cromwellChart, // true // ) // ) shouldBe Some(1) +// // Galaxy on GCP // test.get( -// AppHealthMetric(CloudProvider.Gcp, -// AppType.Galaxy, -// ServiceName("galaxy"), -// RuntimeUI.Terra, -// None, -// false, -// galaxyChart, -// true +// AppStatusMetric(CloudProvider.Gcp, AppType.Galaxy, AppStatus.Running, RuntimeUI.Terra, None, galaxyChart, true) +// ) shouldBe Some(1) +// // Custom app on GCP +// test.get( +// AppStatusMetric(CloudProvider.Gcp, AppType.Custom, AppStatus.Running, RuntimeUI.Terra, None, customChart, true) +// ) shouldBe Some(1) +// // Cromwell on GCP on AoU +// test.get( +// AppStatusMetric(CloudProvider.Gcp, AppType.Cromwell, AppStatus.Running, RuntimeUI.AoU, None, cromwellChart, true) +// ) shouldBe Some(1) +// // RStudio on GCP on AoU +// test.get( +// AppStatusMetric(CloudProvider.Gcp, AppType.Allowed, AppStatus.Running, RuntimeUI.AoU, None, rstudioChart, true) +// ) shouldBe Some(1) +// } +// +// it should "count runtimes by status" in { +// val test = leoMetricsMonitor.countRuntimesByDbStatus(allRuntimes) +// // 4 runtimes +// test.size shouldBe 4 +// // Jupyter on GCP on Terra +// test.get( +// RuntimeStatusMetric(CloudProvider.Gcp, +// jupyterImage.imageType, +// jupyterImage.imageUrl, +// RuntimeStatus.Running, +// RuntimeUI.Terra, +// None // ) -// ) shouldBe Some(0) -// List("cromwell-reader", "cbas").foreach { s => -// test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.WorkflowsApp, -// ServiceName(s), -// RuntimeUI.Terra, -// Some(azureContext2), -// s != "cbas", -// workflowsAppChart, -// true -// ) -// ) shouldBe Some(1) -// test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.WorkflowsApp, -// ServiceName(s), -// RuntimeUI.Terra, -// Some(azureContext2), -// s == "cbas", -// workflowsAppChart, -// true -// ) -// ) shouldBe Some(0) -// } +// ) shouldBe Some(1) +// // RStudio on GCP on Terra // test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.CromwellRunnerApp, -// ServiceName("cromwell-runner"), -// RuntimeUI.Terra, -// Some(azureContext2), -// true, -// cromwellRunnerAppChart, -// true +// RuntimeStatusMetric(CloudProvider.Gcp, +// rstudioImage.imageType, +// rstudioImage.imageUrl, +// RuntimeStatus.Running, +// RuntimeUI.Terra, +// None // ) // ) shouldBe Some(1) +// // Jupyter on Azure // test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.CromwellRunnerApp, -// ServiceName("cromwell-runner"), -// RuntimeUI.Terra, -// Some(azureContext2), -// false, -// cromwellRunnerAppChart, -// true +// RuntimeStatusMetric(CloudProvider.Azure, +// azureImage.imageType, +// azureImage.imageUrl, +// RuntimeStatus.Running, +// RuntimeUI.Terra, +// Some(azureContext) +// ) +// ) shouldBe Some(1) +// // Jupyter on GCP on AoU +// test.get( +// RuntimeStatusMetric(CloudProvider.Gcp, +// jupyterImage.imageType, +// jupyterImage.imageUrl, +// RuntimeStatus.Running, +// RuntimeUI.AoU, +// None // ) -// ) shouldBe Some(0) +// ) shouldBe Some(1) // } - - it should "health check runtimes" in { - val test = leoMetricsMonitor.countRuntimesByHealth(List(jupyterAzure, rstudioGcp)).unsafeRunSync()(IORuntime.global) - // An up and a down for jupyter, rstudio, welder * 2 - test.size shouldBe 8 - // Jupyter Azure - List(azureImage, welderImage).foreach { i => - test.get( - RuntimeHealthMetric(CloudProvider.Azure, i.imageType, i.imageUrl, RuntimeUI.Terra, Some(azureContext), true) - ) shouldBe Some(1) - test.get( - RuntimeHealthMetric(CloudProvider.Azure, i.imageType, i.imageUrl, RuntimeUI.Terra, Some(azureContext), false) - ) shouldBe Some(0) - } - // RStudio GCP - List(rstudioImage, welderImage).foreach { i => - test.get( - RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, None, i != rstudioImage) - ) shouldBe Some(1) - test.get( - RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, None, i == rstudioImage) - ) shouldBe Some(0) - } - } - -// it should "not include AzureCloudContext if disabled" in { -// val config = LeoMetricsMonitorConfig(true, 1 minute, false) -// val azureDisabledMetricsMonitor = new LeoMetricsMonitor[IO]( -// config, -// appDAO, -// wdsDAO, -// cbasDAO, -// cromwellDAO, -// hailBatchDAO, -// relayListenerDAO, -// samDAO, -// kube, -// containerService -// ) -// val test = -// azureDisabledMetricsMonitor -// .countAppsByHealth(List(cromwellAppAzure, galaxyAppGcp, workflowsApp, cromwellRunnerApp)) -// .unsafeRunSync()(IORuntime.global) -// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy -// test.size shouldBe 12 -// List("cromwell", "cbas").foreach { s => +// +//// it should "health check apps" in { +//// val test = +//// leoMetricsMonitor +//// .countAppsByHealth(List(galaxyAppGcp)) +//// .unsafeRunSync()(IORuntime.global) +//// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy +//// test.size shouldBe 12 +//// List("cromwell", "cbas").foreach { s => +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.Cromwell, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// Some(azureContext), +//// s != "cbas", +//// cromwellOnAzureChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.Cromwell, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// Some(azureContext), +//// s == "cbas", +//// cromwellOnAzureChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// } +//// test.get( +//// AppHealthMetric(CloudProvider.Gcp, +//// AppType.Galaxy, +//// ServiceName("galaxy"), +//// RuntimeUI.Terra, +//// None, +//// true, +//// galaxyChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Gcp, +//// AppType.Galaxy, +//// ServiceName("galaxy"), +//// RuntimeUI.Terra, +//// None, +//// false, +//// galaxyChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// List("cromwell-reader", "cbas").foreach { s => +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.WorkflowsApp, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// s != "cbas", +//// workflowsAppChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.WorkflowsApp, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// s == "cbas", +//// workflowsAppChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// } +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.CromwellRunnerApp, +//// ServiceName("cromwell-runner"), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// true, +//// cromwellRunnerAppChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.CromwellRunnerApp, +//// ServiceName("cromwell-runner"), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// false, +//// cromwellRunnerAppChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// } +// +// it should "health check runtimes" in { +// val test = leoMetricsMonitor.countRuntimesByHealth(List(jupyterAzure, rstudioGcp)).unsafeRunSync()(IORuntime.global) +// // An up and a down for jupyter, rstudio, welder * 2 +// test.size shouldBe 8 +// // Jupyter Azure +// List(azureImage, welderImage).foreach { i => // test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.Cromwell, -// ServiceName(s), -// RuntimeUI.Terra, -// None, -// s != "cbas", -// cromwellOnAzureChart, -// true -// ) +// RuntimeHealthMetric(CloudProvider.Azure, i.imageType, i.imageUrl, RuntimeUI.Terra, Some(azureContext), true) // ) shouldBe Some(1) // test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.Cromwell, -// ServiceName(s), -// RuntimeUI.Terra, -// None, -// s == "cbas", -// cromwellOnAzureChart, -// true -// ) +// RuntimeHealthMetric(CloudProvider.Azure, i.imageType, i.imageUrl, RuntimeUI.Terra, Some(azureContext), false) // ) shouldBe Some(0) // } -// test.get( -// AppHealthMetric(CloudProvider.Gcp, -// AppType.Galaxy, -// ServiceName("galaxy"), -// RuntimeUI.Terra, -// None, -// true, -// galaxyChart, -// true -// ) -// ) shouldBe Some(1) -// test.get( -// AppHealthMetric(CloudProvider.Gcp, -// AppType.Galaxy, -// ServiceName("galaxy"), -// RuntimeUI.Terra, -// None, -// false, -// galaxyChart, -// true -// ) -// ) shouldBe Some(0) -// List("cromwell-reader", "cbas").foreach { s => +// // RStudio GCP +// List(rstudioImage, welderImage).foreach { i => // test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.WorkflowsApp, -// ServiceName(s), -// RuntimeUI.Terra, -// None, -// s != "cbas", -// workflowsAppChart, -// true -// ) +// RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, None, i != rstudioImage) // ) shouldBe Some(1) // test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.WorkflowsApp, -// ServiceName(s), -// RuntimeUI.Terra, -// None, -// s == "cbas", -// workflowsAppChart, -// true -// ) +// RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, None, i == rstudioImage) // ) shouldBe Some(0) // } -// test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.CromwellRunnerApp, -// ServiceName("cromwell-runner"), -// RuntimeUI.Terra, -// None, -// true, -// cromwellRunnerAppChart, -// true -// ) -// ) shouldBe Some(1) -// test.get( -// AppHealthMetric(CloudProvider.Azure, -// AppType.CromwellRunnerApp, -// ServiceName("cromwell-runner"), -// RuntimeUI.Terra, -// None, -// false, -// cromwellRunnerAppChart, -// true +// } +// +//// it should "not include AzureCloudContext if disabled" in { +//// val config = LeoMetricsMonitorConfig(true, 1 minute, false) +//// val azureDisabledMetricsMonitor = new LeoMetricsMonitor[IO]( +//// config, +//// appDAO, +//// wdsDAO, +//// cbasDAO, +//// cromwellDAO, +//// hailBatchDAO, +//// relayListenerDAO, +//// samDAO, +//// kube, +//// containerService +//// ) +//// val test = +//// azureDisabledMetricsMonitor +//// .countAppsByHealth(List(cromwellAppAzure, galaxyAppGcp, workflowsApp, cromwellRunnerApp)) +//// .unsafeRunSync()(IORuntime.global) +//// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy +//// test.size shouldBe 12 +//// List("cromwell", "cbas").foreach { s => +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.Cromwell, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// None, +//// s != "cbas", +//// cromwellOnAzureChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.Cromwell, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// None, +//// s == "cbas", +//// cromwellOnAzureChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// } +//// test.get( +//// AppHealthMetric(CloudProvider.Gcp, +//// AppType.Galaxy, +//// ServiceName("galaxy"), +//// RuntimeUI.Terra, +//// None, +//// true, +//// galaxyChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Gcp, +//// AppType.Galaxy, +//// ServiceName("galaxy"), +//// RuntimeUI.Terra, +//// None, +//// false, +//// galaxyChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// List("cromwell-reader", "cbas").foreach { s => +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.WorkflowsApp, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// None, +//// s != "cbas", +//// workflowsAppChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.WorkflowsApp, +//// ServiceName(s), +//// RuntimeUI.Terra, +//// None, +//// s == "cbas", +//// workflowsAppChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// } +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.CromwellRunnerApp, +//// ServiceName("cromwell-runner"), +//// RuntimeUI.Terra, +//// None, +//// true, +//// cromwellRunnerAppChart, +//// true +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppHealthMetric(CloudProvider.Azure, +//// AppType.CromwellRunnerApp, +//// ServiceName("cromwell-runner"), +//// RuntimeUI.Terra, +//// None, +//// false, +//// cromwellRunnerAppChart, +//// true +//// ) +//// ) shouldBe Some(0) +//// } +// +//// it should "record nodepool size" in { +//// val test = leoMetricsMonitor.getNodepoolSize(List(wdsAppAzure, hailBatchAppAzure)).unsafeRunSync()(IORuntime.global) +//// test.size shouldBe 4 +//// test.get(NodepoolSizeMetric(azureContext, "pool1")) shouldBe Some(10) +//// test.get(NodepoolSizeMetric(azureContext, "pool2")) shouldBe Some(1) +//// test.get(NodepoolSizeMetric(azureContext2, "pool1")) shouldBe Some(10) +//// test.get(NodepoolSizeMetric(azureContext2, "pool2")) shouldBe Some(1) +//// } +// +//// it should "record app k8s metrics" in { +//// val chart = Chart.fromString("wds-0.0.1").get +//// val test = leoMetricsMonitor.getAppK8sResources(List(wdsAppAzure)).unsafeRunSync()(IORuntime.global) +//// test.size shouldBe 4 +//// test.get( +//// AppResourcesMetric(CloudProvider.Azure, +//// AppType.Wds, +//// ServiceName("wds"), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// "request", +//// "cpu", +//// chart +//// ) +//// ) shouldBe Some(1) +//// test.get( +//// AppResourcesMetric(CloudProvider.Azure, +//// AppType.Wds, +//// ServiceName("wds"), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// "request", +//// "memory", +//// chart +//// ) +//// ) shouldBe Some(1073741824d) +//// test.get( +//// AppResourcesMetric(CloudProvider.Azure, +//// AppType.Wds, +//// ServiceName("wds"), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// "limit", +//// "cpu", +//// chart +//// ) +//// ) shouldBe Some(2) +//// test.get( +//// AppResourcesMetric(CloudProvider.Azure, +//// AppType.Wds, +//// ServiceName("wds"), +//// RuntimeUI.Terra, +//// Some(azureContext2), +//// "limit", +//// "memory", +//// chart +//// ) +//// ) shouldBe Some(2147483648d) +//// } +// +// // Data generators +// +// private def genApp(isAzure: Boolean, +// appType: AppType, +// chart: Chart, +// isAou: Boolean, +// isCromwell: Boolean, +// isWorkflowsApp: Boolean, +// isCromwellRunnerApp: Boolean = false +// ): KubernetesCluster = { +// val cluster = if (isAzure) makeAzureCluster(1) else makeKubeCluster(1) +// val clusterWithAsyncFields = cluster.copy(asyncFields = +// Some( +// KubernetesClusterAsyncFields(IP("1.2.3.4"), +// IP("2.4.5.6"), +// NetworkFields(NetworkName("network"), SubnetworkName("subnet"), IpRange("ipRange")) +// ) // ) -// ) shouldBe Some(0) +// ) +// val nodepool = makeNodepool(1, clusterWithAsyncFields.id) +// val app = makeApp(1, nodepool.id).copy( +// appType = appType, +// chart = chart, +// status = AppStatus.Running, +// labels = if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") +// ) +// val services = +// if (isCromwell) List("cbas", "cromwell") +// else if (isCromwellRunnerApp) List("cromwell-runner") +// else if (isWorkflowsApp) List("cbas", "cromwell-reader") +// else List(appType.toString.toLowerCase) +// val appWithServices = app.copy(appResources = app.appResources.copy(services = services.map(genService))) +// clusterWithAsyncFields.copy(nodepools = List(nodepool.copy(apps = List(appWithServices)))) // } - -// it should "record nodepool size" in { -// val test = leoMetricsMonitor.getNodepoolSize(List(wdsAppAzure, hailBatchAppAzure)).unsafeRunSync()(IORuntime.global) -// test.size shouldBe 4 -// test.get(NodepoolSizeMetric(azureContext, "pool1")) shouldBe Some(10) -// test.get(NodepoolSizeMetric(azureContext, "pool2")) shouldBe Some(1) -// test.get(NodepoolSizeMetric(azureContext2, "pool1")) shouldBe Some(10) -// test.get(NodepoolSizeMetric(azureContext2, "pool2")) shouldBe Some(1) +// +// def genService(name: String): KubernetesService = +// KubernetesService(ServiceId(-1), ServiceConfig(ServiceName(name), KubernetesServiceKindName("ClusterIP"))) +// +// private def cromwellAppGcp: KubernetesCluster = +// genApp(false, AppType.Cromwell, cromwellChart, false, true, false) +// private def galaxyAppGcp: KubernetesCluster = +// genApp(false, AppType.Galaxy, galaxyChart, false, false, false) +// private def customAppGcp: KubernetesCluster = +// genApp(false, AppType.Custom, customChart, false, false, false) +// private def cromwellAppGcpAou: KubernetesCluster = +// genApp(false, AppType.Cromwell, cromwellChart, true, true, false) +// private def rstudioAppGcpAou: KubernetesCluster = +// genApp(false, AppType.Allowed, rstudioChart, true, false, false) +// +// private def cromwellChart = Chart.fromString("cromwell-0.0.1").get +// private def galaxyChart = Chart.fromString("galaxy-0.0.1").get +// private def customChart = Chart.fromString("custom-0.0.1").get +// private def rstudioChart = Chart.fromString("rstudio-0.0.1").get +// +// private def allApps = +// List( +// cromwellAppGcp, +// galaxyAppGcp, +// customAppGcp, +// cromwellAppGcpAou, +// rstudioAppGcpAou +// ) +// +// private def genRuntime(isJupyter: Boolean, isAou: Boolean, isGcp: Boolean): RuntimeMetrics = +// RuntimeMetrics( +// if (isGcp) CloudContext.Gcp(GoogleProject("project")) +// else +// CloudContext.Azure( +// AzureCloudContext( +// TenantId("tenant"), +// SubscriptionId("sub"), +// ManagedResourceGroupName("mrg") +// ) +// ), +// RuntimeName("runtime"), +// RuntimeStatus.Running, +// Some(WorkspaceId(UUID.randomUUID())), +// Set(if (isJupyter) if (isGcp) jupyterImage else azureImage else rstudioImage, welderImage), +// if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") +// ) +// +// private def jupyterGcp: RuntimeMetrics = genRuntime(true, false, true) +// private def rstudioGcp: RuntimeMetrics = genRuntime(false, false, true) +// private def jupyterAzure: RuntimeMetrics = genRuntime(true, false, false) +// private def jupyterGcpAou: RuntimeMetrics = genRuntime(true, true, true) +// +// private val jupyterImage = RuntimeImage(RuntimeImageType.Jupyter, "jupyter:0.0.1", None, Instant.now) +// private val rstudioImage = RuntimeImage(RuntimeImageType.RStudio, "rstudio:0.0.1", None, Instant.now) +// private val welderImage = RuntimeImage(RuntimeImageType.Welder, "welder:0.0.1", None, Instant.now) +// private val azureImage = RuntimeImage(RuntimeImageType.Azure, "azure:0.0.1", None, Instant.now) +// +// private def allRuntimes = List(jupyterGcp, rstudioGcp, jupyterAzure, jupyterGcpAou) +// +// // Mocks +// +// private def setUpMockSamDAO: SamDAO[IO] = { +// val sam = mock[SamDAO[IO]] +// when { +// sam.getCachedArbitraryPetAccessToken(any)(any) +// } thenReturn IO.pure(Some("token")) +// sam // } - -// it should "record app k8s metrics" in { -// val chart = Chart.fromString("wds-0.0.1").get -// val test = leoMetricsMonitor.getAppK8sResources(List(wdsAppAzure)).unsafeRunSync()(IORuntime.global) -// test.size shouldBe 4 -// test.get( -// AppResourcesMetric(CloudProvider.Azure, -// AppType.Wds, -// ServiceName("wds"), -// RuntimeUI.Terra, -// Some(azureContext2), -// "request", -// "cpu", -// chart -// ) -// ) shouldBe Some(1) -// test.get( -// AppResourcesMetric(CloudProvider.Azure, -// AppType.Wds, -// ServiceName("wds"), -// RuntimeUI.Terra, -// Some(azureContext2), -// "request", -// "memory", -// chart -// ) -// ) shouldBe Some(1073741824d) -// test.get( -// AppResourcesMetric(CloudProvider.Azure, -// AppType.Wds, -// ServiceName("wds"), -// RuntimeUI.Terra, -// Some(azureContext2), -// "limit", -// "cpu", -// chart +// +// private def setUpMockAppDAO: AppDAO[IO] = { +// val app = mock[AppDAO[IO]] +// when { +// app.isProxyAvailable(any, any[String].asInstanceOf[AppName], any, TraceId(anyString())) +// } thenReturn IO.pure(true) +// app +// } +// +// private def setUpMockJupyterDAO: JupyterDAO[IO] = { +// val jupyter = mock[JupyterDAO[IO]] +// when { +// jupyter.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) +// } thenReturn IO.pure(true) +// jupyter +// } +// +// // RStudio is down +// private def setUpMockRStudioDAO: RStudioDAO[IO] = { +// val rstudio = mock[RStudioDAO[IO]] +// when { +// rstudio.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) +// } thenReturn IO.pure(false) +// rstudio +// } +// +// private def setUpMockWelderDAO: WelderDAO[IO] = { +// val welder = mock[WelderDAO[IO]] +// when { +// welder.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) +// } thenReturn IO.pure(true) +// welder +// } +// +// private def setUpMockKubeDAO: KubernetesAlgebra[IO] = { +// val client = mock[CoreV1Api] +// val podList = mock[V1PodList] +// val pod = mock[V1Pod] +// val mockRequest = mock[CoreV1Api#APIlistNamespacedPodRequest] +// val spec = mock[V1PodSpec] +// val container = mock[V1Container] +// val kube = mock[KubernetesAlgebra[IO]] +// when { +// container.getResources +// } thenReturn new V1ResourceRequirements() +// .requests( +// Map("cpu" -> Quantity.fromString("1"), "memory" -> Quantity.fromString("1073741824")).asJava // ) -// ) shouldBe Some(2) -// test.get( -// AppResourcesMetric(CloudProvider.Azure, -// AppType.Wds, -// ServiceName("wds"), -// RuntimeUI.Terra, -// Some(azureContext2), -// "limit", -// "memory", -// chart +// .limits( +// Map("cpu" -> Quantity.fromString("2"), "memory" -> Quantity.fromString("2147483648")).asJava // ) -// ) shouldBe Some(2147483648d) +// when { +// spec.getContainers +// } thenReturn List(container).asJava +// when { +// pod.getSpec +// } thenReturn spec +// when { +// pod.getMetadata +// } thenReturn new V1ObjectMeta().labels(Map("leoServiceName" -> "wds").asJava) +// when { +// podList.getItems +// } thenReturn List(pod).asJava +// when { +// client +// .listNamespacedPod(any) +// } thenReturn mockRequest +// when { +// client +// .listNamespacedPod(any) +// .pretty(any) +// } thenReturn mockRequest +// when { +// client +// .listNamespacedPod(any) +// .labelSelector(any) +// } thenReturn mockRequest +// when { +// client +// .listNamespacedPod(any) +// .pretty(any) +// .labelSelector(any) +// .execute() +// } thenReturn podList +// when { +// kube.createAzureClient(any, any[String].asInstanceOf[AKSClusterName])(any) +// } thenReturn IO.pure(client) +// kube // } - - // Data generators - - private def genApp(isAzure: Boolean, - appType: AppType, - chart: Chart, - isAou: Boolean, - isCromwell: Boolean, - isWorkflowsApp: Boolean, - isCromwellRunnerApp: Boolean = false - ): KubernetesCluster = { - val cluster = if (isAzure) makeAzureCluster(1) else makeKubeCluster(1) - val clusterWithAsyncFields = cluster.copy(asyncFields = - Some( - KubernetesClusterAsyncFields(IP("1.2.3.4"), - IP("2.4.5.6"), - NetworkFields(NetworkName("network"), SubnetworkName("subnet"), IpRange("ipRange")) - ) - ) - ) - val nodepool = makeNodepool(1, clusterWithAsyncFields.id) - val app = makeApp(1, nodepool.id).copy( - appType = appType, - chart = chart, - status = AppStatus.Running, - labels = if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") - ) - val services = - if (isCromwell) List("cbas", "cromwell") - else if (isCromwellRunnerApp) List("cromwell-runner") - else if (isWorkflowsApp) List("cbas", "cromwell-reader") - else List(appType.toString.toLowerCase) - val appWithServices = app.copy(appResources = app.appResources.copy(services = services.map(genService))) - clusterWithAsyncFields.copy(nodepools = List(nodepool.copy(apps = List(appWithServices)))) - } - - def genService(name: String): KubernetesService = - KubernetesService(ServiceId(-1), ServiceConfig(ServiceName(name), KubernetesServiceKindName("ClusterIP"))) - - private def cromwellAppGcp: KubernetesCluster = - genApp(false, AppType.Cromwell, cromwellChart, false, true, false) - private def galaxyAppGcp: KubernetesCluster = - genApp(false, AppType.Galaxy, galaxyChart, false, false, false) - private def customAppGcp: KubernetesCluster = - genApp(false, AppType.Custom, customChart, false, false, false) - private def cromwellAppGcpAou: KubernetesCluster = - genApp(false, AppType.Cromwell, cromwellChart, true, true, false) - private def rstudioAppGcpAou: KubernetesCluster = - genApp(false, AppType.Allowed, rstudioChart, true, false, false) - - private def cromwellChart = Chart.fromString("cromwell-0.0.1").get - private def galaxyChart = Chart.fromString("galaxy-0.0.1").get - private def customChart = Chart.fromString("custom-0.0.1").get - private def rstudioChart = Chart.fromString("rstudio-0.0.1").get - - private def allApps = - List( - cromwellAppGcp, - galaxyAppGcp, - customAppGcp, - cromwellAppGcpAou, - rstudioAppGcpAou - ) - - private def genRuntime(isJupyter: Boolean, isAou: Boolean, isGcp: Boolean): RuntimeMetrics = - RuntimeMetrics( - if (isGcp) CloudContext.Gcp(GoogleProject("project")) - else - CloudContext.Azure( - AzureCloudContext( - TenantId("tenant"), - SubscriptionId("sub"), - ManagedResourceGroupName("mrg") - ) - ), - RuntimeName("runtime"), - RuntimeStatus.Running, - Some(WorkspaceId(UUID.randomUUID())), - Set(if (isJupyter) if (isGcp) jupyterImage else azureImage else rstudioImage, welderImage), - if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") - ) - - private def jupyterGcp: RuntimeMetrics = genRuntime(true, false, true) - private def rstudioGcp: RuntimeMetrics = genRuntime(false, false, true) - private def jupyterAzure: RuntimeMetrics = genRuntime(true, false, false) - private def jupyterGcpAou: RuntimeMetrics = genRuntime(true, true, true) - - private val jupyterImage = RuntimeImage(RuntimeImageType.Jupyter, "jupyter:0.0.1", None, Instant.now) - private val rstudioImage = RuntimeImage(RuntimeImageType.RStudio, "rstudio:0.0.1", None, Instant.now) - private val welderImage = RuntimeImage(RuntimeImageType.Welder, "welder:0.0.1", None, Instant.now) - private val azureImage = RuntimeImage(RuntimeImageType.Azure, "azure:0.0.1", None, Instant.now) - - private def allRuntimes = List(jupyterGcp, rstudioGcp, jupyterAzure, jupyterGcpAou) - - // Mocks - - private def setUpMockSamDAO: SamDAO[IO] = { - val sam = mock[SamDAO[IO]] - when { - sam.getCachedArbitraryPetAccessToken(any)(any) - } thenReturn IO.pure(Some("token")) - sam - } - - private def setUpMockAppDAO: AppDAO[IO] = { - val app = mock[AppDAO[IO]] - when { - app.isProxyAvailable(any, any[String].asInstanceOf[AppName], any, TraceId(anyString())) - } thenReturn IO.pure(true) - app - } - - private def setUpMockJupyterDAO: JupyterDAO[IO] = { - val jupyter = mock[JupyterDAO[IO]] - when { - jupyter.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) - } thenReturn IO.pure(true) - jupyter - } - - // RStudio is down - private def setUpMockRStudioDAO: RStudioDAO[IO] = { - val rstudio = mock[RStudioDAO[IO]] - when { - rstudio.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) - } thenReturn IO.pure(false) - rstudio - } - - private def setUpMockWelderDAO: WelderDAO[IO] = { - val welder = mock[WelderDAO[IO]] - when { - welder.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) - } thenReturn IO.pure(true) - welder - } - - private def setUpMockKubeDAO: KubernetesAlgebra[IO] = { - val client = mock[CoreV1Api] - val podList = mock[V1PodList] - val pod = mock[V1Pod] - val mockRequest = mock[CoreV1Api#APIlistNamespacedPodRequest] - val spec = mock[V1PodSpec] - val container = mock[V1Container] - val kube = mock[KubernetesAlgebra[IO]] - when { - container.getResources - } thenReturn new V1ResourceRequirements() - .requests( - Map("cpu" -> Quantity.fromString("1"), "memory" -> Quantity.fromString("1073741824")).asJava - ) - .limits( - Map("cpu" -> Quantity.fromString("2"), "memory" -> Quantity.fromString("2147483648")).asJava - ) - when { - spec.getContainers - } thenReturn List(container).asJava - when { - pod.getSpec - } thenReturn spec - when { - pod.getMetadata - } thenReturn new V1ObjectMeta().labels(Map("leoServiceName" -> "wds").asJava) - when { - podList.getItems - } thenReturn List(pod).asJava - when { - client - .listNamespacedPod(any) - } thenReturn mockRequest - when { - client - .listNamespacedPod(any) - .pretty(any) - } thenReturn mockRequest - when { - client - .listNamespacedPod(any) - .labelSelector(any) - } thenReturn mockRequest - when { - client - .listNamespacedPod(any) - .pretty(any) - .labelSelector(any) - .execute() - } thenReturn podList - when { - kube.createAzureClient(any, any[String].asInstanceOf[AKSClusterName])(any) - } thenReturn IO.pure(client) - kube - } - - private def setUpMockAzureContainerService: AzureContainerService[IO] = { - val container = mock[AzureContainerService[IO]] - val cluster = mock[com.azure.resourcemanager.containerservice.models.KubernetesCluster] - val pool1 = mock[KubernetesClusterAgentPool] - when { - pool1.count() - } thenReturn 10 - val pool2 = mock[KubernetesClusterAgentPool] - when { - pool2.count() - } thenReturn 1 - when { - cluster.agentPools() - } thenReturn Map("pool1" -> pool1, "pool2" -> pool2).asJava - when { - container.getCluster(any[String].asInstanceOf[AKSClusterName], any)(any) - } thenReturn IO.pure(cluster) - container - } -} +// +// private def setUpMockAzureContainerService: AzureContainerService[IO] = { +// val container = mock[AzureContainerService[IO]] +// val cluster = mock[com.azure.resourcemanager.containerservice.models.KubernetesCluster] +// val pool1 = mock[KubernetesClusterAgentPool] +// when { +// pool1.count() +// } thenReturn 10 +// val pool2 = mock[KubernetesClusterAgentPool] +// when { +// pool2.count() +// } thenReturn 1 +// when { +// cluster.agentPools() +// } thenReturn Map("pool1" -> pool1, "pool2" -> pool2).asJava +// when { +// container.getCluster(any[String].asInstanceOf[AKSClusterName], any)(any) +// } thenReturn IO.pure(cluster) +// container +// } +//} From aed9cbea6a608088993a42435674fc97991863fe Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Mon, 7 Jul 2025 16:46:45 -0400 Subject: [PATCH 23/43] Clean up LeoMetricsMonitorSpec --- .../monitor/LeoMetricsMonitorSpec.scala | 964 ++++++------------ 1 file changed, 284 insertions(+), 680 deletions(-) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index ca10ea5c20..2c5ff552fe 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -1,682 +1,286 @@ package org.broadinstitute.dsde.workbench.leonardo.monitor -//import cats.effect.IO -//import cats.effect.unsafe.IORuntime -//import com.azure.resourcemanager.containerservice.models.KubernetesClusterAgentPool -//import io.kubernetes.client.custom.Quantity -//import io.kubernetes.client.openapi.apis.CoreV1Api -//import io.kubernetes.client.openapi.models._ -//import org.broadinstitute.dsde.workbench.azure._ -//import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName -//import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} -//import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{ -// makeApp, -// makeAzureCluster, -// makeKubeCluster, -// makeNodepool -//} -//import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -//import org.broadinstitute.dsde.workbench.leonardo.config.Config -//import org.broadinstitute.dsde.workbench.leonardo.dao._ -//import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent -//import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ -//import org.broadinstitute.dsde.workbench.leonardo.util.KubernetesAlgebra -//import org.broadinstitute.dsde.workbench.leonardo.{ -// AppName, -// AppStatus, -// AppType, -// Chart, -// CloudContext, -// CloudProvider, -// IpRange, -// KubernetesCluster, -// KubernetesClusterAsyncFields, -// KubernetesService, -// KubernetesServiceKindName, -// LeonardoTestSuite, -// NetworkFields, -// RuntimeContainerServiceType, -// RuntimeImage, -// RuntimeImageType, -// RuntimeMetrics, -// RuntimeName, -// RuntimeStatus, -// RuntimeUI, -// ServiceConfig, -// ServiceId, -// WorkspaceId -//} -//import org.broadinstitute.dsde.workbench.model.google.GoogleProject -//import org.broadinstitute.dsde.workbench.model.{IP, TraceId} -//import org.mockito.ArgumentMatchers.{any, anyString} -//import org.mockito.Mockito.when -//import org.scalatest.flatspec.AnyFlatSpec -//import org.scalatestplus.mockito.MockitoSugar -// -//import java.time.Instant -//import java.util.UUID -//import scala.concurrent.ExecutionContext -//import scala.concurrent.duration._ -//import scala.jdk.CollectionConverters._ -// -//class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { -// val azureContext = AzureCloudContext( -// TenantId("tenant"), -// SubscriptionId("sub"), -// ManagedResourceGroupName("mrg") -// ) -// val azureContext2 = AzureCloudContext( -// TenantId("tenant2"), -// SubscriptionId("sub2"), -// ManagedResourceGroupName("mrg2") -// ) -// -// // Mocks -// val appDAO = setUpMockAppDAO -// val jupyterDAO = setUpMockJupyterDAO -// val rstudioDAO = setUpMockRStudioDAO -// val welderDAO = setUpMockWelderDAO -// val kube = setUpMockKubeDAO -// val containerService = setUpMockAzureContainerService -// -// // Test object -// implicit val clusterToolToToolDao: RuntimeContainerServiceType => ToolDAO[IO, RuntimeContainerServiceType] = -// ToolDAO.clusterToolToToolDao(jupyterDAO, welderDAO, rstudioDAO) -// implicit val ec: ExecutionContext = cats.effect.unsafe.IORuntime.global.compute -// val config = LeoMetricsMonitorConfig(true, 1 minute, true) -// val leoMetricsMonitor = new LeoMetricsMonitor[IO]( -// config, -// appDAO, -// kube, -// containerService -// ) -// -// "LeoMetricsMonitor" should "count apps by status" in { -// val test = leoMetricsMonitor.countAppsByDbStatus(allApps) -// // 10 apps -// test.size shouldBe 5 -// // Cromwell on GCP on Terra -// test.get( -// AppStatusMetric(CloudProvider.Gcp, -// AppType.Cromwell, -// AppStatus.Running, -// RuntimeUI.Terra, -// None, -// cromwellChart, -// true -// ) -// ) shouldBe Some(1) -// // Galaxy on GCP -// test.get( -// AppStatusMetric(CloudProvider.Gcp, AppType.Galaxy, AppStatus.Running, RuntimeUI.Terra, None, galaxyChart, true) -// ) shouldBe Some(1) -// // Custom app on GCP -// test.get( -// AppStatusMetric(CloudProvider.Gcp, AppType.Custom, AppStatus.Running, RuntimeUI.Terra, None, customChart, true) -// ) shouldBe Some(1) -// // Cromwell on GCP on AoU -// test.get( -// AppStatusMetric(CloudProvider.Gcp, AppType.Cromwell, AppStatus.Running, RuntimeUI.AoU, None, cromwellChart, true) -// ) shouldBe Some(1) -// // RStudio on GCP on AoU -// test.get( -// AppStatusMetric(CloudProvider.Gcp, AppType.Allowed, AppStatus.Running, RuntimeUI.AoU, None, rstudioChart, true) -// ) shouldBe Some(1) -// } -// -// it should "count runtimes by status" in { -// val test = leoMetricsMonitor.countRuntimesByDbStatus(allRuntimes) -// // 4 runtimes -// test.size shouldBe 4 -// // Jupyter on GCP on Terra -// test.get( -// RuntimeStatusMetric(CloudProvider.Gcp, -// jupyterImage.imageType, -// jupyterImage.imageUrl, -// RuntimeStatus.Running, -// RuntimeUI.Terra, -// None -// ) -// ) shouldBe Some(1) -// // RStudio on GCP on Terra -// test.get( -// RuntimeStatusMetric(CloudProvider.Gcp, -// rstudioImage.imageType, -// rstudioImage.imageUrl, -// RuntimeStatus.Running, -// RuntimeUI.Terra, -// None -// ) -// ) shouldBe Some(1) -// // Jupyter on Azure -// test.get( -// RuntimeStatusMetric(CloudProvider.Azure, -// azureImage.imageType, -// azureImage.imageUrl, -// RuntimeStatus.Running, -// RuntimeUI.Terra, -// Some(azureContext) -// ) -// ) shouldBe Some(1) -// // Jupyter on GCP on AoU -// test.get( -// RuntimeStatusMetric(CloudProvider.Gcp, -// jupyterImage.imageType, -// jupyterImage.imageUrl, -// RuntimeStatus.Running, -// RuntimeUI.AoU, -// None -// ) -// ) shouldBe Some(1) -// } -// -//// it should "health check apps" in { -//// val test = -//// leoMetricsMonitor -//// .countAppsByHealth(List(galaxyAppGcp)) -//// .unsafeRunSync()(IORuntime.global) -//// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy -//// test.size shouldBe 12 -//// List("cromwell", "cbas").foreach { s => -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.Cromwell, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// Some(azureContext), -//// s != "cbas", -//// cromwellOnAzureChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.Cromwell, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// Some(azureContext), -//// s == "cbas", -//// cromwellOnAzureChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// } -//// test.get( -//// AppHealthMetric(CloudProvider.Gcp, -//// AppType.Galaxy, -//// ServiceName("galaxy"), -//// RuntimeUI.Terra, -//// None, -//// true, -//// galaxyChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Gcp, -//// AppType.Galaxy, -//// ServiceName("galaxy"), -//// RuntimeUI.Terra, -//// None, -//// false, -//// galaxyChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// List("cromwell-reader", "cbas").foreach { s => -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.WorkflowsApp, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// s != "cbas", -//// workflowsAppChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.WorkflowsApp, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// s == "cbas", -//// workflowsAppChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// } -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.CromwellRunnerApp, -//// ServiceName("cromwell-runner"), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// true, -//// cromwellRunnerAppChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.CromwellRunnerApp, -//// ServiceName("cromwell-runner"), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// false, -//// cromwellRunnerAppChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// } -// -// it should "health check runtimes" in { -// val test = leoMetricsMonitor.countRuntimesByHealth(List(jupyterAzure, rstudioGcp)).unsafeRunSync()(IORuntime.global) -// // An up and a down for jupyter, rstudio, welder * 2 -// test.size shouldBe 8 -// // Jupyter Azure -// List(azureImage, welderImage).foreach { i => -// test.get( -// RuntimeHealthMetric(CloudProvider.Azure, i.imageType, i.imageUrl, RuntimeUI.Terra, Some(azureContext), true) -// ) shouldBe Some(1) -// test.get( -// RuntimeHealthMetric(CloudProvider.Azure, i.imageType, i.imageUrl, RuntimeUI.Terra, Some(azureContext), false) -// ) shouldBe Some(0) -// } -// // RStudio GCP -// List(rstudioImage, welderImage).foreach { i => -// test.get( -// RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, None, i != rstudioImage) -// ) shouldBe Some(1) -// test.get( -// RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, None, i == rstudioImage) -// ) shouldBe Some(0) -// } -// } -// -//// it should "not include AzureCloudContext if disabled" in { -//// val config = LeoMetricsMonitorConfig(true, 1 minute, false) -//// val azureDisabledMetricsMonitor = new LeoMetricsMonitor[IO]( -//// config, -//// appDAO, -//// wdsDAO, -//// cbasDAO, -//// cromwellDAO, -//// hailBatchDAO, -//// relayListenerDAO, -//// samDAO, -//// kube, -//// containerService -//// ) -//// val test = -//// azureDisabledMetricsMonitor -//// .countAppsByHealth(List(cromwellAppAzure, galaxyAppGcp, workflowsApp, cromwellRunnerApp)) -//// .unsafeRunSync()(IORuntime.global) -//// // An up and a down metric for 7 services: 2 cbases, cromwell, cromwell-reader, cromwell-runner, galaxy -//// test.size shouldBe 12 -//// List("cromwell", "cbas").foreach { s => -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.Cromwell, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// None, -//// s != "cbas", -//// cromwellOnAzureChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.Cromwell, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// None, -//// s == "cbas", -//// cromwellOnAzureChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// } -//// test.get( -//// AppHealthMetric(CloudProvider.Gcp, -//// AppType.Galaxy, -//// ServiceName("galaxy"), -//// RuntimeUI.Terra, -//// None, -//// true, -//// galaxyChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Gcp, -//// AppType.Galaxy, -//// ServiceName("galaxy"), -//// RuntimeUI.Terra, -//// None, -//// false, -//// galaxyChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// List("cromwell-reader", "cbas").foreach { s => -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.WorkflowsApp, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// None, -//// s != "cbas", -//// workflowsAppChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.WorkflowsApp, -//// ServiceName(s), -//// RuntimeUI.Terra, -//// None, -//// s == "cbas", -//// workflowsAppChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// } -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.CromwellRunnerApp, -//// ServiceName("cromwell-runner"), -//// RuntimeUI.Terra, -//// None, -//// true, -//// cromwellRunnerAppChart, -//// true -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppHealthMetric(CloudProvider.Azure, -//// AppType.CromwellRunnerApp, -//// ServiceName("cromwell-runner"), -//// RuntimeUI.Terra, -//// None, -//// false, -//// cromwellRunnerAppChart, -//// true -//// ) -//// ) shouldBe Some(0) -//// } -// -//// it should "record nodepool size" in { -//// val test = leoMetricsMonitor.getNodepoolSize(List(wdsAppAzure, hailBatchAppAzure)).unsafeRunSync()(IORuntime.global) -//// test.size shouldBe 4 -//// test.get(NodepoolSizeMetric(azureContext, "pool1")) shouldBe Some(10) -//// test.get(NodepoolSizeMetric(azureContext, "pool2")) shouldBe Some(1) -//// test.get(NodepoolSizeMetric(azureContext2, "pool1")) shouldBe Some(10) -//// test.get(NodepoolSizeMetric(azureContext2, "pool2")) shouldBe Some(1) -//// } -// -//// it should "record app k8s metrics" in { -//// val chart = Chart.fromString("wds-0.0.1").get -//// val test = leoMetricsMonitor.getAppK8sResources(List(wdsAppAzure)).unsafeRunSync()(IORuntime.global) -//// test.size shouldBe 4 -//// test.get( -//// AppResourcesMetric(CloudProvider.Azure, -//// AppType.Wds, -//// ServiceName("wds"), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// "request", -//// "cpu", -//// chart -//// ) -//// ) shouldBe Some(1) -//// test.get( -//// AppResourcesMetric(CloudProvider.Azure, -//// AppType.Wds, -//// ServiceName("wds"), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// "request", -//// "memory", -//// chart -//// ) -//// ) shouldBe Some(1073741824d) -//// test.get( -//// AppResourcesMetric(CloudProvider.Azure, -//// AppType.Wds, -//// ServiceName("wds"), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// "limit", -//// "cpu", -//// chart -//// ) -//// ) shouldBe Some(2) -//// test.get( -//// AppResourcesMetric(CloudProvider.Azure, -//// AppType.Wds, -//// ServiceName("wds"), -//// RuntimeUI.Terra, -//// Some(azureContext2), -//// "limit", -//// "memory", -//// chart -//// ) -//// ) shouldBe Some(2147483648d) -//// } -// -// // Data generators -// -// private def genApp(isAzure: Boolean, -// appType: AppType, -// chart: Chart, -// isAou: Boolean, -// isCromwell: Boolean, -// isWorkflowsApp: Boolean, -// isCromwellRunnerApp: Boolean = false -// ): KubernetesCluster = { -// val cluster = if (isAzure) makeAzureCluster(1) else makeKubeCluster(1) -// val clusterWithAsyncFields = cluster.copy(asyncFields = -// Some( -// KubernetesClusterAsyncFields(IP("1.2.3.4"), -// IP("2.4.5.6"), -// NetworkFields(NetworkName("network"), SubnetworkName("subnet"), IpRange("ipRange")) -// ) -// ) -// ) -// val nodepool = makeNodepool(1, clusterWithAsyncFields.id) -// val app = makeApp(1, nodepool.id).copy( -// appType = appType, -// chart = chart, -// status = AppStatus.Running, -// labels = if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") -// ) -// val services = -// if (isCromwell) List("cbas", "cromwell") -// else if (isCromwellRunnerApp) List("cromwell-runner") -// else if (isWorkflowsApp) List("cbas", "cromwell-reader") -// else List(appType.toString.toLowerCase) -// val appWithServices = app.copy(appResources = app.appResources.copy(services = services.map(genService))) -// clusterWithAsyncFields.copy(nodepools = List(nodepool.copy(apps = List(appWithServices)))) -// } -// -// def genService(name: String): KubernetesService = -// KubernetesService(ServiceId(-1), ServiceConfig(ServiceName(name), KubernetesServiceKindName("ClusterIP"))) -// -// private def cromwellAppGcp: KubernetesCluster = -// genApp(false, AppType.Cromwell, cromwellChart, false, true, false) -// private def galaxyAppGcp: KubernetesCluster = -// genApp(false, AppType.Galaxy, galaxyChart, false, false, false) -// private def customAppGcp: KubernetesCluster = -// genApp(false, AppType.Custom, customChart, false, false, false) -// private def cromwellAppGcpAou: KubernetesCluster = -// genApp(false, AppType.Cromwell, cromwellChart, true, true, false) -// private def rstudioAppGcpAou: KubernetesCluster = -// genApp(false, AppType.Allowed, rstudioChart, true, false, false) -// -// private def cromwellChart = Chart.fromString("cromwell-0.0.1").get -// private def galaxyChart = Chart.fromString("galaxy-0.0.1").get -// private def customChart = Chart.fromString("custom-0.0.1").get -// private def rstudioChart = Chart.fromString("rstudio-0.0.1").get -// -// private def allApps = -// List( -// cromwellAppGcp, -// galaxyAppGcp, -// customAppGcp, -// cromwellAppGcpAou, -// rstudioAppGcpAou -// ) -// -// private def genRuntime(isJupyter: Boolean, isAou: Boolean, isGcp: Boolean): RuntimeMetrics = -// RuntimeMetrics( -// if (isGcp) CloudContext.Gcp(GoogleProject("project")) -// else -// CloudContext.Azure( -// AzureCloudContext( -// TenantId("tenant"), -// SubscriptionId("sub"), -// ManagedResourceGroupName("mrg") -// ) -// ), -// RuntimeName("runtime"), -// RuntimeStatus.Running, -// Some(WorkspaceId(UUID.randomUUID())), -// Set(if (isJupyter) if (isGcp) jupyterImage else azureImage else rstudioImage, welderImage), -// if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") -// ) -// -// private def jupyterGcp: RuntimeMetrics = genRuntime(true, false, true) -// private def rstudioGcp: RuntimeMetrics = genRuntime(false, false, true) -// private def jupyterAzure: RuntimeMetrics = genRuntime(true, false, false) -// private def jupyterGcpAou: RuntimeMetrics = genRuntime(true, true, true) -// -// private val jupyterImage = RuntimeImage(RuntimeImageType.Jupyter, "jupyter:0.0.1", None, Instant.now) -// private val rstudioImage = RuntimeImage(RuntimeImageType.RStudio, "rstudio:0.0.1", None, Instant.now) -// private val welderImage = RuntimeImage(RuntimeImageType.Welder, "welder:0.0.1", None, Instant.now) -// private val azureImage = RuntimeImage(RuntimeImageType.Azure, "azure:0.0.1", None, Instant.now) -// -// private def allRuntimes = List(jupyterGcp, rstudioGcp, jupyterAzure, jupyterGcpAou) -// -// // Mocks -// -// private def setUpMockSamDAO: SamDAO[IO] = { -// val sam = mock[SamDAO[IO]] -// when { -// sam.getCachedArbitraryPetAccessToken(any)(any) -// } thenReturn IO.pure(Some("token")) -// sam -// } -// -// private def setUpMockAppDAO: AppDAO[IO] = { -// val app = mock[AppDAO[IO]] -// when { -// app.isProxyAvailable(any, any[String].asInstanceOf[AppName], any, TraceId(anyString())) -// } thenReturn IO.pure(true) -// app -// } -// -// private def setUpMockJupyterDAO: JupyterDAO[IO] = { -// val jupyter = mock[JupyterDAO[IO]] -// when { -// jupyter.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) -// } thenReturn IO.pure(true) -// jupyter -// } -// -// // RStudio is down -// private def setUpMockRStudioDAO: RStudioDAO[IO] = { -// val rstudio = mock[RStudioDAO[IO]] -// when { -// rstudio.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) -// } thenReturn IO.pure(false) -// rstudio -// } -// -// private def setUpMockWelderDAO: WelderDAO[IO] = { -// val welder = mock[WelderDAO[IO]] -// when { -// welder.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) -// } thenReturn IO.pure(true) -// welder -// } -// -// private def setUpMockKubeDAO: KubernetesAlgebra[IO] = { -// val client = mock[CoreV1Api] -// val podList = mock[V1PodList] -// val pod = mock[V1Pod] -// val mockRequest = mock[CoreV1Api#APIlistNamespacedPodRequest] -// val spec = mock[V1PodSpec] -// val container = mock[V1Container] -// val kube = mock[KubernetesAlgebra[IO]] -// when { -// container.getResources -// } thenReturn new V1ResourceRequirements() -// .requests( -// Map("cpu" -> Quantity.fromString("1"), "memory" -> Quantity.fromString("1073741824")).asJava -// ) -// .limits( -// Map("cpu" -> Quantity.fromString("2"), "memory" -> Quantity.fromString("2147483648")).asJava -// ) -// when { -// spec.getContainers -// } thenReturn List(container).asJava -// when { -// pod.getSpec -// } thenReturn spec -// when { -// pod.getMetadata -// } thenReturn new V1ObjectMeta().labels(Map("leoServiceName" -> "wds").asJava) -// when { -// podList.getItems -// } thenReturn List(pod).asJava -// when { -// client -// .listNamespacedPod(any) -// } thenReturn mockRequest -// when { -// client -// .listNamespacedPod(any) -// .pretty(any) -// } thenReturn mockRequest -// when { -// client -// .listNamespacedPod(any) -// .labelSelector(any) -// } thenReturn mockRequest -// when { -// client -// .listNamespacedPod(any) -// .pretty(any) -// .labelSelector(any) -// .execute() -// } thenReturn podList -// when { -// kube.createAzureClient(any, any[String].asInstanceOf[AKSClusterName])(any) -// } thenReturn IO.pure(client) -// kube -// } -// -// private def setUpMockAzureContainerService: AzureContainerService[IO] = { -// val container = mock[AzureContainerService[IO]] -// val cluster = mock[com.azure.resourcemanager.containerservice.models.KubernetesCluster] -// val pool1 = mock[KubernetesClusterAgentPool] -// when { -// pool1.count() -// } thenReturn 10 -// val pool2 = mock[KubernetesClusterAgentPool] -// when { -// pool2.count() -// } thenReturn 1 -// when { -// cluster.agentPools() -// } thenReturn Map("pool1" -> pool1, "pool2" -> pool2).asJava -// when { -// container.getCluster(any[String].asInstanceOf[AKSClusterName], any)(any) -// } thenReturn IO.pure(cluster) -// container -// } -//} +import cats.effect.IO +import cats.effect.unsafe.IORuntime +import io.kubernetes.client.openapi.models._ +import org.broadinstitute.dsde.workbench.azure._ +import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName +import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} +import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{makeApp, makeKubeCluster, makeNodepool} +import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext +import org.broadinstitute.dsde.workbench.leonardo.config.Config +import org.broadinstitute.dsde.workbench.leonardo.dao._ +import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ +import org.broadinstitute.dsde.workbench.leonardo.{AppName, AppStatus, AppType, Chart, CloudContext, CloudProvider, IpRange, KubernetesCluster, KubernetesClusterAsyncFields, KubernetesService, KubernetesServiceKindName, LeonardoTestSuite, NetworkFields, RuntimeContainerServiceType, RuntimeImage, RuntimeImageType, RuntimeMetrics, RuntimeName, RuntimeStatus, RuntimeUI, ServiceConfig, ServiceId, WorkspaceId} +import org.broadinstitute.dsde.workbench.model.google.GoogleProject +import org.broadinstitute.dsde.workbench.model.{IP, TraceId} +import org.mockito.ArgumentMatchers.{any, anyString} +import org.mockito.Mockito.when +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatestplus.mockito.MockitoSugar + +import java.time.Instant +import java.util.UUID +import scala.concurrent.ExecutionContext +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { + + // Mocks + val appDAO = setUpMockAppDAO + val jupyterDAO = setUpMockJupyterDAO + val rstudioDAO = setUpMockRStudioDAO + val welderDAO = setUpMockWelderDAO + + // Test object + implicit val clusterToolToToolDao: RuntimeContainerServiceType => ToolDAO[IO, RuntimeContainerServiceType] = + ToolDAO.clusterToolToToolDao(jupyterDAO, welderDAO, rstudioDAO) + implicit val ec: ExecutionContext = cats.effect.unsafe.IORuntime.global.compute + val config = LeoMetricsMonitorConfig(true, 1 minute) + val leoMetricsMonitor = new LeoMetricsMonitor[IO]( + config, + appDAO + ) + + "LeoMetricsMonitor" should "count apps by status" in { + val test = leoMetricsMonitor.countAppsByDbStatus(allApps) + // 10 apps + test.size shouldBe 5 + // Cromwell on GCP on Terra + test.get( + AppStatusMetric(CloudProvider.Gcp, + AppType.Cromwell, + AppStatus.Running, + RuntimeUI.Terra, + cromwellChart, + true + ) + ) shouldBe Some(1) + // Galaxy on GCP + test.get( + AppStatusMetric(CloudProvider.Gcp, AppType.Galaxy, AppStatus.Running, RuntimeUI.Terra, galaxyChart, true) + ) shouldBe Some(1) + // Custom app on GCP + test.get( + AppStatusMetric(CloudProvider.Gcp, AppType.Custom, AppStatus.Running, RuntimeUI.Terra, customChart, true) + ) shouldBe Some(1) + // Cromwell on GCP on AoU + test.get( + AppStatusMetric(CloudProvider.Gcp, AppType.Cromwell, AppStatus.Running, RuntimeUI.AoU, cromwellChart, true) + ) shouldBe Some(1) + // RStudio on GCP on AoU + test.get( + AppStatusMetric(CloudProvider.Gcp, AppType.Allowed, AppStatus.Running, RuntimeUI.AoU, rstudioChart, true) + ) shouldBe Some(1) + } + + it should "count runtimes by status" in { + val test = leoMetricsMonitor.countRuntimesByDbStatus(allRuntimes) + // 4 runtimes + test.size shouldBe 4 + // Jupyter on GCP on Terra + test.get( + RuntimeStatusMetric(CloudProvider.Gcp, + jupyterImage.imageType, + jupyterImage.imageUrl, + RuntimeStatus.Running, + RuntimeUI.Terra + ) + ) shouldBe Some(1) + // RStudio on GCP on Terra + test.get( + RuntimeStatusMetric(CloudProvider.Gcp, + rstudioImage.imageType, + rstudioImage.imageUrl, + RuntimeStatus.Running, + RuntimeUI.Terra + ) + ) shouldBe Some(1) + // Jupyter on GCP on AoU + test.get( + RuntimeStatusMetric(CloudProvider.Gcp, + jupyterImage.imageType, + jupyterImage.imageUrl, + RuntimeStatus.Running, + RuntimeUI.AoU + ) + ) shouldBe Some(1) + } + + it should "health check apps" in { + val test = + leoMetricsMonitor + .countAppsByHealth(List(cromwellAppGcp, galaxyAppGcp, cromwellAppGcpAou)) + .unsafeRunSync()(IORuntime.global) + // An up and a down metric for 3 services + test.size shouldBe 3 + test.get( + AppHealthMetric(CloudProvider.Gcp, + AppType.Cromwell, + ServiceName("cromwell"), + RuntimeUI.Terra, + true, + cromwellChart, + true + ) + ) shouldBe Some(1) + test.get( + AppHealthMetric(CloudProvider.Gcp, + AppType.Galaxy, + ServiceName("galaxy"), + RuntimeUI.Terra, + true, + galaxyChart, + true + ) + ) shouldBe Some(1) + test.get( + AppHealthMetric(CloudProvider.Gcp, + AppType.Cromwell, + ServiceName("cromwell"), + RuntimeUI.AoU, + true, + cromwellChart, + true + ) + ) shouldBe Some(1) + + } + + it should "health check runtimes" in { + val test = leoMetricsMonitor.countRuntimesByHealth(List(jupyterGcp, rstudioGcp)).unsafeRunSync()(IORuntime.global) + // An up and a down for jupyter, rstudio, welder * 2 + test.size shouldBe 8 + // Jupyter Azure + List(jupyterImage, welderImage).foreach { i => + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, true) + ) shouldBe Some(1) + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, false) + ) shouldBe Some(0) + } + // RStudio GCP + List(rstudioImage, welderImage).foreach { i => + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, i != rstudioImage) + ) shouldBe Some(1) + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, i == rstudioImage) + ) shouldBe Some(0) + } + } + + // Data generators + + private def genApp(appType: AppType, + chart: Chart, + isAou: Boolean, + isCromwell: Boolean + ): KubernetesCluster = { + val cluster = makeKubeCluster(1) + val clusterWithAsyncFields = cluster.copy(asyncFields = + Some( + KubernetesClusterAsyncFields(IP("1.2.3.4"), + IP("2.4.5.6"), + NetworkFields(NetworkName("network"), SubnetworkName("subnet"), IpRange("ipRange")) + ) + ) + ) + val nodepool = makeNodepool(1, clusterWithAsyncFields.id) + val app = makeApp(1, nodepool.id).copy( + appType = appType, + chart = chart, + status = AppStatus.Running, + labels = if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") + ) + val services = List(appType.toString.toLowerCase) + val appWithServices = app.copy(appResources = app.appResources.copy(services = services.map(genService))) + clusterWithAsyncFields.copy(nodepools = List(nodepool.copy(apps = List(appWithServices)))) + } + + def genService(name: String): KubernetesService = + KubernetesService(ServiceId(-1), ServiceConfig(ServiceName(name), KubernetesServiceKindName("ClusterIP"))) + + private def cromwellAppGcp: KubernetesCluster = + genApp(AppType.Cromwell, cromwellChart, false, true) + private def galaxyAppGcp: KubernetesCluster = + genApp(AppType.Galaxy, galaxyChart, false, false) + private def customAppGcp: KubernetesCluster = + genApp(AppType.Custom, customChart, false, false) + private def cromwellAppGcpAou: KubernetesCluster = + genApp(AppType.Cromwell, cromwellChart, true, true) + private def rstudioAppGcpAou: KubernetesCluster = + genApp(AppType.Allowed, rstudioChart, true, false) + + private def cromwellChart = Chart.fromString("cromwell-0.0.1").get + private def galaxyChart = Chart.fromString("galaxy-0.0.1").get + private def customChart = Chart.fromString("custom-0.0.1").get + private def rstudioChart = Chart.fromString("rstudio-0.0.1").get + + private def allApps = + List( + cromwellAppGcp, + galaxyAppGcp, + customAppGcp, + cromwellAppGcpAou, + rstudioAppGcpAou + ) + + private def genRuntime(isJupyter: Boolean, isAou: Boolean, isGcp: Boolean): RuntimeMetrics = + RuntimeMetrics( + CloudContext.Gcp(GoogleProject("project")), + RuntimeName("runtime"), + RuntimeStatus.Running, + Some(WorkspaceId(UUID.randomUUID())), + Set(if (isJupyter) jupyterImage else rstudioImage, welderImage), + if (isAou) Map(Config.uiConfig.allOfUsLabel -> "true") else Map(Config.uiConfig.terraLabel -> "true") + ) + + private def jupyterGcp: RuntimeMetrics = genRuntime(true, false, true) + private def rstudioGcp: RuntimeMetrics = genRuntime(false, false, true) + private def jupyterGcpAou: RuntimeMetrics = genRuntime(true, true, true) + + private val jupyterImage = RuntimeImage(RuntimeImageType.Jupyter, "jupyter:0.0.1", None, Instant.now) + private val rstudioImage = RuntimeImage(RuntimeImageType.RStudio, "rstudio:0.0.1", None, Instant.now) + private val welderImage = RuntimeImage(RuntimeImageType.Welder, "welder:0.0.1", None, Instant.now) + + private def allRuntimes = List(jupyterGcp, rstudioGcp, jupyterGcpAou) + + // Mocks + + private def setUpMockAppDAO: AppDAO[IO] = { + val app = mock[AppDAO[IO]] + when { + app.isProxyAvailable(any, any[String].asInstanceOf[AppName], any, TraceId(anyString())) + } thenReturn IO.pure(true) + app + } + + private def setUpMockJupyterDAO: JupyterDAO[IO] = { + val jupyter = mock[JupyterDAO[IO]] + when { + jupyter.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) + } thenReturn IO.pure(true) + jupyter + } + + // RStudio is down + private def setUpMockRStudioDAO: RStudioDAO[IO] = { + val rstudio = mock[RStudioDAO[IO]] + when { + rstudio.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) + } thenReturn IO.pure(false) + rstudio + } + + private def setUpMockWelderDAO: WelderDAO[IO] = { + val welder = mock[WelderDAO[IO]] + when { + welder.isProxyAvailable(any, any[String].asInstanceOf[RuntimeName]) + } thenReturn IO.pure(true) + welder + } +} From af0313d46827e1a8660ad1df0bc08c8cef61a583 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 8 Jul 2025 09:38:52 -0400 Subject: [PATCH 24/43] scalafmt --- .../dsde/workbench/leonardo/azureModels.scala | 2 - .../leonardo/config/KubernetesAppConfig.scala | 10 ++-- .../http/AppDependenciesBuilder.scala | 15 +++++- .../http/BaselineDependenciesBuilder.scala | 5 +- .../leonardo/http/api/AppRoutes.scala | 32 ++++++------ .../http/service/LeoAppServiceInterp.scala | 17 +++++- .../leonardo/monitor/LeoMetricsMonitor.scala | 13 ++--- .../monitor/LeoPubsubMessageSubscriber.scala | 5 +- .../leoPubsubMessageSubscriberModels.scala | 52 +++++++++---------- .../workbench/leonardo/CommonTestData.scala | 26 ++++++++-- .../http/service/AppServiceInterpSpec.scala | 6 ++- .../monitor/LeoMetricsMonitorSpec.scala | 42 ++++++++++----- .../LeoPubsubMessageSubscriberSpec.scala | 18 ++++++- .../leonardo/monitor/MonitorAtBootSpec.scala | 5 +- 14 files changed, 154 insertions(+), 94 deletions(-) diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala index 452b19004e..0a02af1f21 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureModels.scala @@ -7,5 +7,3 @@ final case class WsmControlledResourceId(value: UUID) extends AnyVal final case class AzureUnimplementedException(message: String) extends Exception { override def getMessage: String = message } - - diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala index 9a46cfe94d..fe1cbf365e 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/config/KubernetesAppConfig.scala @@ -35,11 +35,11 @@ sealed trait KubernetesAppConfig extends Product with Serializable { object KubernetesAppConfig { def configForTypeAndCloud(appType: AppType, cloudProvider: CloudProvider): Option[KubernetesAppConfig] = (appType, cloudProvider) match { - case (Galaxy, CloudProvider.Gcp) => Some(Config.gkeGalaxyAppConfig) - case (Custom, CloudProvider.Gcp) => Some(Config.gkeCustomAppConfig) - case (Cromwell, CloudProvider.Gcp) => Some(Config.gkeCromwellAppConfig) - case (AppType.Allowed, CloudProvider.Gcp) => Some(Config.gkeAllowedAppConfig) - case _ => None + case (Galaxy, CloudProvider.Gcp) => Some(Config.gkeGalaxyAppConfig) + case (Custom, CloudProvider.Gcp) => Some(Config.gkeCustomAppConfig) + case (Cromwell, CloudProvider.Gcp) => Some(Config.gkeCromwellAppConfig) + case (AppType.Allowed, CloudProvider.Gcp) => Some(Config.gkeAllowedAppConfig) + case _ => None } } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index 5a3ba49902..d3e55be718 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -5,7 +5,20 @@ import cats.effect.std.Semaphore import cats.effect.{IO, Resource} import fs2.Stream import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor -import org.broadinstitute.dsde.workbench.leonardo.config.Config.{applicationConfig, asyncTaskProcessorConfig, autoFreezeConfig, autodeleteConfig, contentSecurityPolicy, dateAccessUpdaterConfig, dbConcurrency, leoExecutionModeConfig, leoPubsubMessageSubscriberConfig, liquibaseConfig, prometheusConfig, refererConfig} +import org.broadinstitute.dsde.workbench.leonardo.config.Config.{ + applicationConfig, + asyncTaskProcessorConfig, + autoFreezeConfig, + autodeleteConfig, + contentSecurityPolicy, + dateAccessUpdaterConfig, + dbConcurrency, + leoExecutionModeConfig, + leoPubsubMessageSubscriberConfig, + liquibaseConfig, + prometheusConfig, + refererConfig +} import org.broadinstitute.dsde.workbench.leonardo.config.LeoExecutionModeConfig import org.broadinstitute.dsde.workbench.leonardo.dao.ToolDAO import org.broadinstitute.dsde.workbench.leonardo.db.DbReference diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index 633f7d3339..e694dbb1ad 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -36,10 +36,7 @@ import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{HttpSamApiClientProvider, SamService, SamServiceInterp} import org.broadinstitute.dsde.workbench.leonardo.db.DbReference import org.broadinstitute.dsde.workbench.leonardo.dns._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.{ - RuntimeServiceConfig, - SamResourceCacheKey -} +import org.broadinstitute.dsde.workbench.leonardo.http.service.{RuntimeServiceConfig, SamResourceCacheKey} import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubCodec.leoPubsubMessageDecoder import org.broadinstitute.dsde.workbench.leonardo.monitor.{LeoPubsubMessage, UpdateDateAccessedMessage} import org.broadinstitute.dsde.workbench.leonardo.util._ diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala index c7d939a640..6d44f0e2f3 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/AppRoutes.scala @@ -308,22 +308,22 @@ object AppRoutes { "autodeleteThreshold" )(x => (x.workspaceId, - x.cloudContext, - x.region, - x.kubernetesRuntimeConfig, - x.autopilot, - x.errors, - x.status, - x.proxyUrls, - x.appName, - x.appType, - x.chartName, - x.diskName, - x.auditInfo, - x.accessScope, - x.labels, - x.autodeleteEnabled, - x.autodeleteThreshold + x.cloudContext, + x.region, + x.kubernetesRuntimeConfig, + x.autopilot, + x.errors, + x.status, + x.proxyUrls, + x.appName, + x.appType, + x.chartName, + x.diskName, + x.auditInfo, + x.accessScope, + x.labels, + x.autodeleteEnabled, + x.autodeleteThreshold ) ) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala index 7b678ae178..336fdbf5ce 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/LeoAppServiceInterp.scala @@ -13,7 +13,16 @@ import monocle.macros.syntax.lens._ import org.apache.commons.lang3.RandomStringUtils import org.broadinstitute.dsde.workbench.google2.GKEModels.{KubernetesClusterName, NodepoolName} import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.NamespaceName -import org.broadinstitute.dsde.workbench.google2.{DiskName, GoogleComputeService, GoogleResourceService, KubernetesName, Location, MachineTypeName, RegionName, ZoneName} +import org.broadinstitute.dsde.workbench.google2.{ + DiskName, + GoogleComputeService, + GoogleResourceService, + KubernetesName, + Location, + MachineTypeName, + RegionName, + ZoneName +} import org.broadinstitute.dsde.workbench.leonardo.AppRestore.GalaxyRestore import org.broadinstitute.dsde.workbench.leonardo.AppType._ import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ @@ -22,7 +31,11 @@ import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService import org.broadinstitute.dsde.workbench.leonardo.db.DBIOInstances.dbioInstance import org.broadinstitute.dsde.workbench.leonardo.db._ -import org.broadinstitute.dsde.workbench.leonardo.http.service.LeoAppServiceInterp.{checkIfCanBeDeleted, getAppSamPolicyMap, isPatchVersionDifference} +import org.broadinstitute.dsde.workbench.leonardo.http.service.LeoAppServiceInterp.{ + checkIfCanBeDeleted, + getAppSamPolicyMap, + isPatchVersionDifference +} import org.broadinstitute.dsde.workbench.leonardo.model.SamResourceAction._ import org.broadinstitute.dsde.workbench.leonardo.model._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage._ diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala index d70d873e4c..c61cc3e8cc 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitor.scala @@ -9,7 +9,7 @@ import fs2.Stream import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName import org.broadinstitute.dsde.workbench.leonardo.config.{Config, KubernetesAppConfig} import org.broadinstitute.dsde.workbench.leonardo.dao._ -import org.broadinstitute.dsde.workbench.leonardo.db.{DbReference, KubernetesServiceDbQueries, clusterQuery} +import org.broadinstitute.dsde.workbench.leonardo.db.{clusterQuery, DbReference, KubernetesServiceDbQueries} import org.broadinstitute.dsde.workbench.leonardo.http.dbioToIO import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ import org.broadinstitute.dsde.workbench.model.TraceId @@ -20,9 +20,7 @@ import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration /** Collects metrics about active Leo runtimes and apps. */ -class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, - appDAO: AppDAO[F] -)(implicit +class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, appDAO: AppDAO[F])(implicit F: Async[F], dbRef: DbReference[F], metrics: OpenTelemetryMetrics[F], @@ -108,12 +106,7 @@ class LeoMetricsMonitor[F[_]](config: LeoMetricsMonitorConfig, imageTypes = Set(RuntimeImageType.Jupyter, RuntimeImageType.RStudio) c <- r.images.filter(i => imageTypes.contains(i.imageType)).headOption } yield Map( - RuntimeStatusMetric(r.cloudContext.cloudProvider, - c.imageType, - c.imageUrl, - r.status, - getRuntimeUI(r.labels) - ) -> 1d + RuntimeStatusMetric(r.cloudContext.cloudProvider, c.imageType, c.imageUrl, r.status, getRuntimeUI(r.labels)) -> 1d ) allContainers.combineAll } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala index 420e728445..828018bcce 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriber.scala @@ -458,10 +458,7 @@ class LeoPubsubMessageSubscriber[F[_]]( ctx.traceId, runtimeConfig.cloudService.process(msg.runtimeId, RuntimeStatus.Starting, None).compile.drain, Some( - handleRuntimeMessageError(msg.runtimeId, - ctx.now, - s"starting runtime ${runtime.projectNameString} failed" - ) + handleRuntimeMessageError(msg.runtimeId, ctx.now, s"starting runtime ${runtime.projectNameString} failed") ), ctx.now, TaskMetricsTags("startRuntime", None, Some(isAoU), CloudProvider.Gcp, Some(runtimeConfig.cloudService)) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala index ce07b98904..2ca7aac745 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala @@ -509,19 +509,19 @@ object LeoPubsubCodec { for { messageType <- message.downField("messageType").as[LeoPubsubMessageType] value <- messageType match { - case LeoPubsubMessageType.CreateDisk => message.as[CreateDiskMessage] - case LeoPubsubMessageType.UpdateDisk => message.as[UpdateDiskMessage] - case LeoPubsubMessageType.DeleteDisk => message.as[DeleteDiskMessage] - case LeoPubsubMessageType.CreateRuntime => message.as[CreateRuntimeMessage] - case LeoPubsubMessageType.DeleteRuntime => message.as[DeleteRuntimeMessage] - case LeoPubsubMessageType.StopRuntime => message.as[StopRuntimeMessage] - case LeoPubsubMessageType.StartRuntime => message.as[StartRuntimeMessage] - case LeoPubsubMessageType.UpdateRuntime => message.as[UpdateRuntimeMessage] - case LeoPubsubMessageType.CreateApp => message.as[CreateAppMessage] - case LeoPubsubMessageType.DeleteApp => message.as[DeleteAppMessage] - case LeoPubsubMessageType.StopApp => message.as[StopAppMessage] - case LeoPubsubMessageType.StartApp => message.as[StartAppMessage] - case LeoPubsubMessageType.UpdateApp => message.as[UpdateAppMessage] + case LeoPubsubMessageType.CreateDisk => message.as[CreateDiskMessage] + case LeoPubsubMessageType.UpdateDisk => message.as[UpdateDiskMessage] + case LeoPubsubMessageType.DeleteDisk => message.as[DeleteDiskMessage] + case LeoPubsubMessageType.CreateRuntime => message.as[CreateRuntimeMessage] + case LeoPubsubMessageType.DeleteRuntime => message.as[DeleteRuntimeMessage] + case LeoPubsubMessageType.StopRuntime => message.as[StopRuntimeMessage] + case LeoPubsubMessageType.StartRuntime => message.as[StartRuntimeMessage] + case LeoPubsubMessageType.UpdateRuntime => message.as[UpdateRuntimeMessage] + case LeoPubsubMessageType.CreateApp => message.as[CreateAppMessage] + case LeoPubsubMessageType.DeleteApp => message.as[DeleteAppMessage] + case LeoPubsubMessageType.StopApp => message.as[StopAppMessage] + case LeoPubsubMessageType.StartApp => message.as[StartAppMessage] + case LeoPubsubMessageType.UpdateApp => message.as[UpdateAppMessage] } } yield value @@ -853,19 +853,19 @@ object LeoPubsubCodec { )(x => (x.messageType, x.jobId, x.appId, x.appName, x.cloudContext, x.workspaceId, x.googleProject, x.traceId)) implicit val leoPubsubMessageEncoder: Encoder[LeoPubsubMessage] = Encoder.instance { - case m: CreateDiskMessage => m.asJson - case m: UpdateDiskMessage => m.asJson - case m: DeleteDiskMessage => m.asJson - case m: CreateRuntimeMessage => m.asJson - case m: DeleteRuntimeMessage => m.asJson - case m: StopRuntimeMessage => m.asJson - case m: StartRuntimeMessage => m.asJson - case m: UpdateRuntimeMessage => m.asJson - case m: CreateAppMessage => m.asJson - case m: DeleteAppMessage => m.asJson - case m: StopAppMessage => m.asJson - case m: StartAppMessage => m.asJson - case m: UpdateAppMessage => m.asJson + case m: CreateDiskMessage => m.asJson + case m: UpdateDiskMessage => m.asJson + case m: DeleteDiskMessage => m.asJson + case m: CreateRuntimeMessage => m.asJson + case m: DeleteRuntimeMessage => m.asJson + case m: StopRuntimeMessage => m.asJson + case m: StartRuntimeMessage => m.asJson + case m: UpdateRuntimeMessage => m.asJson + case m: CreateAppMessage => m.asJson + case m: DeleteAppMessage => m.asJson + case m: StopAppMessage => m.asJson + case m: StartAppMessage => m.asJson + case m: UpdateAppMessage => m.asJson } } diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index 7165da91e7..86a56818f7 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -14,16 +14,36 @@ import com.typesafe.config.ConfigFactory import net.ceedubs.ficus.Ficus._ import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.mock.BaseFakeGoogleStorage -import org.broadinstitute.dsde.workbench.google2.{DataprocRole, DiskName, MachineTypeName, NetworkName, OperationName, RegionName, SubnetworkName, ZoneName} +import org.broadinstitute.dsde.workbench.google2.{ + DataprocRole, + DiskName, + MachineTypeName, + NetworkName, + OperationName, + RegionName, + SubnetworkName, + ZoneName +} import org.broadinstitute.dsde.workbench.leonardo import org.broadinstitute.dsde.workbench.leonardo.ContainerRegistry.DockerHub -import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{BootSource, CryptoDetector, Jupyter, Proxy, RStudio, Welder} +import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{ + BootSource, + CryptoDetector, + Jupyter, + Proxy, + RStudio, + Welder +} import org.broadinstitute.dsde.workbench.leonardo.SamResourceId._ import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.dao.MockSamDAO import org.broadinstitute.dsde.workbench.leonardo.db.ClusterRecord -import org.broadinstitute.dsde.workbench.leonardo.http.{CreateRuntimeRequest, RuntimeConfigRequest, userScriptStartupOutputUriMetadataKey} +import org.broadinstitute.dsde.workbench.leonardo.http.{ + userScriptStartupOutputUriMetadataKey, + CreateRuntimeRequest, + RuntimeConfigRequest +} import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google._ import org.broadinstitute.dsde.workbench.oauth2.mock.FakeOpenIDConnectConfiguration diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala index e2cf5495f0..87306c6818 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/AppServiceInterpSpec.scala @@ -22,7 +22,11 @@ import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.model._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, DeleteAppMessage} -import org.broadinstitute.dsde.workbench.leonardo.monitor.{ClusterNodepoolAction, LeoPubsubMessage, LeoPubsubMessageType} +import org.broadinstitute.dsde.workbench.leonardo.monitor.{ + ClusterNodepoolAction, + LeoPubsubMessage, + LeoPubsubMessageType +} import org.broadinstitute.dsde.workbench.leonardo.util.QueueFactory import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index 2c5ff552fe..89ab71a528 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -12,7 +12,31 @@ import org.broadinstitute.dsde.workbench.leonardo.config.Config import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ -import org.broadinstitute.dsde.workbench.leonardo.{AppName, AppStatus, AppType, Chart, CloudContext, CloudProvider, IpRange, KubernetesCluster, KubernetesClusterAsyncFields, KubernetesService, KubernetesServiceKindName, LeonardoTestSuite, NetworkFields, RuntimeContainerServiceType, RuntimeImage, RuntimeImageType, RuntimeMetrics, RuntimeName, RuntimeStatus, RuntimeUI, ServiceConfig, ServiceId, WorkspaceId} +import org.broadinstitute.dsde.workbench.leonardo.{ + AppName, + AppStatus, + AppType, + Chart, + CloudContext, + CloudProvider, + IpRange, + KubernetesCluster, + KubernetesClusterAsyncFields, + KubernetesService, + KubernetesServiceKindName, + LeonardoTestSuite, + NetworkFields, + RuntimeContainerServiceType, + RuntimeImage, + RuntimeImageType, + RuntimeMetrics, + RuntimeName, + RuntimeStatus, + RuntimeUI, + ServiceConfig, + ServiceId, + WorkspaceId +} import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{IP, TraceId} import org.mockito.ArgumentMatchers.{any, anyString} @@ -50,13 +74,7 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test test.size shouldBe 5 // Cromwell on GCP on Terra test.get( - AppStatusMetric(CloudProvider.Gcp, - AppType.Cromwell, - AppStatus.Running, - RuntimeUI.Terra, - cromwellChart, - true - ) + AppStatusMetric(CloudProvider.Gcp, AppType.Cromwell, AppStatus.Running, RuntimeUI.Terra, cromwellChart, true) ) shouldBe Some(1) // Galaxy on GCP test.get( @@ -172,14 +190,10 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test ) shouldBe Some(0) } } - + // Data generators - private def genApp(appType: AppType, - chart: Chart, - isAou: Boolean, - isCromwell: Boolean - ): KubernetesCluster = { + private def genApp(appType: AppType, chart: Chart, isAou: Boolean, isCromwell: Boolean): KubernetesCluster = { val cluster = makeKubeCluster(1) val clusterWithAsyncFields = cluster.copy(asyncFields = Some( diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala index aa3fb404b4..dea620e938 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubMessageSubscriberSpec.scala @@ -18,11 +18,25 @@ import org.broadinstitute.dsde.workbench.google.mock._ import org.broadinstitute.dsde.workbench.google2.KubernetesModels.PodStatus import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceAccountName import org.broadinstitute.dsde.workbench.google2.mock.{MockKubernetesService => _, _} -import org.broadinstitute.dsde.workbench.google2.{DiskName, GKEModels, GoogleDiskService, GoogleStorageService, KubernetesModels, MachineTypeName, RegionName, ZoneName} +import org.broadinstitute.dsde.workbench.google2.{ + DiskName, + GKEModels, + GoogleDiskService, + GoogleStorageService, + KubernetesModels, + MachineTypeName, + RegionName, + ZoneName +} import org.broadinstitute.dsde.workbench.leonardo.AppRestore.GalaxyRestore import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor.Task import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{makeApp, makeKubeCluster, makeNodepool, makeService} +import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{ + makeApp, + makeKubeCluster, + makeNodepool, + makeService +} import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.BootSource import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext import org.broadinstitute.dsde.workbench.leonardo.config.Config diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala index 537e683f57..e810d3240b 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/MonitorAtBootSpec.scala @@ -13,10 +13,7 @@ import org.broadinstitute.dsde.workbench.leonardo.monitor.ClusterNodepoolAction. CreateClusterAndNodepool, CreateNodepool } -import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{ - CreateAppMessage, - DeleteAppMessage -} +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, DeleteAppMessage} import org.broadinstitute.dsde.workbench.leonardo.{ AppMachineType, AppStatus, From a9b389a895553ca63c4285ca76e63b9314cb4602 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 8 Jul 2025 11:58:27 -0400 Subject: [PATCH 25/43] Get tests passing --- .../leonardo/http/api/HttpRoutesSpec.scala | 218 ------------------ .../monitor/LeoMetricsMonitorSpec.scala | 74 +++--- 2 files changed, 28 insertions(+), 264 deletions(-) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala index 352f6f1c0b..9f9e8ece7f 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala @@ -433,124 +433,6 @@ class HttpRoutesSpec } } - "RuntimeRoutesV2" should "create azure runtime" in { - Post(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/azureruntime1") - .withEntity(ContentTypes.`application/json`, - defaultCreateAzureRuntimeReq.asJson.spaces2 - ) ~> routes.route ~> check { - status shouldEqual StatusCodes.Accepted - validateRawCookie(header("Set-Cookie")) - } - } - - it should "reject azure runtime with invalid workspaceId" in { - Post(s"/api/v2/runtimes/invalidWorkspaceId/azure/azureruntime1") - .withEntity(ContentTypes.`application/json`, - defaultCreateAzureRuntimeReq.asJson.spaces2 - ) ~> routes.route ~> check { - status shouldEqual StatusCodes.BadRequest - responseEntity.toStrict(5 seconds).futureValue.data.utf8String should include( - "Invalid workspace id invalidWorkspaceId, workspace id must be a valid UUID" - ) - } - } - - it should "reject azure runtime with invalid runtimeName" in { - Post(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/invalidRuntime") - .withEntity(ContentTypes.`application/json`, - defaultCreateAzureRuntimeReq.asJson.spaces2 - ) ~> routes.route ~> check { - status shouldEqual StatusCodes.BadRequest - responseEntity.toStrict(5 seconds).futureValue.data.utf8String should include( - "Invalid runtime name invalidRuntime" - ) - } - } - - it should "get an azure runtime" in { - Get(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/azureruntime1") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - responseAs[GetRuntimeResponse].clusterName shouldBe RuntimeName("azureruntime1") - validateRawCookie(header("Set-Cookie")) - } - } - - it should "404 on get azure runtime when it does not exist" in { - Get(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/fakeruntime") ~> httpRoutes.route ~> check { - status shouldEqual StatusCodes.NotFound - } - } - - it should "delete an azure runtime" in { - Delete(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/azureruntime1") ~> routes.route ~> check { - status shouldEqual StatusCodes.Accepted - } - } - - it should "404 on delete azure runtime when it does not exist" in { - Delete(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/azureruntime1") ~> httpRoutes.route ~> check { - status shouldEqual StatusCodes.NotFound - } - } - - it should "list runtimes v2 with a workspace" in { - Get(s"/api/v2/runtimes/${workspaceId.value.toString}") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - responseAs[Vector[ListRuntimeResponse2]].map(_.clusterName) shouldBe Vector(RuntimeName("azureruntime1")) - validateRawCookie(header("Set-Cookie")) - } - } - - it should "list runtimes v2 without a workspace or cloudContext" in { - Get("/api/v2/runtimes") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - val response = responseAs[Vector[ListRuntimeResponse2]] - response.map(_.clusterName) shouldBe Vector(RuntimeName("azureruntime1")) - validateRawCookie(header("Set-Cookie")) - } - } - - it should "list runtimes v2 with a workspace and cloud context" in { - Get(s"/api/v2/runtimes/${workspaceId.value.toString}/azure") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - val response = responseAs[Vector[ListRuntimeResponse2]] - response.map(_.clusterName) shouldBe Vector(RuntimeName("azureruntime1")) - validateRawCookie(header("Set-Cookie")) - } - } - - // Tests only parameter parsing, not service logic. - it should "list runtimes v2 with labels" in isolatedDbTest { - Get( - s"/api/v2/runtimes/${workspaceId.value.toString}/azure?foo=bar" - ) ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - - val responseClusters = responseAs[List[ListRuntimeResponse2]] - responseClusters should have size 1 - validateRawCookie(header("Set-Cookie")) - } - - Get(s"/api/v2/runtimes/${workspaceId.value.toString}/azure?_labels=foo%3Dbar") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - - val responseClusters = responseAs[List[ListRuntimeResponse2]] - responseClusters should have size 1 - } - - Get(s"/api/v2/runtimes/${workspaceId.value.toString}/azure?_labels=bad") ~> httpRoutes.route ~> check { - status shouldEqual StatusCodes.BadRequest - } - } - - it should "list runtimes v2 with parameters" in { - Get(s"/api/v2/runtimes/${workspaceId.value.toString}/azure?project=foo&creator=bar") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - responseAs[Vector[ListRuntimeResponse2]].map(_.clusterName) shouldBe Vector(RuntimeName("azureruntime1")) - validateRawCookie(header("Set-Cookie")) - } - } - "DiskRoutes" should "create a disk" in { val diskCreateRequest = CreateDiskRequest( Map("foo" -> "bar"), @@ -625,30 +507,6 @@ class HttpRoutesSpec } } - it should "get a disk v2" in { - Get(s"/api/v2/disks/-1") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - responseAs[GetPersistentDiskV2Response].name shouldBe CommonTestData.diskName - validateRawCookie(header("Set-Cookie")) - } - } - - it should "delete a disk v2" in { - Delete(s"/api/v2/disks/-1") ~> routes.route ~> check { - status shouldEqual StatusCodes.Accepted - validateRawCookie(header("Set-Cookie")) - } - } - - it should "reject get a disk if id is invalid" in { - Get(s"/api/v2/disks/diskid") ~> routes.route ~> check { - val expectedResponse = - """Invalid workspace id workspaceId, workspace id must be a valid UUID""" - responseEntity.toStrict(5 seconds).futureValue.data.utf8String.contains(expectedResponse) - status shouldEqual StatusCodes.InternalServerError - } - } - "HttpRoutes" should "not handle unrecognized routes" in { Post("/api/google/v1/runtime/googleProject1/runtime1/unhandled") ~> routes.route ~> check { status shouldBe StatusCodes.NotFound @@ -660,11 +518,6 @@ class HttpRoutesSpec val resp = responseEntity.toStrict(5 seconds).futureValue.data.utf8String resp shouldBe "\"API not found. Make sure you're calling the correct endpoint with correct method\"" } - Get("/api/google/v2/runtime/googleProject1/runtime1") ~> routes.route ~> check { - status shouldBe StatusCodes.NotFound - val resp = responseEntity.toStrict(5 seconds).futureValue.data.utf8String - resp shouldBe "\"API not found. Make sure you're calling the correct endpoint with correct method\"" - } Post("/api/google/v1/runtime/googleProject1/runtime1") ~> routes.route ~> check { status shouldBe StatusCodes.NotFound val resp = responseEntity.toStrict(5 seconds).futureValue.data.utf8String @@ -680,37 +533,17 @@ class HttpRoutesSpec val resp = responseEntity.toStrict(5 seconds).futureValue.data.utf8String resp shouldBe "\"API not found. Make sure you're calling the correct endpoint with correct method\"" } - Get(s"/api/disks/v2/${workspaceId.value.toString}/disk1") ~> routes.route ~> check { - status shouldBe StatusCodes.NotFound - val resp = responseEntity.toStrict(5 seconds).futureValue.data.utf8String - resp shouldBe "\"API not found. Make sure you're calling the correct endpoint with correct method\"" - } } it should "have expected azure routes when azure hosting mode is true" in { val adminRoute = "/api/admin/v2/apps/update" - val appsV2Route = s"/api/apps/v2/${workspaceId.value.toString}/app1" - val diskV2Route = "/api/v2/disks/-1" - val runtimeV2Route = "/api/v2/runtimes" val statusRoute = "/status" Get(adminRoute) ~> httpRoutesAzureOnly.route ~> check { status should not be StatusCodes.NotFound } - Get(appsV2Route) ~> httpRoutesAzureOnly.route ~> check { - status should not be StatusCodes.NotFound - } - - Get(diskV2Route) ~> httpRoutesAzureOnly.route ~> check { - status should not be StatusCodes.NotFound - } - - Get(runtimeV2Route) ~> httpRoutesAzureOnly.route ~> check { - status should not be StatusCodes.NotFound - } - Get(statusRoute) ~> httpRoutesAzureOnly.route ~> check { status should not be StatusCodes.NotFound } @@ -799,57 +632,6 @@ class HttpRoutesSpec } } - it should "create an app V2" in { - Post(s"/api/apps/v2/${workspaceId.value.toString}/app1") - .withEntity(ContentTypes.`application/json`, - createAppRequest.copy(accessScope = Some(AppAccessScope.WorkspaceShared)).asJson.spaces2 - ) ~> routes.route ~> check { - status shouldEqual StatusCodes.Accepted - validateRawCookie(header("Set-Cookie")) - } - } - - it should "list apps v2 with project" in { - Get(s"/api/apps/v2/${workspaceId.value.toString}") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - validateRawCookie(header("Set-Cookie")) - val response = responseAs[Vector[ListAppResponse]] - response shouldBe listAppResponse - } - } - - it should "get app V2" in { - Get(s"/api/apps/v2/${workspaceId.value.toString}/app1") ~> routes.route ~> check { - status shouldEqual StatusCodes.OK - validateRawCookie(header("Set-Cookie")) - responseAs[GetAppResponse] shouldBe getAppResponse - } - } - - it should "delete app V2" in { - Delete(s"/api/apps/v2/${workspaceId.value.toString}/app1") ~> routes.route ~> check { - status shouldEqual StatusCodes.Accepted - validateRawCookie(header("Set-Cookie")) - } - } - - it should "validate create appV2 request" in { - Post(s"/api/apps/v2/${workspaceId.value.toString}/app1") - .withEntity( - ContentTypes.`application/json`, - createAppRequest - .copy(kubernetesRuntimeConfig = - createAppRequest.kubernetesRuntimeConfig.map(c => c.copy(numNodes = NumNodes(-1))) - ) - .asJson - .spaces2 - ) ~> httpRoutes.route ~> check { - status shouldBe StatusCodes.BadRequest - val resp = responseEntity.toStrict(5 seconds).futureValue.data.utf8String - resp shouldBe "The request content was malformed:\nDecodingFailure at .kubernetesRuntimeConfig.numNodes: Minimum number of nodes is 1" - } - } - it should "run a basic app update request" in { Post(s"/api/admin/v2/apps/update") .withEntity( diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index 89ab71a528..7f0e8cb1f9 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -12,31 +12,7 @@ import org.broadinstitute.dsde.workbench.leonardo.config.Config import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ -import org.broadinstitute.dsde.workbench.leonardo.{ - AppName, - AppStatus, - AppType, - Chart, - CloudContext, - CloudProvider, - IpRange, - KubernetesCluster, - KubernetesClusterAsyncFields, - KubernetesService, - KubernetesServiceKindName, - LeonardoTestSuite, - NetworkFields, - RuntimeContainerServiceType, - RuntimeImage, - RuntimeImageType, - RuntimeMetrics, - RuntimeName, - RuntimeStatus, - RuntimeUI, - ServiceConfig, - ServiceId, - WorkspaceId -} +import org.broadinstitute.dsde.workbench.leonardo.{AppName, AppStatus, AppType, Chart, CloudContext, CloudProvider, IpRange, KubernetesCluster, KubernetesClusterAsyncFields, KubernetesService, KubernetesServiceKindName, LeonardoTestSuite, NetworkFields, RuntimeContainerServiceType, RuntimeImage, RuntimeImageType, RuntimeMetrics, RuntimeName, RuntimeStatus, RuntimeUI, ServiceConfig, ServiceId, WorkspaceId} import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{IP, TraceId} import org.mockito.ArgumentMatchers.{any, anyString} @@ -97,7 +73,7 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test it should "count runtimes by status" in { val test = leoMetricsMonitor.countRuntimesByDbStatus(allRuntimes) // 4 runtimes - test.size shouldBe 4 + test.size shouldBe 3 // Jupyter on GCP on Terra test.get( RuntimeStatusMetric(CloudProvider.Gcp, @@ -133,7 +109,7 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test .countAppsByHealth(List(cromwellAppGcp, galaxyAppGcp, cromwellAppGcpAou)) .unsafeRunSync()(IORuntime.global) // An up and a down metric for 3 services - test.size shouldBe 3 + test.size shouldBe 6 test.get( AppHealthMetric(CloudProvider.Gcp, AppType.Cromwell, @@ -170,25 +146,31 @@ class LeoMetricsMonitorSpec extends AnyFlatSpec with LeonardoTestSuite with Test it should "health check runtimes" in { val test = leoMetricsMonitor.countRuntimesByHealth(List(jupyterGcp, rstudioGcp)).unsafeRunSync()(IORuntime.global) // An up and a down for jupyter, rstudio, welder * 2 - test.size shouldBe 8 - // Jupyter Azure - List(jupyterImage, welderImage).foreach { i => - test.get( - RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, true) - ) shouldBe Some(1) - test.get( - RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, false) - ) shouldBe Some(0) - } - // RStudio GCP - List(rstudioImage, welderImage).foreach { i => - test.get( - RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, i != rstudioImage) - ) shouldBe Some(1) - test.get( - RuntimeHealthMetric(CloudProvider.Gcp, i.imageType, i.imageUrl, RuntimeUI.Terra, i == rstudioImage) - ) shouldBe Some(0) - } + test.size shouldBe 6 + + // Jupyter + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, jupyterImage.imageType, jupyterImage.imageUrl, RuntimeUI.Terra, true) + ) shouldBe Some(1) + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, jupyterImage.imageType, jupyterImage.imageUrl, RuntimeUI.Terra, false) + ) shouldBe Some(0) + + // Rstudio + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, rstudioImage.imageType, rstudioImage.imageUrl, RuntimeUI.Terra, true) + ) shouldBe Some(0) + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, rstudioImage.imageType, rstudioImage.imageUrl, RuntimeUI.Terra, false) + ) shouldBe Some(1) + + // Welder + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, welderImage.imageType, welderImage.imageUrl, RuntimeUI.Terra, true) + ) shouldBe Some(2) + test.get( + RuntimeHealthMetric(CloudProvider.Gcp, welderImage.imageType, welderImage.imageUrl, RuntimeUI.Terra, false) + ) shouldBe Some(0) } // Data generators From e37ea454ba01c6b91558bc1f9a37b28041f57dc5 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 8 Jul 2025 12:49:44 -0400 Subject: [PATCH 26/43] Remove unused services from baseline dependencies --- .../http/BaselineDependenciesBuilder.scala | 8 -------- .../leonardo/http/GcpDependenciesBuilderSpec.scala | 14 +------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index e694dbb1ad..e0d3a6f35a 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -283,11 +283,7 @@ class BaselineDependenciesBuilder { kubernetesDnsCache, appDescriptorDAO, helmClient, - azureRelay, - azureVmService, operationFutureCache, - azureBatchService, - azureApplicationInsightsService, openTelemetry, samService ) @@ -410,11 +406,7 @@ final case class BaselineDependencies[F[_]]( kubernetesDnsCache: KubernetesDnsCache[F], appDescriptorDAO: HttpAppDescriptorDAO[F], helmClient: HelmInterpreter[F], - azureRelay: AzureRelayService[F], - azureVmService: AzureVmService[F], operationFutureCache: Cache[F, Long, OperationFuture[Operation, Operation]], - azureBatchService: AzureBatchService[F], - azureApplicationInsightsService: AzureApplicationInsightsService[F], openTelemetryMetrics: OpenTelemetryMetrics[F], samService: SamService[F] ) diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index e6835f208b..122b1fd850 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -12,15 +12,7 @@ import fs2.Stream import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google.{GoogleProjectDAO, HttpGoogleDirectoryDAO, HttpGoogleIamDAO} import org.broadinstitute.dsde.workbench.google2.GKEModels.KubernetesClusterId -import org.broadinstitute.dsde.workbench.google2.{ - GKEService, - GoogleComputeService, - GoogleDataprocService, - GoogleDiskService, - GoogleResourceService, - GoogleStorageService, - KubernetesService -} +import org.broadinstitute.dsde.workbench.google2.{GKEService, GoogleComputeService, GoogleDataprocService, GoogleDiskService, GoogleResourceService, GoogleStorageService, KubernetesService} import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor.Task import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider import org.broadinstitute.dsde.workbench.leonardo.dao._ @@ -138,11 +130,7 @@ class GcpDependenciesBuilderSpec mock[KubernetesDnsCache[IO]], mock[HttpAppDescriptorDAO[IO]], mock[HelmInterpreter[IO]], - mock[AzureRelayService[IO]], - mock[AzureVmService[IO]], mock[Cache[IO, Long, OperationFuture[Operation, Operation]]], - mock[AzureBatchService[IO]], - mock[AzureApplicationInsightsService[IO]], mock[OpenTelemetryMetrics[IO]], mock[SamService[IO]] ) From fb00ad0b348239db8fca8f5bd0ec78afd4716f4d Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 8 Jul 2025 12:54:44 -0400 Subject: [PATCH 27/43] Remove unused route models --- .../leonardo/LeonardoApiClient.scala | 108 ------------------ .../dsde/workbench/leonardo/JsonCodec.scala | 15 --- .../leonardo/azureRoutesModels.scala | 35 ------ .../http/RuntimeRoutesTestJsonCodec.scala | 24 +--- .../workbench/leonardo/CommonTestData.scala | 13 --- 5 files changed, 1 insertion(+), 194 deletions(-) delete mode 100644 core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureRoutesModels.scala diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala index adabacb13a..ea9607ea32 100644 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala +++ b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala @@ -135,19 +135,6 @@ object LeonardoApiClient { None ) - val defaultCreateAzureRuntimeRequest = CreateAzureRuntimeRequest( - Map.empty, - VirtualMachineSizeTypes.STANDARD_DS1_V2, - Map.empty, - CreateAzureDiskRequest( - Map.empty, - AzureDiskName(UUID.randomUUID().toString.substring(0, 8)), - None, - None - ), - Some(0) - ) - def createRuntime( googleProject: GoogleProject, runtimeName: RuntimeName, @@ -685,101 +672,6 @@ object LeonardoApiClient { } } yield r - def createAzureRuntime( - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - useExistingDisk: Boolean, - createAzureRuntimeRequest: CreateAzureRuntimeRequest = defaultCreateAzureRuntimeRequest - )(implicit client: Client[IO], authorization: IO[Authorization]): IO[Unit] = - for { - traceIdHeader <- genTraceIdHeader() - authHeader <- authorization - r <- client - .run( - Request[IO]( - method = Method.POST, - headers = Headers(authHeader, defaultMediaType, traceIdHeader), - uri = rootUri - .withPath( - Uri.Path - .unsafeFromString(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/${runtimeName.asString}") - ) - .withQueryParam("useExistingDisk", useExistingDisk), - entity = createAzureRuntimeRequest - ) - ) - .use { resp => - if (!resp.status.isSuccess) { - onError(s"Failed to create runtime ${workspaceId.value.toString}/${runtimeName.asString}")(resp) - .flatMap(IO.raiseError) - } else - IO.unit - } - } yield () - - def getAzureRuntime( - workspaceId: WorkspaceId, - runtimeName: RuntimeName - )(implicit client: Client[IO], authorization: IO[Authorization]): IO[GetRuntimeResponseCopy] = - for { - traceIdHeader <- genTraceIdHeader() - authHeader <- authorization - r <- client.expectOr[GetRuntimeResponseCopy]( - Request[IO]( - method = Method.GET, - headers = Headers(authHeader, traceIdHeader), - uri = rootUri.withPath( - Uri.Path.unsafeFromString(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/${runtimeName.asString}") - ) - ) - )(onError(s"Failed to get runtime ${workspaceId.value.toString}/${runtimeName.asString}")) - } yield r - - def deleteRuntimeV2( - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - deleteDisk: Boolean = true - )(implicit client: Client[IO], authorization: IO[Authorization]): IO[Unit] = - for { - traceIdHeader <- genTraceIdHeader() - authHeader <- authorization - r <- client - .run( - Request[IO]( - method = Method.DELETE, - headers = Headers(authHeader, traceIdHeader), - uri = rootUri - .withPath( - Uri.Path - .unsafeFromString(s"/api/v2/runtimes/${workspaceId.value.toString}/azure/${runtimeName.asString}") - ) - .withQueryParam("deleteDisk", deleteDisk) - ) - ) - .use { resp => - if (!resp.status.isSuccess) { - onError(s"Failed to delete runtime ${workspaceId.value.toString}/${runtimeName.asString}")(resp) - .flatMap(IO.raiseError) - } else - IO.unit - } - } yield r - - // TODO: delete this - def deleteRuntimeV2WithWait( - workspaceId: WorkspaceId, - runtimeName: RuntimeName, - deleteDisk: Boolean = true - )(implicit client: Client[IO], authorization: IO[Authorization]): IO[Unit] = - for { - _ <- deleteRuntimeV2(workspaceId, runtimeName, deleteDisk) - ioa = getAzureRuntime(workspaceId, runtimeName).attempt - res <- IO.sleep(20 seconds) >> streamFUntilDone(ioa, 50, 5 seconds).compile.lastOrError - _ <- - if (res.isDone) IO.unit - else IO.raiseError(new TimeoutException(s"delete runtime ${workspaceId}/${runtimeName.asString}")) - } yield () - def testSparkWebUi( googleProject: GoogleProject, runtimeName: RuntimeName, diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala index 776051f684..26b94a9f94 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/JsonCodec.scala @@ -179,13 +179,6 @@ object JsonCodec { else none[VirtualMachineSizeTypes] machineSizeOpt.toRight(s"Invalid azure virtualMachineSizeType ${s}") } - implicit val azureImageUriDecoder: Decoder[AzureImage] = Decoder.forProduct4( - "publisher", - "offer", - "sku", - "version" - )((x, y, z, v) => AzureImage(x, y, z, v)) - implicit val azureDiskNameDecoder: Decoder[AzureDiskName] = Decoder.decodeString.map(AzureDiskName) implicit val userJupyterExtensionConfigEncoder: Encoder[UserJupyterExtensionConfig] = Encoder.forProduct4( "nbExtensions", @@ -757,16 +750,8 @@ object JsonCodec { ) implicit val azureMachineTypeEncoder: Encoder[VirtualMachineSizeTypes] = Encoder.encodeString.contramap(_.toString) - implicit val azureDiskNameEncoder: Encoder[AzureDiskName] = Encoder.encodeString.contramap(_.value) implicit val relayNamespaceEncoder: Encoder[RelayNamespace] = Encoder.encodeString.contramap(_.value) - implicit val azureImageEncoder: Encoder[AzureImage] = Encoder.forProduct4( - "publisher", - "offer", - "sku", - "version" - )(x => (x.publisher, x.offer, x.sku, x.version)) - implicit val autodeleteThresholdEncoder: Encoder[AutodeleteThreshold] = Encoder.encodeInt.contramap(_.value) implicit val autodeleteThresholdDecoder: Decoder[AutodeleteThreshold] = Decoder.decodeInt.emap { diff --git a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureRoutesModels.scala b/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureRoutesModels.scala deleted file mode 100644 index c1e9c7608c..0000000000 --- a/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/azureRoutesModels.scala +++ /dev/null @@ -1,35 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo - -import bio.terra.workspace.model.AzureVmImage -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes - -final case class CreateAzureRuntimeRequest(labels: LabelMap, - machineSize: VirtualMachineSizeTypes, - customEnvironmentVariables: Map[String, String], - azureDiskConfig: CreateAzureDiskRequest, - autopauseThreshold: Option[Int] -) - -final case class CreateAzureDiskRequest(labels: LabelMap, - name: AzureDiskName, - size: Option[DiskSize], - diskType: Option[DiskType] -) - -//TODO: implement -final case class UpdateAzureRuntimeRequest(machineSize: VirtualMachineSizeTypes) - -//TODO: delete this case class when current pd.diskName is no longer coupled to google2 diskService -final case class AzureDiskName(value: String) extends AnyVal - -final case class AzureImage(publisher: String, offer: String, sku: String, version: String) { - def asString: String = s"${publisher}, ${offer}, ${sku}, ${version}" - - def toWsm(): AzureVmImage = - new AzureVmImage() - .offer(offer) - .publisher(publisher) - .sku(sku) - .version(version) - -} diff --git a/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala b/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala index fe62c805cd..5984b2e451 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala @@ -4,22 +4,7 @@ import io.circe.syntax._ import io.circe.{Decoder, Encoder} import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.RuntimeSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.{ - AsyncRuntimeFields, - AuditInfo, - CloudContext, - CreateAzureDiskRequest, - CreateAzureRuntimeRequest, - LabelMap, - RuntimeConfig, - RuntimeError, - RuntimeImage, - RuntimeName, - RuntimeStatus, - UserJupyterExtensionConfig, - UserScriptPath, - WorkspaceId -} +import org.broadinstitute.dsde.workbench.leonardo.{AsyncRuntimeFields, AuditInfo, CloudContext, LabelMap, RuntimeConfig, RuntimeError, RuntimeImage, RuntimeName, RuntimeStatus, UserJupyterExtensionConfig, UserScriptPath, WorkspaceId} import org.broadinstitute.dsde.workbench.model.WorkbenchEmail import org.broadinstitute.dsde.workbench.model.google.GoogleProject @@ -79,13 +64,6 @@ object RuntimeRoutesTestJsonCodec { } } - implicit val azureDiskConfigEncoder: Encoder[CreateAzureDiskRequest] = - Encoder.forProduct4("labels", "name", "size", "diskType")(x => CreateAzureDiskRequest.unapply(x).get) - implicit val createAzureRuntimeRequestEncoder: Encoder[CreateAzureRuntimeRequest] = - Encoder.forProduct5("labels", "machineSize", "customEnvironmentVariables", "disk", "autopauseThreshold")(x => - CreateAzureRuntimeRequest.unapply(x).get - ) - implicit val createRuntime2RequestEncoder: Encoder[CreateRuntimeRequest] = Encoder.forProduct13( "labels", "userScriptUri", diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index 86a56818f7..935200d2a3 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -524,19 +524,6 @@ object CommonTestData { ) .gcpContext(new GcpContext().projectId("googleProject")) - val defaultCreateAzureRuntimeReq = CreateAzureRuntimeRequest( - Map.empty, - VirtualMachineSizeTypes.STANDARD_A1, - Map.empty, - CreateAzureDiskRequest( - Map.empty, - AzureDiskName("diskName1"), - Some(DiskSize(100)), - None - ), - Some(0) - ) - def modifyInstance(instance: DataprocInstance): DataprocInstance = instance.copy(key = modifyInstanceKey(instance.key), googleId = instance.googleId + 1) def modifyInstanceKey(instanceKey: DataprocInstanceKey): DataprocInstanceKey = From 81a4cc4fceeaf58deff36feb9fc29230ac068b12 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 8 Jul 2025 12:59:55 -0400 Subject: [PATCH 28/43] Remove WSM and BPM dependencies --- project/Dependencies.scala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 5438edce37..0b4e5d207d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -103,12 +103,14 @@ object Dependencies { excludeGuava, excludeOpenTelemetry ) + // TODO remove once all Azure-related code is gone val workbenchAzure: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-azure" % workbenchAzureV + val workbenchAzureTest: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-azure" % workbenchAzureV % "test" classifier "tests" + val workbenchOauth2: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-oauth2" % workbenchOauth2V val workbenchOauth2Tests: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-oauth2" % workbenchOauth2V % "test" classifier "tests" val workbenchGoogleTest: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-google" % workbenchGoogleV % "test" classifier "tests" excludeAll (excludeGuava, excludeStatsD) val workbenchGoogle2Test: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-google2" % workbenchGoogle2V % "test" classifier "tests" excludeAll (excludeGuava) //for generators - val workbenchAzureTest: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-azure" % workbenchAzureV % "test" classifier "tests" val workbenchOpenTelemetry: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-opentelemetry" % workbenchOpenTelemetryV excludeAll ( excludeIoGrpc, excludeGuava @@ -139,10 +141,7 @@ object Dependencies { val commonsBeanUtils = "commons-beanutils" % "commons-beanutils" % commonsBeanUtilsV val okHttp = "com.squareup.okhttp3" % "okhttp" % "4.12.0" - val workSpaceManagerV = "0.254.1127-SNAPSHOT" - val terraCommonLibV = "1.1.38-SNAPSHOT" - val bpmV = "0.1.548-SNAPSHOT" val samV = "v0.0.274" def excludeJakartaActivationApi = ExclusionRule("jakarta.activation", "jakarta.activation-api") @@ -164,8 +163,6 @@ object Dependencies { // [IA-4939] commons-text:1.9 is unsafe def excludeCommonsText = ExclusionRule("org.apache.commons", "commons-text") def tclExclusions(m: ModuleID): ModuleID = m.excludeAll(excludeSpringBoot, excludeSpringAop, excludeSpringData, excludeSpringFramework, excludeOpenCensus, excludeGoogleFindBugs, excludeBroadWorkbench, excludePostgresql, excludeSnakeyaml, excludeSlf4j, excludeCommonsText, excludeLiquibase, excludeOpenTelemetry, excludeFlagsmith) - val workspaceManager = excludeJakarta("bio.terra" % "workspace-manager-client" % workSpaceManagerV) - val bpm = excludeJakarta("bio.terra" % "billing-profile-manager-client" % bpmV) val terraCommonLib = tclExclusions(excludeJakarta("bio.terra" % "terra-common-lib" % terraCommonLibV classifier "plain")) val sam = excludeJakarta("org.broadinstitute.dsde.workbench" %% "sam-client" % samV) @@ -197,8 +194,6 @@ object Dependencies { workbenchAzure, workbenchAzureTest, logbackClassic, - workspaceManager, - bpm, terraCommonLib, sam ) From eea428e057ae853a00857cc96658a51599d8d618 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 8 Jul 2025 13:43:11 -0400 Subject: [PATCH 29/43] Remove most v2 endpoints from swagger doc --- http/src/main/resources/swagger/api-docs.yaml | 1400 +++-------------- .../leonardo/http/api/HttpRoutes.scala | 1 + 2 files changed, 241 insertions(+), 1160 deletions(-) diff --git a/http/src/main/resources/swagger/api-docs.yaml b/http/src/main/resources/swagger/api-docs.yaml index 739b14cf4f..c4e5c98c0c 100644 --- a/http/src/main/resources/swagger/api-docs.yaml +++ b/http/src/main/resources/swagger/api-docs.yaml @@ -17,7 +17,7 @@ tags: - name: disks description: Persistent Disks API. - name: apps - description: Apps API. Support Google Kubernetes Engine and Azure Kubernetes Service. + description: Apps API. Support Google Kubernetes Engine. - name: proxy description: Proxy API - name: admin @@ -503,24 +503,25 @@ paths: schema: $ref: "#/components/schemas/ErrorReport" + ## Persistent Disk paths ## - "/api/v2/runtimes": + /api/google/v1/disks: get: - summary: List all runtimes that the caller has access to - description: List all runtimes, optionally filtering on a set of labels - operationId: listRuntimesV2 + summary: List all persistent disks that the caller has access to + description: List all persistent disks, optionally filtering on a set of labels + operationId: listDisks tags: - - runtimes + - disks parameters: - in: query name: _labels description: > Optional label key-value pairs to filter results by. Example: Querying by key1=val1,key2=val2 - returns all azure runtimes that contain the key1/val1 and key2/val2 labels (possibly among other labels). + returns all persistent disks that contain the key1/val1 and key2/val2 labels (possibly among other labels). Note: this string format is a workaround because Swagger doesn't support free-form query string parameters. The recommended way to use this endpoint is to specify the - labels as top-level query string parameters. For instance: GET /api/v2/runtimes?key1=val1&key2=val2. + labels as top-level query string parameters. For instance: GET /api/google/v1/disks?key1=val1&key2=val2. required: false schema: type: string @@ -529,76 +530,25 @@ paths: description: > Optional label keys of the labels returned in response. Example: Querying by key1,key2,key3 - returns all labels key1/val1 key2/val2 and key3 and val3 for each runtime response - required: false - schema: - type: string - responses: - "200": - description: List of runtimes - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/ListRuntimeResponse" - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "/api/v2/runtimes/{workspaceId}": - get: - summary: List all runtimes within the given workspace that the caller has access to - description: List all runtimes within the given workspace, optionally - filtering on a set of labels - operationId: listRuntimesByWorkspaceV2 - tags: - - runtimes - parameters: - - in: path - name: workspaceId - description: workspaceId - required: true - schema: - type: string - - in: query - name: _labels - description: > - Optional label key-value pairs to filter results by. Example: - Querying by key1=val1,key2=val2 - returns all azure runtimes that contain the key1/val1 and key2/val2 labels (possibly among other labels). - Note: this string format is a workaround because Swagger doesn't support free-form - query string parameters. The recommended way to use this endpoint is to specify the - labels as top-level query string parameters. For instance: GET /api/v2/runtimes/{workspaceId}?key1=val1&key2=val2. + returns all labels key1/val1 key2/val2 and key3 and val3 for each persistent disk in response required: false schema: type: string - in: query - name: includeLabels - description: > - Optional label keys of the labels returned in response. Example: - Querying by key1,key2,key3 - returns all labels key1/val1 key2/val2 and key3 and val3 for each runtime response + name: role + description: Optional filter that excludes persistent disks you did not create. Accepts "creator" or nothing. required: false schema: type: string responses: "200": - description: List of runtimes + description: List of persistent disks content: application/json: schema: type: array items: - $ref: "#/components/schemas/ListRuntimeResponse" + $ref: "#/components/schemas/ListPersistentDiskResponse" "400": description: Bad Request content: @@ -611,18 +561,18 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - "/api/v2/runtimes/{workspaceId}/azure": + /api/google/v1/disks/{googleProject}: get: - summary: List all azure runtimes within the given workspace that the caller has access to - description: List all azure runtimes within the given workspace, optionally + summary: List all persistent disks within the given Google project that the caller has access to + description: List all persistent disks within the given Google project, optionally filtering on a set of labels - operationId: listAzureRuntimesV2 + operationId: listDisksByProject tags: - - runtimes + - disks parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string @@ -631,10 +581,10 @@ paths: description: > Optional label key-value pairs to filter results by. Example: Querying by key1=val1,key2=val2 - returns all azure runtimes that contain the key1/val1 and key2/val2 labels (possibly among other labels). + returns all persistent disks that contain the key1/val1 and key2/val2 labels (possibly among other labels). Note: this string format is a workaround because Swagger doesn't support free-form query string parameters. The recommended way to use this endpoint is to specify the - labels as top-level query string parameters. For instance: GET /api/v2/runtimes/{workspaceId}/azure?key1=val1&key2=val2. + labels as top-level query string parameters. For instance: GET /api/google/v1/disks?key1=val1&key2=val2. required: false schema: type: string @@ -643,19 +593,25 @@ paths: description: > Optional label keys of the labels returned in response. Example: Querying by key1,key2,key3 - returns all labels key1/val1 key2/val2 and key3 and val3 for each runtime response + returns all labels key1/val1 key2/val2 and key3 and val3 for each persistent disk in response + required: false + schema: + type: string + - in: query + name: role + description: Optional filter that excludes persistent disks you did not create. Accepts "creator" or nothing. required: false schema: type: string responses: "200": - description: List of runtimes + description: List of persistent disks content: application/json: schema: type: array items: - $ref: "#/components/schemas/ListRuntimeResponse" + $ref: "#/components/schemas/ListPersistentDiskResponse" "400": description: Bad Request content: @@ -668,34 +624,39 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - "/api/v2/runtimes/{workspaceId}/{name}/start": - post: - summary: Start runtime (WIP). + /api/google/v1/disks/{googleProject}/{name}: + get: + summary: Get details of a persistent disk description: > - Start an existing Leonardo managed runtime - operationId: startRuntimeV2 + Returns information about an existing persistent disk managed by Leo. + Poll this to find out when your disk has finished starting up. + operationId: getDisk tags: - - runtimes + - disks parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string - in: path name: name - description: runtimeName + description: diskName required: true schema: type: string responses: - "202": - description: Runtime start request accepted + "200": + description: Persistent disk found, here are the details + content: + application/json: + schema: + $ref: "#/components/schemas/GetPersistentDiskResponse" "403": - description: User does not have permission to perform action on runtime + description: User does not have permission to perform action on persistent disk "404": - description: Runtime not found + description: Persistent disk not found content: application/json: schema: @@ -706,34 +667,41 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - "/api/v2/runtimes/{workspaceId}/{name}/stop": post: - summary: Stop runtime (WIP). + summary: Creates a new persistent disk in the given project with the given name. description: > - Stop an existing Leonardo managed runtime - operationId: stopRuntimeV2 + The request is completed without waiting for the persistent disk to be created in Google. + The disk status can be polled using the getDisk API. + Default labels diskName, googleProject, and creator cannot be overridden. + operationId: createDisk tags: - - runtimes + - disks parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string - in: path name: name - description: runtimeName + description: diskName. only lowercase alphanumeric characters, numbers and dashes are allowed required: true schema: type: string + requestBody: + $ref: "#/components/requestBodies/CreateDiskRequest" responses: "202": - description: Runtime stop request accepted - "403": - description: User does not have permission to perform action on runtime - "404": - description: Runtime not found + description: Persistent disk creation request accepted + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "409": + description: Conflict content: application/json: schema: @@ -744,36 +712,32 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - "/api/v2/runtimes/{workspaceId}/{name}/updateDateAccessed": - patch: - summary: Update the date accessed of a runtime. - description: > - This API is used in conjunction with v2 runtime's autopause functionality. Since Azure runtimes are not proxied to Leonardo, the relay must send a 'keep-alive' - message of sorts. If no requests are received after a period of time, the runtime will be stopped. - Each request to this API does not result in the runtime's date accessed being updated. An async process is responsible for batching them and executing updates. - operationId: updateDateAccessed + delete: + summary: Deletes an existing persistent disk in the given project + description: Deletes a persistent disk + operationId: deleteDisk tags: - - runtimes + - disks parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string - in: path name: name - description: runtimeName + description: diskName required: true schema: type: string responses: "202": - description: Runtime update date accessed request accepted + description: Persistent disk deletion request accepted "403": - description: User does not have permission to perform action on runtime + description: User does not have permission to perform action on persistent disk "404": - description: Runtime not found + description: Persistent disk not found content: application/json: schema: @@ -784,39 +748,38 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - "/api/v2/runtimes/{workspaceId}/azure/{name}": - get: - summary: Get details of an azure runtime - description: > - Returns information about an existing azure runtime managed by Leo. - Poll this to find out when your runtime has finished starting up. - operationId: getAzureRuntime + patch: + summary: Updates the configuration of a persistent disk + description: In order to update the configuration of a persistent disk, it must first be ready + operationId: updateDisk tags: - - runtimes + - disks parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string - in: path name: name - description: runtimeName + description: diskName required: true schema: type: string + requestBody: + $ref: "#/components/requestBodies/UpdateDiskRequest" responses: - "200": - description: Runtime found, here are the details + "202": + description: Persistent disk update request accepted + "400": + description: Bad Request content: application/json: schema: - $ref: "#/components/schemas/GetRuntimeResponse" - "403": - description: User does not have permission to perform action on runtime - "404": - description: Runtime not found + $ref: "#/components/schemas/ErrorReport" + "409": + description: Conflict content: application/json: schema: @@ -827,569 +790,75 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - post: - summary: Creates a Azure runtime instance in the given workspace with the given name. - description: > - The request is completed without waiting for the runtime to be created in Azure. The runtime status can be polled using the - getRuntime API. - operationId: createAzureRuntime + + ## Kubernetes App API ## + "/api/google/v1/apps": + get: tags: - - runtimes + - apps + summary: List apps + description: List kubernetes apps the caller has access to, without specifying a project + operationId: listApp parameters: - - in: path - name: workspaceId - description: workspaceId - required: true + - in: query + name: _labels + description: > + Optional label key-value pairs to filter results by. Example: + Querying by key1=val1,key2=val2 + returns all apps that contain the key1/val1 and key2/val2 labels (possibly among other labels). + Note: this string format is a workaround because Swagger doesn't support free-form + query string parameters. The recommended way to use this endpoint is to specify the + labels as top-level query string parameters. For instance: GET /api/google/v1/app?key1=val1&key2=val2. + required: false schema: type: string - - in: path - name: name - description: runtimeName. only lowercase alphanumeric characters, numbers and dashes are allowed - required: true + - in: query + name: includeLabels + description: > + Optional label keys of the labels returned in response. Example: + Querying by key1,key2,key3 + returns all labels key1/val1 key2/val2 and key3 and val3 for each app in response + required: false schema: type: string - in: query - name: useExistingDisk - description: Defaults to false. If true ignores disk request and creates runtime with an existing disk associated with the user and workspace. Will error if 0 or multiple disks are found. + name: role + description: Optional filter that excludes apps you did not create. Accepts "creator" or nothing. required: false schema: - type: boolean - default: false - - requestBody: - $ref: "#/components/requestBodies/CreateAzureRuntimeRequest" + type: string responses: - "202": - description: Runtime creation request accepted + "200": + description: List of apps content: application/json: schema: - $ref: "#/components/schemas/CreateRuntimeResponse" + type: array + items: + $ref: "#/components/schemas/ListAppResponse" "400": description: Bad Request content: application/json: schema: $ref: "#/components/schemas/ErrorReport" - "409": - description: Conflict - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" "500": description: Internal Error content: application/json: schema: $ref: "#/components/schemas/ErrorReport" - delete: - summary: Deletes an Azure runtime instance in the given workspace - description: deletes a runtime - operationId: deleteAzureRuntime + "/api/google/v1/apps/{googleProject}": + get: + summary: List apps within a project + description: List kubernetes apps the caller has access to with a project filter + operationId: listAppByProject tags: - - runtimes + - apps parameters: - in: path - name: workspaceId - description: workspaceId - required: true - schema: - type: string - - in: path - name: name - description: runtimeName - required: true - schema: - type: string - - in: query - name: deleteDisk - description: Whether or not disk should be deleted upon runtime deletion. Defaults to true if not specified - required: false - schema: - type: boolean - default: true - responses: - "202": - description: Runtime deletion request accepted - "403": - description: User does not have permission to perform action on runtime - "404": - description: Runtime not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - - "/api/v2/runtimes/{workspaceId}/deleteAll": - post: - summary: Deletes ALL runtimes associated with the given workspace Id - description: deletes all runtimes in a workspace - operationId: deleteAllRuntimesV2 - tags: - - runtimes - parameters: - - in: path - name: workspaceId - description: workspaceId - required: true - schema: - type: string - - in: query - name: deleteDisk - description: Whether or not the disk associated with the apps should be deleted. Default to false if not specified. - required: false - schema: - type: boolean - default: false - responses: - "202": - description: Runtime(s) deletion request accepted - "403": - description: User does not have permission to perform action on runtime(s) - "404": - description: Runtime(s) not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - - ## Persistent Disk paths ## - - /api/google/v1/disks: - get: - summary: List all persistent disks that the caller has access to - description: List all persistent disks, optionally filtering on a set of labels - operationId: listDisks - tags: - - disks - parameters: - - in: query - name: _labels - description: > - Optional label key-value pairs to filter results by. Example: - Querying by key1=val1,key2=val2 - returns all persistent disks that contain the key1/val1 and key2/val2 labels (possibly among other labels). - Note: this string format is a workaround because Swagger doesn't support free-form - query string parameters. The recommended way to use this endpoint is to specify the - labels as top-level query string parameters. For instance: GET /api/google/v1/disks?key1=val1&key2=val2. - required: false - schema: - type: string - - in: query - name: includeLabels - description: > - Optional label keys of the labels returned in response. Example: - Querying by key1,key2,key3 - returns all labels key1/val1 key2/val2 and key3 and val3 for each persistent disk in response - required: false - schema: - type: string - - in: query - name: role - description: Optional filter that excludes persistent disks you did not create. Accepts "creator" or nothing. - required: false - schema: - type: string - responses: - "200": - description: List of persistent disks - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/ListPersistentDiskResponse" - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - /api/google/v1/disks/{googleProject}: - get: - summary: List all persistent disks within the given Google project that the caller has access to - description: List all persistent disks within the given Google project, optionally - filtering on a set of labels - operationId: listDisksByProject - tags: - - disks - parameters: - - in: path - name: googleProject - description: googleProject - required: true - schema: - type: string - - in: query - name: _labels - description: > - Optional label key-value pairs to filter results by. Example: - Querying by key1=val1,key2=val2 - returns all persistent disks that contain the key1/val1 and key2/val2 labels (possibly among other labels). - Note: this string format is a workaround because Swagger doesn't support free-form - query string parameters. The recommended way to use this endpoint is to specify the - labels as top-level query string parameters. For instance: GET /api/google/v1/disks?key1=val1&key2=val2. - required: false - schema: - type: string - - in: query - name: includeLabels - description: > - Optional label keys of the labels returned in response. Example: - Querying by key1,key2,key3 - returns all labels key1/val1 key2/val2 and key3 and val3 for each persistent disk in response - required: false - schema: - type: string - - in: query - name: role - description: Optional filter that excludes persistent disks you did not create. Accepts "creator" or nothing. - required: false - schema: - type: string - responses: - "200": - description: List of persistent disks - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/ListPersistentDiskResponse" - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - /api/google/v1/disks/{googleProject}/{name}: - get: - summary: Get details of a persistent disk - description: > - Returns information about an existing persistent disk managed by Leo. - Poll this to find out when your disk has finished starting up. - operationId: getDisk - tags: - - disks - parameters: - - in: path - name: googleProject - description: googleProject - required: true - schema: - type: string - - in: path - name: name - description: diskName - required: true - schema: - type: string - responses: - "200": - description: Persistent disk found, here are the details - content: - application/json: - schema: - $ref: "#/components/schemas/GetPersistentDiskResponse" - "403": - description: User does not have permission to perform action on persistent disk - "404": - description: Persistent disk not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - post: - summary: Creates a new persistent disk in the given project with the given name. - description: > - The request is completed without waiting for the persistent disk to be created in Google. - The disk status can be polled using the getDisk API. - Default labels diskName, googleProject, and creator cannot be overridden. - operationId: createDisk - tags: - - disks - parameters: - - in: path - name: googleProject - description: googleProject - required: true - schema: - type: string - - in: path - name: name - description: diskName. only lowercase alphanumeric characters, numbers and dashes are allowed - required: true - schema: - type: string - requestBody: - $ref: "#/components/requestBodies/CreateDiskRequest" - responses: - "202": - description: Persistent disk creation request accepted - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "409": - description: Conflict - 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 persistent disk in the given project - description: Deletes a persistent disk - operationId: deleteDisk - tags: - - disks - parameters: - - in: path - name: googleProject - description: googleProject - required: true - schema: - type: string - - in: path - name: name - description: diskName - required: true - schema: - type: string - responses: - "202": - description: Persistent disk deletion request accepted - "403": - description: User does not have permission to perform action on persistent disk - "404": - description: Persistent disk not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - patch: - summary: Updates the configuration of a persistent disk - description: In order to update the configuration of a persistent disk, it must first be ready - operationId: updateDisk - tags: - - disks - parameters: - - in: path - name: googleProject - description: googleProject - required: true - schema: - type: string - - in: path - name: name - description: diskName - required: true - schema: - type: string - requestBody: - $ref: "#/components/requestBodies/UpdateDiskRequest" - responses: - "202": - description: Persistent disk update request accepted - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "409": - description: Conflict - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - - "/api/v2/disks/{id}": - get: - summary: Get details of a persistent disk - description: > - Returns information about an existing persistent disk managed by Leo. - Poll this to find out your disk status. - operationId: getDiskV2 - tags: - - disks - parameters: - - in: path - name: id - description: diskId - required: true - schema: - type: integer - responses: - "200": - description: Persistent disk found, here are the details - content: - application/json: - schema: - $ref: "#/components/schemas/GetPersistentDiskV2Response" - "403": - description: User does not have permission to perform action on disk - "404": - description: Persistent disk not found - 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 persistent disk instance - description: Deletes a persistent disk - operationId: deleteDiskV2 - tags: - - disks - parameters: - - in: path - name: id - description: diskId - required: true - schema: - type: integer - responses: - "202": - description: Persistent disk deletion request accepted - "403": - description: User does not have permission to perform action on persistent disk - "404": - description: Persistent disk not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - - ## Kubernetes App API ## - "/api/google/v1/apps": - get: - tags: - - apps - summary: List apps - description: List kubernetes apps the caller has access to, without specifying a project - operationId: listApp - parameters: - - in: query - name: _labels - description: > - Optional label key-value pairs to filter results by. Example: - Querying by key1=val1,key2=val2 - returns all apps that contain the key1/val1 and key2/val2 labels (possibly among other labels). - Note: this string format is a workaround because Swagger doesn't support free-form - query string parameters. The recommended way to use this endpoint is to specify the - labels as top-level query string parameters. For instance: GET /api/google/v1/app?key1=val1&key2=val2. - required: false - schema: - type: string - - in: query - name: includeLabels - description: > - Optional label keys of the labels returned in response. Example: - Querying by key1,key2,key3 - returns all labels key1/val1 key2/val2 and key3 and val3 for each app in response - required: false - schema: - type: string - - in: query - name: role - description: Optional filter that excludes apps you did not create. Accepts "creator" or nothing. - required: false - schema: - type: string - responses: - "200": - description: List of apps - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/ListAppResponse" - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "/api/google/v1/apps/{googleProject}": - get: - summary: List apps within a project - description: List kubernetes apps the caller has access to with a project filter - operationId: listAppByProject - tags: - - apps - parameters: - - in: path - name: googleProject - description: googleProject + name: googleProject + description: googleProject required: true schema: type: string @@ -1496,275 +965,17 @@ paths: schema: $ref: "#/components/schemas/ErrorReport" get: - summary: Get details of an app - description: > - Returns information about an existing App managed by Leo. - Poll this to find out when your app has finished starting up. - operationId: getApp - 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 - responses: - "200": - description: App found, here are the details - content: - application/json: - schema: - $ref: "#/components/schemas/GetAppResponse" - "403": - description: User does not have permission to perform action on App - "404": - description: App not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - patch: - summary: Updates the configuration of an app - description: Updates the configuration of an app. Currently limited to autodeletion config. - 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 - operationId: deleteApp - 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 - - in: query - name: deleteDisk - description: Whether or not the disk associated with the app should be deleted. Default to false if not specified. - required: false - schema: - type: boolean - default: false - responses: - "202": - description: App deletion request accepted - "403": - description: User does not have permission to perform action on App - "404": - description: App not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "/api/google/v1/apps/{googleProject}/{appName}/stop": - post: - summary: Stops an app with the given project and name - description: > - Stops the running compute, but retains any data persisted on disk. The app may be restarted with the /start endpoint. - operationId: stopApp - 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 - responses: - "202": - description: App stop request accepted - "403": - description: User does not have permission to perform action on app - "404": - description: App not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "409": - description: App cannot be stopped - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "/api/google/v1/apps/{googleProject}/{appName}/start": - post: - summary: Starts an app with the given project and name - description: Starts the stopped app - operationId: startApp - 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 - responses: - "202": - description: App start request accepted - "403": - description: User does not have permission to perform action on app - "404": - description: App not found - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "409": - description: App cannot be started - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - - ## AppsV2 Paths ## - - "/api/apps/v2/{workspaceId}/{appName}": - post: - summary: Creates a new app in the given project with the given appName - description: > - The specified appName is suffixed and the resulting string must adhere to Google's name validation regex ?:[a-z](?:[-a-z0-9]{0,38}[a-z0-9])?. - Default labels appName, googleProject, serviceAccount, and creator cannot be overridden. - operationId: createAppV2 - tags: - - apps - parameters: - - in: path - name: workspaceId - description: workspaceId - required: true - schema: - type: string - - in: path - name: appName - description: appName - required: true - schema: - type: string - requestBody: - $ref: "#/components/requestBodies/CreateAppV2Request" - responses: - "202": - description: App creation request has been received - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "403": - description: User does not have permission to perform action on App - "409": - description: Conflict - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - "500": - description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" - get: - summary: Get details of an app + summary: Get details of an app description: > Returns information about an existing App managed by Leo. Poll this to find out when your app has finished starting up. - operationId: getAppV2 + operationId: getApp tags: - apps parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string @@ -1795,17 +1006,52 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - + patch: + summary: Updates the configuration of an app + description: Updates the configuration of an app. Currently limited to autodeletion config. + 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 - operationId: deleteAppV2 + operationId: deleteApp tags: - apps parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string @@ -1839,93 +1085,83 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" - - "/api/apps/v2/{workspaceId}": - get: - summary: List apps V2 - description: List all apps for a given workspaceId. - operationId: listAppsV2 + "/api/google/v1/apps/{googleProject}/{appName}/stop": + post: + summary: Stops an app with the given project and name + description: > + Stops the running compute, but retains any data persisted on disk. The app may be restarted with the /start endpoint. + operationId: stopApp tags: - apps parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string - - in: query - name: _labels - description: > - Optional label key-value pairs to filter results by. Example: - Querying by key1=val1,key2=val2 - returns all apps that contain the key1/val1 and key2/val2 labels (possibly among other labels). - Note: this string format is a workaround because Swagger doesn't support free-form - query string parameters. The recommended way to use this endpoint is to specify the - labels as top-level query string parameters. For instance: GET /api/google/v1/app?key1=val1&key2=val2. - required: false - schema: - type: string - - in: query - name: includeDeleted - description: Optional filter that includes any apps with a Deleted status. - required: false - schema: - type: boolean - default: false - - in: query - name: includeLabels - description: > - Optional label keys of the labels returned in response. Example: - Querying by key1,key2,key3 - returns all labels key1/val1 key2/val2 and key3 and val3 for each app in response - required: false - schema: - type: string - - in: query - name: role - description: Optional filter that excludes apps you did not create. Accepts "creator" or nothing. - required: false + - in: path + name: appName + description: appName + required: true schema: type: string responses: - "200": - description: List of apps + "202": + description: App stop request accepted + "403": + description: User does not have permission to perform action on app + "404": + description: App not found content: application/json: schema: - type: array - items: - $ref: "#/components/schemas/ListAppResponse" - - "/api/apps/v2/{workspaceId}/deleteAll": + $ref: "#/components/schemas/ErrorReport" + "409": + description: App cannot be stopped + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "500": + description: Internal Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "/api/google/v1/apps/{googleProject}/{appName}/start": post: - summary: Deletes ALL apps associated with the given workspace Id - description: deletes all Apps in a workspace - operationId: deleteAllAppsV2 + summary: Starts an app with the given project and name + description: Starts the stopped app + operationId: startApp tags: - apps parameters: - in: path - name: workspaceId - description: workspaceId + name: googleProject + description: googleProject required: true schema: type: string - - in: query - name: deleteDisk - description: Whether or not the disk associated with the apps should be deleted. Default to false if not specified. - required: false + - in: path + name: appName + description: appName + required: true schema: - type: boolean - default: false + type: string responses: "202": - description: App(s) deletion request accepted + description: App start request accepted "403": - description: User does not have permission to perform action on App(s) + description: User does not have permission to perform action on app "404": - description: App(s) not found + description: App not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "409": + description: App cannot be started content: application/json: schema: @@ -1975,10 +1211,6 @@ paths: $ref: "#/components/schemas/ErrorReport" "500": description: Internal Error - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorReport" ## Deprecated Notebook API ## @@ -2876,23 +2108,6 @@ components: diskConfig: name: "disk1" appType: "GALAXY" - CreateAppV2Request: - content: - application/json: - schema: - $ref: "#/components/schemas/CreateAppRequest" - example: - appType: "CROMWELL" - CreateAzureRuntimeRequest: - content: - application/json: - schema: - $ref: "#/components/schemas/CreateAzureRuntimeRequest" - example: - labels: { saturnRuntimeName: "saturn-ab9134" } - machineSize: "Standard_DS1_v2" - disk: { labels: {}, name: "disk1", size: 50 } - autopauseThreshold: 15 UpdateAppRequest: content: application/json: @@ -2908,7 +2123,7 @@ components: $ref: "#/components/schemas/UpdateAppsRequest" example: appType: CROMWELL - cloudProvider: AZURE + cloudProvider: GCP dryRun: true securitySchemes: @@ -2927,11 +2142,7 @@ components: - CROMWELL - CUSTOM - GALAXY - - WDS - - HAIL_BATCH - ALLOWED - - WORKFLOWS_APP - - CROMWELL_RUNNER_APP AllowedChartName: type: string enum: @@ -2964,7 +2175,6 @@ components: type: string enum: - GCP - - AZURE ClusterStatus: type: string enum: @@ -3306,68 +2516,6 @@ components: type: string description: The id of the disk's workspace - - GetPersistentDiskV2Response: - description: "" - required: - - id - - cloudContext - - zone - - name - - serviceAccount - - samResource - - status - - auditInfo - - size - - diskType - - blockSize - - labels - properties: - id: - type: integer - description: The cloud-platform provided unique ID of this persistent disk - cloudContext: - $ref: '#/components/schemas/CloudContext' - zone: - type: string - description: The zone in which the persistent disk was created - name: - type: string - description: The name of the persistent disk - serviceAccount: - type: string - description: The account used to create the disk - samResource: - type: string - description: Internal resource ID of the persistent disk - status: - $ref: "#/components/schemas/DiskStatus" - auditInfo: - $ref: "#/components/schemas/AuditInfo" - size: - type: integer - description: Size of persistent disk in GB - diskType: - $ref: "#/components/schemas/DiskType" - blockSize: - type: integer - description: Block size of persistent disk in bytes - labels: - type: object - description: The labels to be placed on the persistent disk. Of type Map[String,String] - workspaceId: - type: string - description: The id of the disk's workspace - formattedBy: - type: string - enum: - - GALAXY - - GCE - - CROMWELL - - CUSTOM - - ALLOWED - description: App that formatted disk, missing if unformatted - InstanceKey: description: "" required: @@ -3500,49 +2648,6 @@ components: customEnvironmentVariables: type: object description: Optional environment variables to be set on the cluster. - CreateAzureRuntimeRequest: - description: Creates a new runtime - type: object - required: - - machineSize - - disk - properties: - labels: - type: object - description: The labels to be placed on the runtime. Of type Map[String,String] - machineSize: - type: string - description: The azure-specific machine size identifier string. See https://docs.microsoft.com/en-us/azure/virtual-machines/sizes-general - imageUri: - type: string - description: The azure identifier for the vm image uri. Optional, there is an intelligent default. Note this must be in the same region specified in the request Ex, /subscriptions/3efc5bdf-be0e-44e7-b1d7-c08931e3c16c/resourceGroups/mrg-qi-1-preview-20210517084351/providers/Microsoft.Compute/galleries/msdsvm/images/customized_ms_dsvm/versions/0.1.0 - customEnvironmentVariables: - type: object - description: an optional set of key value pairs for environment variables to be injected into the VM - disk: - $ref: "#/components/schemas/AzureDiskConfig" - autopauseThreshold: - type: int - description: an optional number to determine when a runtime should autopause - - AzureDiskConfig: - description: The config for creating an azure disk - type: object - required: - - name - properties: - labels: - type: object - description: The labels to be placed on the runtime. Of type Map[String,String] - name: - type: string - description: The name of the disk in azure - size: - type: integer - description: the size of the disk in GB - diskType: - type: string - description: Not currently supported, but included for future expansion. One of [Standard, SSD]. UpdateGceConfig: description: Configuration for Google Compute Engine instances. @@ -3742,33 +2847,29 @@ components: properties: cloudService: type: string - enum: [GCE, DATAPROC, AZURE_VM] + enum: [GCE, DATAPROC] configType: type: string - enum: [Dataproc, GceConfig, GceWithPdConfig, AzureVmConfig] + enum: [Dataproc, GceConfig, GceWithPdConfig] OneOfRuntimeConfig: oneOf: - $ref: "#/components/schemas/GceConfig" - $ref: "#/components/schemas/GceWithPdConfig" - $ref: "#/components/schemas/DataprocConfig" - - $ref: "#/components/schemas/AzureConfig" discriminator: propertyName: cloudService mapping: gce: "#/components/schemas/GceWithPdConfig" dataproc: "#/components/schemas/DataprocConfig" - azure_vm: "#/components/schemas/AzureConfig" OneOfRuntimeConfigInResponse: oneOf: - $ref: "#/components/schemas/GceWithPdConfigInResponse" - $ref: "#/components/schemas/DataprocConfig" - - $ref: "#/components/schemas/AzureConfig" discriminator: propertyName: cloudService mapping: GCE: "#/components/schemas/GceWithPdConfigInResponse" DATAPROC: "#/components/schemas/DataprocConfig" - AZURE_VM: "#/components/schemas/AzureConfig" RuntimeImage: type: object required: @@ -3959,28 +3060,7 @@ components: description: > Optional, specifies whether to prevent public Internet access from the Dataproc worker nodes, if any. Does not affect the master node. Defaults to false. - AzureConfig: - description: Configuration for an Azure VM - allOf: - - $ref: '#/components/schemas/RuntimeConfig' - - type: object - required: - - machineType - - persistentDiskId - properties: - machineType: - type: string - description: > - Azure machine type describing number of CPUs/Memory as , ex: Standard_DS1_v2. - See https://learn.microsoft.com/en-us/azure/virtual-machines/sizes - persistentDiskId: - type: number - description: > - The id of the persistent disk associated with this runtime (this is Leo's internal ID for the disk) - region: - type: string - description: > - The azure region this resource resides in + UserJupyterExtensionConfig: description: Specification of Jupyter Extensions to be installed on the cluster properties: @@ -4097,7 +3177,7 @@ components: description: Extra arguments to pass to the application. Only used if appType is CUSTOM. workspaceId: type: string - description: Optional id of workspace. Every app must have a workspaceId, but this is not required for backwards-compatibility. Note, if you're using v2 APIs, this field is not used. + description: Optional id of workspace. Every app must have a workspaceId, but this is not required for backwards-compatibility. sourceWorkspaceId: type: string description: Optional id of source workspace if app is a clone. @@ -4136,7 +3216,7 @@ components: threshold time has elapsed. UpdateAppsRequest: - description: a request to update a specific set of apps (v1 or v2) + description: a request to update a specific set of apps required: - appType - cloudProvider @@ -4334,7 +3414,7 @@ components: $ref: '#/components/schemas/CloudProvider' cloudResource: type: string - description: Cloud resource name. Google project if cloud provider is GCP OR Azure's tenantId/subscriptionId/managedResourceGroupId if in Azure + description: Cloud resource name. Google project if cloud provider is GCP. Autopilot: description: Defines app's Autopilot configuration. See https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview for more info. diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala index 6978a5f6a4..2ea5ddeb58 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala @@ -111,6 +111,7 @@ class HttpRoutes( val route: Route = logRequestResult { + // Note that this is Azure-only as in hosted on Azure, not operating on Azure resources enableAzureOnlyRoutes match { case false => Route.seal( From 4f27c44720fefaef404fd56556c3ea5ef5e493ee Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 8 Jul 2025 14:26:16 -0400 Subject: [PATCH 30/43] Test support cleanup --- .../workbench/leonardo/CommonTestData.scala | 17 -- .../leonardo/util/AzureTestUtils.scala | 206 ------------------ 2 files changed, 223 deletions(-) delete mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index 935200d2a3..1c50197101 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -2,8 +2,6 @@ package org.broadinstitute.dsde.workbench.leonardo import akka.http.scaladsl.model.headers.{HttpCookiePair, OAuth2BearerToken} import akka.http.scaladsl.model.{StatusCode, StatusCodes} -import bio.terra.workspace.client.ApiException -import bio.terra.workspace.model.{AzureContext, GcpContext, WorkspaceDescription} import cats.effect.{IO, Ref} import cats.mtl.Ask import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes @@ -512,17 +510,6 @@ object CommonTestData { val wsmResourceId = WsmControlledResourceId(UUID.randomUUID()) val wsmResourceIdOpt = Some(wsmResourceId) val cloudContextAzure = CloudContext.Azure(azureCloudContext) - val billingProfileId = BillingProfileId("spend-profile") - val wsmWorkspaceDesc = new WorkspaceDescription() - .id(workspaceId.value) - .spendProfile("spendProfile") - .azureContext( - new AzureContext() - .resourceGroupId(azureCloudContext.managedResourceGroupName.value) - .tenantId(azureCloudContext.tenantId.value) - .subscriptionId(azureCloudContext.subscriptionId.value) - ) - .gcpContext(new GcpContext().projectId("googleProject")) def modifyInstance(instance: DataprocInstance): DataprocInstance = instance.copy(key = modifyInstanceKey(instance.key), googleId = instance.googleId + 1) @@ -534,7 +521,3 @@ trait GcsPathUtils { def gcsPath(str: String): GcsPath = parseGcsPath(str).right.get } -class TestException(message: String = "Test error", statusCode: StatusCode = StatusCodes.NotFound) - extends ApiException { - override def getCode: Int = statusCode.intValue -} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala deleted file mode 100644 index ccd6f6c7b7..0000000000 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/util/AzureTestUtils.scala +++ /dev/null @@ -1,206 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.util - -import bio.terra.workspace.api.{ControlledAzureResourceApi, ResourceApi, WorkspaceApi} -import bio.terra.workspace.model._ -import cats.effect.IO -import cats.mtl.Ask -import com.azure.resourcemanager.compute.models.{PowerState, VirtualMachine} -import org.broadinstitute.dsde.workbench.azure.AzureCloudContext -import org.broadinstitute.dsde.workbench.azure.mock.FakeAzureVmService -import org.broadinstitute.dsde.workbench.leonardo.AppContext -import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.wsmWorkspaceDesc -import org.broadinstitute.dsde.workbench.model.TraceId -import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.broadinstitute.dsde.workbench.util2.InstanceName -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.when -import org.scalatestplus.mockito.MockitoSugar -import reactor.core.publisher.Mono - -import java.util.UUID -import scala.collection.mutable - -object AzureTestUtils extends MockitoSugar { - implicit val appContext: Ask[IO, AppContext] = AppContext - .lift[IO](None, "") - .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - def setUpMockWsmApiClientProvider( - diskJobStatus: JobReport.StatusEnum = JobReport.StatusEnum.SUCCEEDED, - vmJobStatus: JobReport.StatusEnum = JobReport.StatusEnum.SUCCEEDED, - storageContainerJobStatus: JobReport.StatusEnum = JobReport.StatusEnum.SUCCEEDED, - googleProject: Option[GoogleProject] = None - ): (ControlledAzureResourceApi, ResourceApi, WorkspaceApi) = { - val api = mock[ControlledAzureResourceApi] - val workspaceApi = mock[WorkspaceApi] - val resourceApi = mock[ResourceApi] - val disksByJob = mutable.Map.empty[String, CreateControlledAzureDiskRequestV2Body] - - // Create disk v2 - when { - api.createAzureDiskV2(any, any) - } thenAnswer { invocation => - val requestBody = invocation.getArgument[CreateControlledAzureDiskRequestV2Body](0) - val jobId = requestBody.getJobControl.getId - disksByJob += (jobId -> requestBody) - new CreateControlledAzureResourceResult() - .jobReport( - new JobReport().status(diskJobStatus) - ) - .errorReport(new ErrorReport().message("test exception")) - } - - // Create disk - when { - api.createAzureDisk(any, any) - } thenAnswer { _ => - new CreateControlledAzureResourceResult() - .jobReport( - new JobReport().status(diskJobStatus) - ) - .errorReport(new ErrorReport().message("test exception")) - } - - // Get disk result - when { - api.getCreateAzureDiskResult(any, any) - } thenAnswer { _ => - new CreateControlledAzureResourceResult() - .jobReport( - new JobReport().status(diskJobStatus) - ) - .errorReport(new ErrorReport().message("test exception")) - } - - // Create storage container - when { - api.createAzureStorageContainer(any, any) - } thenAnswer { _ => - if (storageContainerJobStatus == JobReport.StatusEnum.SUCCEEDED) - new CreatedControlledAzureStorageContainer().resourceId(UUID.randomUUID()) - else throw new Exception("storage container failed to create") - } - - // delete disk - when { - api.deleteAzureDisk(any, any, any) - } thenAnswer { _ => - new DeleteControlledAzureResourceResult() - .jobReport( - new JobReport().status(diskJobStatus) - ) - .errorReport(new ErrorReport()) - } - - // delete disk result - when { - api.getDeleteAzureDiskResult(any, any) - } thenAnswer { _ => - new DeleteControlledAzureResourceResult() - .jobReport( - new JobReport().status(diskJobStatus) - ) - .errorReport(new ErrorReport()) - } - - // create vm result - when { - api.createAzureVm(any, any) - } thenAnswer { _ => - new CreatedControlledAzureVmResult() - .jobReport( - new JobReport().status(vmJobStatus) - ) - .azureVm(new AzureVmResource().attributes(new AzureVmAttributes().region("southcentralus"))) - .errorReport(new ErrorReport().message("test exception")) - } - - // create vm result - when { - api.getCreateAzureVmResult(any, any) - } thenAnswer { _ => - new CreatedControlledAzureVmResult() - .jobReport( - new JobReport().status(vmJobStatus) - ) - .azureVm(new AzureVmResource().attributes(new AzureVmAttributes().region("southcentralus"))) - .errorReport(new ErrorReport().message("test exception")) - } - - // delete vm - when { - api.deleteAzureVm(any, any, any) - } thenAnswer { _ => - new DeleteControlledAzureResourceResult() - .jobReport( - new JobReport().status(vmJobStatus) - ) - .errorReport(new ErrorReport()) - } - - // delete vm result - when { - api.getDeleteAzureVmResult(any, any) - } thenAnswer { _ => - new DeleteControlledAzureResourceResult() - .jobReport( - new JobReport().status(vmJobStatus) - ) - .errorReport(new ErrorReport()) - } - - // delete storage container - when { - api.deleteAzureStorageContainer(any, any, any) - } thenAnswer { _ => - new DeleteControlledAzureResourceResult() - .jobReport( - new JobReport().status(storageContainerJobStatus) - ) - .errorReport(new ErrorReport()) - } - - // delete storage container result - when { - api.getDeleteAzureStorageContainerResult(any, any) - } thenAnswer { _ => - new DeleteControlledAzureResourceResult() - .jobReport( - new JobReport().status(storageContainerJobStatus) - ) - .errorReport(new ErrorReport()) - } - - when { - workspaceApi.getWorkspace(any(), any()) - } thenAnswer { invocation => - val workspaceId = invocation.getArgument[UUID](0) - wsmWorkspaceDesc.id(workspaceId) - } - - (api, resourceApi, workspaceApi) - } - - def setupFakeAzureVmService(startVm: Boolean = true, - stopVm: Boolean = true, - vmState: PowerState = PowerState.RUNNING - ): FakeAzureVmService = { - val vmReturn = mock[VirtualMachine] - when(vmReturn.powerState()).thenReturn(vmState) - - new FakeAzureVmService { - override def startAzureVm(name: InstanceName, cloudContext: AzureCloudContext)(implicit - ev: Ask[IO, TraceId] - ): IO[Option[Mono[Void]]] = if (startVm) IO.some(Mono.empty[Void]()) else IO.none - - override def stopAzureVm(name: InstanceName, cloudContext: AzureCloudContext)(implicit - ev: Ask[IO, TraceId] - ): IO[Option[Mono[Void]]] = if (stopVm) IO.some(Mono.empty[Void]()) else IO.none - - override def getAzureVm(name: InstanceName, cloudContext: AzureCloudContext)(implicit - ev: Ask[IO, TraceId] - ): IO[Option[VirtualMachine]] = IO.some(vmReturn) - } - } - -} From fd9fca1b4086f14c541f0b560ca9e07e655dcc27 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Wed, 9 Jul 2025 13:05:15 -0400 Subject: [PATCH 31/43] TODO app update --- .../leonardo/monitor/leoPubsubMessageSubscriberModels.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala index 2ca7aac745..c2eba6b90e 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/leoPubsubMessageSubscriberModels.scala @@ -317,6 +317,7 @@ object LeoPubsubMessage { val messageType: LeoPubsubMessageType = LeoPubsubMessageType.UpdateDisk } + // TODO evaluate whether app update functionality is useful and working for GCP final case class UpdateAppMessage(jobId: UpdateAppJobId, appId: AppId, appName: AppName, From c823dbf70934086de81ac757bf75c5dde277af18 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Wed, 9 Jul 2025 13:15:47 -0400 Subject: [PATCH 32/43] Scalafmt --- .../http/RuntimeRoutesTestJsonCodec.scala | 15 ++++++++++- .../http/GcpDependenciesBuilderSpec.scala | 10 ++++++- .../monitor/LeoMetricsMonitorSpec.scala | 26 ++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala b/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala index 5984b2e451..19b4b63624 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/RuntimeRoutesTestJsonCodec.scala @@ -4,7 +4,20 @@ import io.circe.syntax._ import io.circe.{Decoder, Encoder} import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.RuntimeSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.{AsyncRuntimeFields, AuditInfo, CloudContext, LabelMap, RuntimeConfig, RuntimeError, RuntimeImage, RuntimeName, RuntimeStatus, UserJupyterExtensionConfig, UserScriptPath, WorkspaceId} +import org.broadinstitute.dsde.workbench.leonardo.{ + AsyncRuntimeFields, + AuditInfo, + CloudContext, + LabelMap, + RuntimeConfig, + RuntimeError, + RuntimeImage, + RuntimeName, + RuntimeStatus, + UserJupyterExtensionConfig, + UserScriptPath, + WorkspaceId +} import org.broadinstitute.dsde.workbench.model.WorkbenchEmail import org.broadinstitute.dsde.workbench.model.google.GoogleProject diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index 122b1fd850..ceb660958f 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -12,7 +12,15 @@ import fs2.Stream import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google.{GoogleProjectDAO, HttpGoogleDirectoryDAO, HttpGoogleIamDAO} import org.broadinstitute.dsde.workbench.google2.GKEModels.KubernetesClusterId -import org.broadinstitute.dsde.workbench.google2.{GKEService, GoogleComputeService, GoogleDataprocService, GoogleDiskService, GoogleResourceService, GoogleStorageService, KubernetesService} +import org.broadinstitute.dsde.workbench.google2.{ + GKEService, + GoogleComputeService, + GoogleDataprocService, + GoogleDiskService, + GoogleResourceService, + GoogleStorageService, + KubernetesService +} import org.broadinstitute.dsde.workbench.leonardo.AsyncTaskProcessor.Task import org.broadinstitute.dsde.workbench.leonardo.auth.SamAuthProvider import org.broadinstitute.dsde.workbench.leonardo.dao._ diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index 7f0e8cb1f9..c07853a206 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -12,7 +12,31 @@ import org.broadinstitute.dsde.workbench.leonardo.config.Config import org.broadinstitute.dsde.workbench.leonardo.dao._ import org.broadinstitute.dsde.workbench.leonardo.db.TestComponent import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoMetric._ -import org.broadinstitute.dsde.workbench.leonardo.{AppName, AppStatus, AppType, Chart, CloudContext, CloudProvider, IpRange, KubernetesCluster, KubernetesClusterAsyncFields, KubernetesService, KubernetesServiceKindName, LeonardoTestSuite, NetworkFields, RuntimeContainerServiceType, RuntimeImage, RuntimeImageType, RuntimeMetrics, RuntimeName, RuntimeStatus, RuntimeUI, ServiceConfig, ServiceId, WorkspaceId} +import org.broadinstitute.dsde.workbench.leonardo.{ + AppName, + AppStatus, + AppType, + Chart, + CloudContext, + CloudProvider, + IpRange, + KubernetesCluster, + KubernetesClusterAsyncFields, + KubernetesService, + KubernetesServiceKindName, + LeonardoTestSuite, + NetworkFields, + RuntimeContainerServiceType, + RuntimeImage, + RuntimeImageType, + RuntimeMetrics, + RuntimeName, + RuntimeStatus, + RuntimeUI, + ServiceConfig, + ServiceId, + WorkspaceId +} import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{IP, TraceId} import org.mockito.ArgumentMatchers.{any, anyString} From 03cefaae6a34b533d7e679aa14f7b50b082b1ab7 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 10 Jul 2025 13:05:04 -0400 Subject: [PATCH 33/43] Try adding removed dependencies back in --- project/Dependencies.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 0b4e5d207d..c402ed3fd0 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -111,7 +111,8 @@ object Dependencies { val workbenchOauth2Tests: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-oauth2" % workbenchOauth2V % "test" classifier "tests" val workbenchGoogleTest: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-google" % workbenchGoogleV % "test" classifier "tests" excludeAll (excludeGuava, excludeStatsD) val workbenchGoogle2Test: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-google2" % workbenchGoogle2V % "test" classifier "tests" excludeAll (excludeGuava) //for generators - val workbenchOpenTelemetry: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-opentelemetry" % workbenchOpenTelemetryV excludeAll ( + val + workbenchOpenTelemetry: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-opentelemetry" % workbenchOpenTelemetryV excludeAll ( excludeIoGrpc, excludeGuava ) @@ -159,10 +160,14 @@ object Dependencies { def excludeLiquibase = ExclusionRule("org.liquibase", "liquibase-core") def excludeFlagsmith = ExclusionRule("com.flagsmith", "flagsmith-java-client") + val workSpaceManagerV = "0.254.1127-SNAPSHOT" + val bpmV = "0.1.548-SNAPSHOT" // [IA-4939] commons-text:1.9 is unsafe def excludeCommonsText = ExclusionRule("org.apache.commons", "commons-text") def tclExclusions(m: ModuleID): ModuleID = m.excludeAll(excludeSpringBoot, excludeSpringAop, excludeSpringData, excludeSpringFramework, excludeOpenCensus, excludeGoogleFindBugs, excludeBroadWorkbench, excludePostgresql, excludeSnakeyaml, excludeSlf4j, excludeCommonsText, excludeLiquibase, excludeOpenTelemetry, excludeFlagsmith) + val workspaceManager = excludeJakarta("bio.terra" % "workspace-manager-client" % workSpaceManagerV) + val bpm = excludeJakarta("bio.terra" % "billing-profile-manager-client" % bpmV) val terraCommonLib = tclExclusions(excludeJakarta("bio.terra" % "terra-common-lib" % terraCommonLibV classifier "plain")) val sam = excludeJakarta("org.broadinstitute.dsde.workbench" %% "sam-client" % samV) From 801a2077e92fcab884e729a2a7cc4c059f32072e Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 10 Jul 2025 15:01:46 -0400 Subject: [PATCH 34/43] Actually add removed dependencies back in --- project/Dependencies.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c402ed3fd0..5eb7fdb347 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -111,8 +111,7 @@ object Dependencies { val workbenchOauth2Tests: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-oauth2" % workbenchOauth2V % "test" classifier "tests" val workbenchGoogleTest: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-google" % workbenchGoogleV % "test" classifier "tests" excludeAll (excludeGuava, excludeStatsD) val workbenchGoogle2Test: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-google2" % workbenchGoogle2V % "test" classifier "tests" excludeAll (excludeGuava) //for generators - val - workbenchOpenTelemetry: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-opentelemetry" % workbenchOpenTelemetryV excludeAll ( + val workbenchOpenTelemetry: ModuleID = "org.broadinstitute.dsde.workbench" %% "workbench-opentelemetry" % workbenchOpenTelemetryV excludeAll ( excludeIoGrpc, excludeGuava ) @@ -199,6 +198,8 @@ object Dependencies { workbenchAzure, workbenchAzureTest, logbackClassic, + workspaceManager, + bpm, terraCommonLib, sam ) From 1f47e52b7494230daea8a2682fc1c7125c76cdc2 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Fri, 11 Jul 2025 09:37:54 -0400 Subject: [PATCH 35/43] Import cleanup --- .../leonardo/LeonardoApiClient.scala | 1 - .../leonardo/azure/AzureDiskSpec.scala | 10 +------ .../leonardo/db/RuntimeServiceDbQueries.scala | 2 +- .../workbench/leonardo/CommonTestData.scala | 28 ++----------------- .../db/RuntimeServiceDbQueriesSpec.scala | 7 +---- .../http/GcpDependenciesBuilderSpec.scala | 1 - .../http/service/DiskServiceInterpSpec.scala | 6 +--- .../monitor/LeoMetricsMonitorSpec.scala | 2 -- .../leonardo/monitor/LeoPubsubCodecSpec.scala | 4 +-- 9 files changed, 8 insertions(+), 53 deletions(-) diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala index ea9607ea32..cab1f31798 100644 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala +++ b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoApiClient.scala @@ -1,7 +1,6 @@ package org.broadinstitute.dsde.workbench.leonardo import cats.effect.{IO, Resource} -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes import org.broadinstitute.dsde.workbench.DoneCheckable import org.broadinstitute.dsde.workbench.DoneCheckableSyntax._ import org.broadinstitute.dsde.workbench.google2.{ diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala index 28db4096bb..4a4b6a63f2 100644 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala +++ b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala @@ -9,16 +9,8 @@ import org.broadinstitute.dsde.workbench.google2.streamUntilDoneOrTimeout import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestTags.ExcludeFromJenkins import org.broadinstitute.dsde.workbench.leonardo.SSH.SSHRuntimeInfo import org.broadinstitute.dsde.workbench.leonardo.TestUser.Hermione -import org.broadinstitute.dsde.workbench.leonardo.{AzureBilling, LeonardoTestUtils} +import org.broadinstitute.dsde.workbench.leonardo.{AzureBilling, CloudProvider, LeonardoConfig, LeonardoTestUtils, RuntimeName, SSH} import org.broadinstitute.dsde.workbench.service.test.CleanUp -import org.broadinstitute.dsde.workbench.leonardo.{ - AzureBilling, - CloudProvider, - LeonardoConfig, - LeonardoTestUtils, - RuntimeName, - SSH -} import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.{DoNotDiscover, ParallelTestExecution, Retries} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueries.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueries.scala index 80f5179cc7..35f5110dc6 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueries.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueries.scala @@ -5,7 +5,7 @@ import cats.data.Chain import cats.syntax.all._ import org.broadinstitute.dsde.workbench.azure.AzureCloudContext import org.broadinstitute.dsde.workbench.google2.OperationName -import org.broadinstitute.dsde.workbench.leonardo.{LabelMap, Runtime} +import org.broadinstitute.dsde.workbench.leonardo.LabelMap import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.RuntimeSamResourceId import org.broadinstitute.dsde.workbench.leonardo.config.Config import org.broadinstitute.dsde.workbench.leonardo.db.LeoProfile.api._ diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index 1c50197101..e49f161cdf 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -1,10 +1,8 @@ package org.broadinstitute.dsde.workbench.leonardo import akka.http.scaladsl.model.headers.{HttpCookiePair, OAuth2BearerToken} -import akka.http.scaladsl.model.{StatusCode, StatusCodes} import cats.effect.{IO, Ref} import cats.mtl.Ask -import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes import com.google.auth.oauth2.{AccessToken, GoogleCredentials} import com.google.cloud.compute.v1.Instance.Status import com.google.cloud.compute.v1._ @@ -12,36 +10,16 @@ import com.typesafe.config.ConfigFactory import net.ceedubs.ficus.Ficus._ import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.mock.BaseFakeGoogleStorage -import org.broadinstitute.dsde.workbench.google2.{ - DataprocRole, - DiskName, - MachineTypeName, - NetworkName, - OperationName, - RegionName, - SubnetworkName, - ZoneName -} +import org.broadinstitute.dsde.workbench.google2.{DataprocRole, DiskName, MachineTypeName, NetworkName, OperationName, RegionName, SubnetworkName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo import org.broadinstitute.dsde.workbench.leonardo.ContainerRegistry.DockerHub -import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{ - BootSource, - CryptoDetector, - Jupyter, - Proxy, - RStudio, - Welder -} +import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{BootSource, CryptoDetector, Jupyter, Proxy, RStudio, Welder} import org.broadinstitute.dsde.workbench.leonardo.SamResourceId._ import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.dao.MockSamDAO import org.broadinstitute.dsde.workbench.leonardo.db.ClusterRecord -import org.broadinstitute.dsde.workbench.leonardo.http.{ - userScriptStartupOutputUriMetadataKey, - CreateRuntimeRequest, - RuntimeConfigRequest -} +import org.broadinstitute.dsde.workbench.leonardo.http.{CreateRuntimeRequest, RuntimeConfigRequest, userScriptStartupOutputUriMetadataKey} import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google._ import org.broadinstitute.dsde.workbench.oauth2.mock.FakeOpenIDConnectConfiguration diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueriesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueriesSpec.scala index 3df9fcdbde..54bcc99464 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueriesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/db/RuntimeServiceDbQueriesSpec.scala @@ -4,11 +4,7 @@ package db import cats.effect.IO import org.broadinstitute.dsde.workbench.google2.{DiskName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo.CommonTestData.{makeCluster, _} -import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.{ - ProjectSamResourceId, - RuntimeSamResourceId, - WorkspaceResourceSamResourceId -} +import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.{RuntimeSamResourceId, WorkspaceResourceSamResourceId} import org.broadinstitute.dsde.workbench.leonardo.config.Config import org.broadinstitute.dsde.workbench.leonardo.db.RuntimeServiceDbQueries._ import org.broadinstitute.dsde.workbench.leonardo.http._ @@ -19,7 +15,6 @@ import org.scalatest.flatspec.AnyFlatSpecLike import java.time.Instant import java.util.UUID -import scala.collection.immutable.List import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala index ceb660958f..669ac61326 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/GcpDependenciesBuilderSpec.scala @@ -9,7 +9,6 @@ import com.google.api.gax.longrunning.OperationFuture import com.google.auth.oauth2.GoogleCredentials import com.google.cloud.compute.v1.Operation import fs2.Stream -import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google.{GoogleProjectDAO, HttpGoogleDirectoryDAO, HttpGoogleIamDAO} import org.broadinstitute.dsde.workbench.google2.GKEModels.KubernetesClusterId import org.broadinstitute.dsde.workbench.google2.{ diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskServiceInterpSpec.scala index 13fa6b8a9c..cc20045755 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskServiceInterpSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/DiskServiceInterpSpec.scala @@ -8,16 +8,14 @@ import cats.effect.IO import cats.mtl.Ask import com.google.api.services.cloudresourcemanager.model.{Ancestor, ResourceId} import com.google.cloud.compute.v1.Disk -import org.broadinstitute.dsde.workbench.client.sam.ApiException import org.broadinstitute.dsde.workbench.google.GoogleProjectDAO import org.broadinstitute.dsde.workbench.google.mock.MockGoogleProjectDAO import org.broadinstitute.dsde.workbench.google2.mock.MockGoogleDiskService import org.broadinstitute.dsde.workbench.google2.{DiskName, GoogleDiskService, MachineTypeName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ -import org.broadinstitute.dsde.workbench.leonardo.PersistentDiskAction.ReadPersistentDisk import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.{PersistentDiskSamResourceId, ProjectSamResourceId} import org.broadinstitute.dsde.workbench.leonardo.TestUtils.defaultMockitoAnswer -import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider +import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamException, SamService} import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.model._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage._ @@ -26,7 +24,6 @@ import org.broadinstitute.dsde.workbench.leonardo.util.QueueFactory import org.broadinstitute.dsde.workbench.model import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{TraceId, UserInfo, WorkbenchEmail, WorkbenchUserId} -import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.{any, eq => isEq} import org.mockito.Mockito._ import org.scalatest.flatspec.AnyFlatSpec @@ -36,7 +33,6 @@ import java.time.Instant import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamException, SamService} trait DiskServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { val emptyCreateDiskReq = CreateDiskRequest( diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala index c07853a206..43998fa0ee 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoMetricsMonitorSpec.scala @@ -2,8 +2,6 @@ package org.broadinstitute.dsde.workbench.leonardo.monitor import cats.effect.IO import cats.effect.unsafe.IORuntime -import io.kubernetes.client.openapi.models._ -import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.ServiceName import org.broadinstitute.dsde.workbench.google2.{NetworkName, SubnetworkName} import org.broadinstitute.dsde.workbench.leonardo.KubernetesTestData.{makeApp, makeKubeCluster, makeNodepool} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala index 5e7b4066fc..918707dc8e 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/monitor/LeoPubsubCodecSpec.scala @@ -4,11 +4,9 @@ package monitor import _root_.io.circe.parser.decode import _root_.io.circe.syntax._ import io.circe.Printer -import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.KubernetesSerializableName.NamespaceName -import org.broadinstitute.dsde.workbench.google2.{DiskName, MachineTypeName, NetworkName, SubnetworkName, ZoneName} +import org.broadinstitute.dsde.workbench.google2.{DiskName, MachineTypeName, ZoneName} import org.broadinstitute.dsde.workbench.leonardo.AppType.Galaxy -import org.broadinstitute.dsde.workbench.leonardo.JsonCodec._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubCodec._ import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{CreateAppMessage, CreateRuntimeMessage} import org.broadinstitute.dsde.workbench.model.google.{GcsBucketName, GoogleProject} From 95de5b719cdbeb9d42e121d7353aeaf7f35b8340 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Fri, 11 Jul 2025 09:47:11 -0400 Subject: [PATCH 36/43] Restore cloud-agnostic v2 list runtimes route --- http/src/main/resources/swagger/api-docs.yaml | 51 +++ .../http/AppDependenciesBuilder.scala | 3 + .../dsde/workbench/leonardo/http/Boot.scala | 1 + .../http/CloudDependenciesBuilder.scala | 1 + .../leonardo/http/api/HttpRoutes.scala | 6 +- .../leonardo/http/api/RuntimeV2Routes.scala | 64 +++ .../http/service/RuntimeV2Service.scala | 17 + .../http/service/RuntimeV2ServiceInterp.scala | 50 +++ .../leonardo/http/api/HttpRoutesSpec.scala | 7 + .../leonardo/http/api/ProxyRoutesSpec.scala | 2 + .../leonardo/http/api/TestLeoRoutes.scala | 2 + .../http/service/MockRuntimeV2Interp.scala | 40 ++ .../service/RuntimeV2ServiceInterpSpec.scala | 410 ++++++++++++++++++ .../leonardo/provider/LeoProvider.scala | 1 + 14 files changed, 653 insertions(+), 2 deletions(-) create mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala create mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala create mode 100644 http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala create mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala create mode 100644 http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala diff --git a/http/src/main/resources/swagger/api-docs.yaml b/http/src/main/resources/swagger/api-docs.yaml index c4e5c98c0c..c03416fb44 100644 --- a/http/src/main/resources/swagger/api-docs.yaml +++ b/http/src/main/resources/swagger/api-docs.yaml @@ -503,6 +503,57 @@ paths: schema: $ref: "#/components/schemas/ErrorReport" + "/api/v2/runtimes": + get: + summary: List all runtimes that the caller has access to + description: List all runtimes, optionally filtering on a set of labels + operationId: listRuntimesV2 + tags: + - runtimes + parameters: + - in: query + name: _labels + description: > + Optional label key-value pairs to filter results by. Example: + Querying by key1=val1,key2=val2 + returns all azure runtimes that contain the key1/val1 and key2/val2 labels (possibly among other labels). + Note: this string format is a workaround because Swagger doesn't support free-form + query string parameters. The recommended way to use this endpoint is to specify the + labels as top-level query string parameters. For instance: GET /api/v2/runtimes?key1=val1&key2=val2. + required: false + schema: + type: string + - in: query + name: includeLabels + description: > + Optional label keys of the labels returned in response. Example: + Querying by key1,key2,key3 + returns all labels key1/val1 key2/val2 and key3 and val3 for each runtime response + required: false + schema: + type: string + responses: + "200": + description: List of runtimes + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ListRuntimeResponse" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "500": + description: Internal Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + ## Persistent Disk paths ## /api/google/v1/disks: diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index d3e55be718..e6817ed1e0 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -88,6 +88,8 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu ): Resource[IO, ServicesDependencies] = { val statusService = new StatusService(baselineDependencies.samDAO, dbReference) + val runtimeV2Service = new RuntimeV2ServiceInterp[IO](baselineDependencies.samService) + val adminService = new AdminServiceInterp[IO](baselineDependencies.authProvider, baselineDependencies.publisherQueue) @@ -104,6 +106,7 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu dependenciesRegistry, leoKubernetesService, adminService, + runtimeV2Service, StandardUserInfoDirectives, contentSecurityPolicy, refererConfig, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala index 8b7837f783..5c23406535 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/Boot.scala @@ -63,6 +63,7 @@ object Boot extends IOApp { servicesDependencies.statusService, servicesDependencies.cloudSpecificDependenciesRegistry, servicesDependencies.kubernetesService, + servicesDependencies.runtimeV2Service, servicesDependencies.adminService, StandardUserInfoDirectives, contentSecurityPolicy, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala index e27dace6c8..aadcfd7e1b 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/CloudDependenciesBuilder.scala @@ -74,6 +74,7 @@ final case class ServicesDependencies( cloudSpecificDependenciesRegistry: ServicesRegistry, kubernetesService: AppService[IO], adminService: AdminService[IO], + runtimeV2Service: RuntimeV2Service[IO], userInfoDirectives: UserInfoDirectives, contentSecurityPolicy: ContentSecurityPolicyConfig, refererConfig: RefererConfig, diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala index 2ea5ddeb58..dcd712a76b 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutes.scala @@ -32,6 +32,7 @@ class HttpRoutes( statusService: StatusService, gcpOnlyServicesRegistry: ServicesRegistry, kubernetesService: AppService[IO], + runtimeV2Service: RuntimeV2Service[IO], adminService: AdminService[IO], userInfoDirectives: UserInfoDirectives, contentSecurityPolicy: ContentSecurityPolicyConfig, @@ -43,6 +44,7 @@ class HttpRoutes( private val corsSupport = new CorsSupport(contentSecurityPolicy, refererConfig) private val kubernetesRoutes = new AppRoutes(kubernetesService, userInfoDirectives) private val appRoutes = createAppRoutesUsingServicesRegistry + private val runtimeV2Routes = new RuntimeV2Routes(runtimeV2Service, userInfoDirectives) private val adminRoutes = new AdminRoutes(adminService, userInfoDirectives) private val diskRoutes = createDiskRoutesUsingServicesRegistry private val runtimeRoutes = createRuntimeRoutesUsingServicesRegistry @@ -120,7 +122,7 @@ class HttpRoutes( "swagger/api-docs.yaml" ) ~ oidcConfig.oauth2Routes ~ proxyRoutes.get.route ~ statusRoutes.route ~ pathPrefix("api") { - runtimeRoutes.get.routes ~ diskRoutes.get.routes ~ kubernetesRoutes.routes ~ + runtimeRoutes.get.routes ~ runtimeV2Routes.routes ~ diskRoutes.get.routes ~ kubernetesRoutes.routes ~ adminRoutes.routes ~ resourcesRoutes.get.routes } ) @@ -129,7 +131,7 @@ class HttpRoutes( oidcConfig .swaggerRoutes("swagger/api-docs.yaml") ~ oidcConfig.oauth2Routes ~ statusRoutes.route ~ pathPrefix("api") { - runtimeRoutes.get.routes ~ diskRoutes.get.routes ~ appRoutes.get.routes ~ adminRoutes.routes + runtimeRoutes.get.routes ~ runtimeV2Routes.routes ~ diskRoutes.get.routes ~ appRoutes.get.routes ~ adminRoutes.routes } ) } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala new file mode 100644 index 0000000000..9c67b169de --- /dev/null +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala @@ -0,0 +1,64 @@ +package org.broadinstitute.dsde.workbench.leonardo +package http +package api + +import akka.http.scaladsl.marshalling.ToResponseMarshallable +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server +import akka.http.scaladsl.server.Directives._ +import cats.effect.IO +import cats.mtl.Ask +import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ +import io.opencensus.scala.akka.http.TracingDirective.traceRequestForService +import org.broadinstitute.dsde.workbench.leonardo.http.RuntimeRoutesCodec._ +import org.broadinstitute.dsde.workbench.leonardo.http.service.RuntimeV2Service +import org.broadinstitute.dsde.workbench.model.UserInfo +import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics + +class RuntimeV2Routes(runtimeV2Service: RuntimeV2Service[IO], userInfoDirectives: UserInfoDirectives)(implicit + metrics: OpenTelemetryMetrics[IO] +) { + + val routes: server.Route = traceRequestForService(serviceData) { span => + extractAppContext(Some(span)) { implicit ctx => + userInfoDirectives.requireUserInfo { userInfo => + CookieSupport.setTokenCookie(userInfo) { + pathPrefix("v2" / "runtimes") { + pathEndOrSingleSlash { + parameterMap { params => + get { + complete( + listRuntimesHandler( + userInfo, + None, + None, + params + ) + ) + } + } + } + } + } + } + } + } + + private[api] def listRuntimesHandler(userInfo: UserInfo, + workspaceId: Option[WorkspaceId], + cloudProvider: Option[CloudProvider], + params: Map[String, String] + )(implicit + ev: Ask[IO, AppContext] + ): IO[ToResponseMarshallable] = + for { + ctx <- ev.ask[AppContext] + apiCall = runtimeV2Service.listRuntimes(userInfo, workspaceId, cloudProvider, params) + _ <- metrics.incrementCounter("listRuntimeV2") + resp <- ctx.span.fold(apiCall)(span => + spanResource[IO](span, "listRuntimeV2") + .use(_ => apiCall) + ) + } yield StatusCodes.OK -> resp: ToResponseMarshallable + +} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala new file mode 100644 index 0000000000..ff4edef6b5 --- /dev/null +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala @@ -0,0 +1,17 @@ +package org.broadinstitute.dsde.workbench.leonardo +package http +package service + +import cats.mtl.Ask +import org.broadinstitute.dsde.workbench.model.UserInfo + +trait RuntimeV2Service[F[_]] { + + def listRuntimes(userInfo: UserInfo, + workspaceId: Option[WorkspaceId], + cloudProvider: Option[CloudProvider], + params: Map[String, String] + )(implicit + as: Ask[F, AppContext] + ): F[Vector[ListRuntimeResponse2]] +} diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala new file mode 100644 index 0000000000..37588d247d --- /dev/null +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala @@ -0,0 +1,50 @@ +package org.broadinstitute.dsde.workbench.leonardo +package http +package service + +import cats.Parallel +import cats.effect.Async +import cats.mtl.Ask +import cats.syntax.all._ +import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.RuntimeSamResourceId +import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService +import org.broadinstitute.dsde.workbench.leonardo.db._ +import org.broadinstitute.dsde.workbench.leonardo.model.SamResource.RuntimeSamResource +import org.broadinstitute.dsde.workbench.model.UserInfo + +import scala.concurrent.ExecutionContext + +class RuntimeV2ServiceInterp[F[_]: Parallel]( + samService: SamService[F] +)(implicit F: Async[F], dbReference: DbReference[F], ec: ExecutionContext) + extends RuntimeV2Service[F] { + + override def listRuntimes( + userInfo: UserInfo, + workspaceId: Option[WorkspaceId], + cloudProvider: Option[CloudProvider], + params: Map[String, String] + )(implicit as: Ask[F, AppContext]): F[Vector[ListRuntimeResponse2]] = + for { + ctx <- as.ask + + // Parameters: parse search filters from request + (labelMap, _, _) <- F.fromEither(processListParameters(params)) + excludeStatuses = List(RuntimeStatus.Deleted) + creatorEmail <- F.fromEither(processCreatorOnlyParameter(userInfo.userEmail, params, ctx.traceId)) + + samResources <- samService.listResources(userInfo.accessToken.token, RuntimeSamResource.resourceType) + runtimes <- RuntimeServiceDbQueries + .listRuntimes( + runtimeIds = samResources.map(RuntimeSamResourceId).toSet, + cloudProvider = cloudProvider, + creatorEmail = creatorEmail, + excludeStatuses = excludeStatuses, + labelMap = labelMap, + workspaceId = workspaceId + ) + .map(_.toList) + .transaction + + } yield runtimes.toVector +} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala index 9f9e8ece7f..f477a46ec3 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala @@ -63,6 +63,7 @@ class HttpRoutesSpec statusService, createGcpOnlyServicesRegistry(), MockAppService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -74,6 +75,7 @@ class HttpRoutesSpec statusService, createGcpOnlyServicesRegistry(), MockAppService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -87,6 +89,7 @@ class HttpRoutesSpec statusService, createGcpOnlyServicesRegistry(), MockAppService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -99,6 +102,7 @@ class HttpRoutesSpec statusService, createGcpOnlyServicesRegistry(), MockAppService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -111,6 +115,7 @@ class HttpRoutesSpec statusService, createGcpOnlyServicesRegistry(), MockAppService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -740,6 +745,7 @@ class HttpRoutesSpec statusService, gcpOnlyServicesRegistry, MockAppService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, @@ -759,6 +765,7 @@ class HttpRoutesSpec statusService, gcpOnlyServicesRegistry, kubernetesService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala index 4488c33819..939d46d697 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/ProxyRoutesSpec.scala @@ -554,6 +554,7 @@ class ProxyRoutesSpec statusService, gcpOnlyServicesRegistry, leoKubernetesService, + MockRuntimeV2Interp, MockAdminServiceInterp, userInfoDirectives, contentSecurityPolicy, @@ -729,6 +730,7 @@ class ProxyRoutesSpec statusService, gcpOnlyServicesRegistry, leoKubernetesService, + MockRuntimeV2Interp, MockAdminServiceInterp, userInfoDirectives, contentSecurityPolicy, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala index de208d6658..5f0b83c886 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/TestLeoRoutes.scala @@ -196,6 +196,7 @@ trait TestLeoRoutes { statusService, gcpOnlyServicesRegistry, leoKubernetesService, + MockRuntimeV2Interp, MockAdminServiceInterp, userInfoDirectives, contentSecurityPolicy, @@ -208,6 +209,7 @@ trait TestLeoRoutes { statusService, gcpOnlyServicesRegistry, leoKubernetesService, + MockRuntimeV2Interp, MockAdminServiceInterp, timedUserInfoDirectives, contentSecurityPolicy, diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala new file mode 100644 index 0000000000..482c1c4244 --- /dev/null +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala @@ -0,0 +1,40 @@ +package org.broadinstitute.dsde.workbench.leonardo +package http +package service + +import cats.effect.IO +import cats.mtl.Ask +import com.azure.resourcemanager.compute.models.VirtualMachineSizeTypes +import org.broadinstitute.dsde.workbench.google2.MachineTypeName +import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ +import org.broadinstitute.dsde.workbench.model.UserInfo + +object MockRuntimeV2Interp extends RuntimeV2Service[IO] { + + override def listRuntimes( + userInfo: UserInfo, + workspaceId: Option[WorkspaceId], + cloudProvider: Option[CloudProvider], + params: Map[String, String] + )(implicit as: Ask[IO, AppContext]): IO[Vector[ListRuntimeResponse2]] = + IO.pure( + Vector( + ListRuntimeResponse2( + CommonTestData.testCluster.id, + Some(CommonTestData.workspaceId), + CommonTestData.testCluster.samResource, + RuntimeName("azureruntime1"), + CloudContext.Azure(azureCloudContext), + CommonTestData.testCluster.auditInfo, + RuntimeConfig.AzureConfig(MachineTypeName(VirtualMachineSizeTypes.STANDARD_A0.toString), + Some(DiskId(-1)), + None + ), + CommonTestData.testCluster.proxyUrl, + CommonTestData.testCluster.status, + CommonTestData.testCluster.labels, + CommonTestData.testCluster.patchInProgress + ) + ) + ) +} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala new file mode 100644 index 0000000000..923d5c64aa --- /dev/null +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala @@ -0,0 +1,410 @@ +package org.broadinstitute.dsde.workbench.leonardo +package http +package service + +import akka.http.scaladsl.model.headers.OAuth2BearerToken +import cats.effect.IO +import org.broadinstitute.dsde.workbench.azure._ +import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ +import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.{RuntimeSamResourceId, WsmResourceSamResourceId} +import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext +import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService +import org.broadinstitute.dsde.workbench.leonardo.db._ +import org.broadinstitute.dsde.workbench.leonardo.model.SamResource.RuntimeSamResource +import org.broadinstitute.dsde.workbench.leonardo.model._ +import org.broadinstitute.dsde.workbench.model.google.GoogleProject +import org.broadinstitute.dsde.workbench.model.{UserInfo, WorkbenchEmail, WorkbenchUserId} +import org.mockito.ArgumentMatchers.{any, eq => isEq} +import org.mockito.Mockito.when +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatestplus.mockito.MockitoSugar + +import java.util.UUID +import scala.concurrent.ExecutionContext.Implicits.global + +class RuntimeV2ServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { + + def makeInterp(samService: SamService[IO] = MockSamService) = new RuntimeV2ServiceInterp[IO](samService) + + def setRuntimeDeleted(workspaceId: WorkspaceId, name: RuntimeName): IO[Long] = + for { + now <- IO.realTimeInstant + runtime <- RuntimeServiceDbQueries + .getRuntimeByWorkspaceId(workspaceId, name) + .transaction + + _ <- clusterQuery + .completeDeletion(runtime.id, now) + .transaction + } yield runtime.id + + def mockUserInfo(email: String = userEmail.toString()): UserInfo = + UserInfo(OAuth2BearerToken(""), WorkbenchUserId(s"userId-${email}"), WorkbenchEmail(email), 0) + + val runtimeV2Service = + new RuntimeV2ServiceInterp[IO]( + MockSamService + ) + + val runtimeV2Service2 = + new RuntimeV2ServiceInterp[IO]( + MockSamService + ) + + it should "list runtimes" in isolatedDbTest { + val runtimeId1 = UUID.randomUUID.toString + val runtimeId2 = UUID.randomUUID.toString + val projectIdGcp = cloudContextGcp.asString + val workspaceIdAzure = UUID.randomUUID.toString + + val samService = mock[SamService[IO]] + when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) + .thenReturn(IO.pure(List(runtimeId1, runtimeId2))) + val testService = makeInterp(samService = samService) + + val res = for { + samResource1 <- IO(RuntimeSamResourceId(runtimeId1)) + samResource2 <- IO(RuntimeSamResourceId(runtimeId2)) + // GCP runtime + runtime1 <- IO(makeCluster(1).copy(samResource = samResource1, workspaceId = workspaceIdOpt).save()) + // Azure runtime + runtime2 <- IO( + makeCluster(2) + .copy( + samResource = samResource2, + cloudContext = CloudContext.Azure(CommonTestData.azureCloudContext), + workspaceId = Some(WorkspaceId(UUID.fromString(workspaceIdAzure))) + ) + .save() + ) + listResponse <- testService.listRuntimes(userInfo, None, None, Map.empty) + } yield { + listResponse.map(_.samResource).toSet shouldBe Set(samResource1, samResource2) + listResponse should contain( + ListRuntimeResponse2( + id = runtime1.id, + workspaceId = workspaceIdOpt, + samResource = runtime1.samResource, + clusterName = runtime1.runtimeName, + cloudContext = runtime1.cloudContext, + auditInfo = runtime1.auditInfo, + runtimeConfig = defaultDataprocRuntimeConfig, + proxyUrl = Runtime + .getProxyUrl(proxyUrlBase, cloudContextGcp, runtime1.runtimeName, Set(jupyterImage), None, Map.empty), + runtime1.status, + runtime1.labels, + runtime1.patchInProgress + ) + ) + } + + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "list runtimes with a workspace and/or cloudProvider" in isolatedDbTest { + val runtimeId1 = UUID.randomUUID.toString + val runtimeId2 = UUID.randomUUID.toString + val runtimeId3 = UUID.randomUUID.toString + val runtimeId4 = UUID.randomUUID.toString + val runtimeId5 = UUID.randomUUID.toString + val projectIdGcp1 = "gcp-context-1" + val projectIdGcp2 = "gcp-context-2" + val workspaceId1 = UUID.randomUUID.toString + val workspaceId2 = UUID.randomUUID.toString + val workspaceId3 = UUID.randomUUID.toString + + val samService = mock[SamService[IO]] + when(samService.listResources(any(), isEq(RuntimeSamResource.resourceType))(any())) + .thenReturn(IO.pure(List(runtimeId1, runtimeId2, runtimeId3, runtimeId4, runtimeId5))) + + val testService = makeInterp(samService = samService) + + val res = for { + samResource1 <- IO(RuntimeSamResourceId(runtimeId1)) + samResource2 <- IO(RuntimeSamResourceId(runtimeId2)) + samResource3 <- IO(RuntimeSamResourceId(runtimeId3)) + samResource4 <- IO(RuntimeSamResourceId(runtimeId4)) + samResource5 <- IO(RuntimeSamResourceId(runtimeId5)) + workspace1 <- IO(WorkspaceId(UUID.fromString(workspaceId1))) + workspace2 <- IO(WorkspaceId(UUID.fromString(workspaceId2))) + workspace3 <- IO(WorkspaceId(UUID.fromString(workspaceId3))) + + // hidden runtime 1, owned workspace 1, Azure + _ <- IO( + makeCluster(1) + .copy( + samResource = samResource1, + workspaceId = Some(workspace1), + cloudContext = CloudContext.Azure( + AzureCloudContext( + TenantId(workspaceId1), + SubscriptionId(workspaceId1), + ManagedResourceGroupName(workspaceId1) + ) + ) + ) + .save() + ) + // hidden runtime 2, read workspace 2, owned project 1, Gcp + _ <- IO( + makeCluster(2) + .copy( + samResource = samResource2, + workspaceId = Some(workspace2), + cloudContext = CloudContext.Gcp(GoogleProject(projectIdGcp1)) + ) + .save() + ) + // read runtime 3, read workspace 2, owned project 1, Gcp + _ <- IO( + makeCluster(3) + .copy( + samResource = samResource3, + workspaceId = Some(workspace2), + cloudContext = CloudContext.Gcp(GoogleProject(projectIdGcp1)) + ) + .save() + ) + // read runtime 4, read workspace 3, Azure + _ <- IO( + makeCluster(4) + .copy( + samResource = samResource4, + workspaceId = Some(workspace3), + cloudContext = CloudContext.Azure( + AzureCloudContext( + TenantId(workspaceId3), + SubscriptionId(workspaceId3), + ManagedResourceGroupName(workspaceId3) + ) + ) + ) + .save() + ) + // read runtime 5, read project 2, Gcp + _ <- IO( + makeCluster(5) + .copy(samResource = samResource5, cloudContext = CloudContext.Gcp(GoogleProject(projectIdGcp2))) + .save() + ) + + responseIdsWorkspace1 <- testService.listRuntimes(userInfo, Some(workspace1), None, Map.empty) + responseIdsWorkspace2 <- testService.listRuntimes(userInfo, Some(workspace2), None, Map.empty) + responseIdsWorkspace3 <- testService.listRuntimes(userInfo, Some(workspace3), None, Map.empty) + responseIdsAzure <- testService.listRuntimes(userInfo, None, Some(CloudProvider.Azure), Map.empty) + responseIdsGcp <- testService.listRuntimes(userInfo, None, Some(CloudProvider.Gcp), Map.empty) + responseIdsAzureWorkspace1 <- testService.listRuntimes(userInfo, + Some(workspace1), + Some(CloudProvider.Azure), + Map.empty + ) + responseIdsAzureWorkspace2 <- testService.listRuntimes(userInfo, + Some(workspace2), + Some(CloudProvider.Azure), + Map.empty + ) + responseIdsGcpWorkspace1 <- testService.listRuntimes(userInfo, + Some(workspace1), + Some(CloudProvider.Gcp), + Map.empty + ) + responseIdsGcpWorkspace2 <- testService.listRuntimes(userInfo, + Some(workspace2), + Some(CloudProvider.Gcp), + Map.empty + ) + } yield { + responseIdsWorkspace1.map(_.samResource).toSet shouldBe Set(samResource1) + responseIdsWorkspace2.map(_.samResource).toSet shouldBe Set(samResource2, samResource3) + responseIdsWorkspace3.map(_.samResource).toSet shouldBe Set(samResource4) + responseIdsAzure.map(_.samResource).toSet shouldBe Set(samResource1, samResource4) + responseIdsGcp.map(_.samResource).toSet shouldBe Set(samResource2, samResource3, samResource5) + responseIdsAzureWorkspace1.map(_.samResource).toSet shouldBe Set(samResource1) + responseIdsAzureWorkspace2.map(_.samResource).toSet shouldBe Set.empty + responseIdsGcpWorkspace1.map(_.samResource).toSet shouldBe Set.empty + responseIdsGcpWorkspace2.map(_.samResource).toSet shouldBe Set(samResource2, samResource3) + } + + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "list runtimes with parameters" in isolatedDbTest { + val runtimeId1 = RuntimeSamResourceId(UUID.randomUUID.toString) + val runtimeId2 = RuntimeSamResourceId(UUID.randomUUID.toString) + val workspaceId1 = WorkspaceId(UUID.randomUUID) + + val samService = mock[SamService[IO]] + when(samService.listResources(any(), isEq(RuntimeSamResource.resourceType))(any())) + .thenReturn(IO.pure(List(runtimeId1.resourceId, runtimeId2.resourceId))) + val testService = makeInterp(samService = samService) + val res = for { + samResource1 <- IO(runtimeId1) + samResource2 <- IO(runtimeId2) + runtime1 <- IO( + makeCluster(1) + .copy(samResource = samResource1, workspaceId = Some(workspaceId1)) + .save() + ) + _ <- setRuntimeDeleted(workspaceId1, runtime1.runtimeName) + + runtime2 <- IO(makeCluster(2).copy(samResource = samResource2, workspaceId = Some(workspaceId1)).save()) + _ <- labelQuery.save(runtime2.id, LabelResourceType.Runtime, "foo", "bar").transaction + listResponse1 <- testService.listRuntimes( + userInfo, + None, + None, + Map("foo" -> "bar") + ) // hit + listResponse2 <- testService.listRuntimes( + userInfo, + None, + None, + Map("FOO" -> "BAR") + ) // hit, case insensitive + listResponse3 <- testService.listRuntimes( + userInfo, + None, + None, + Map("foo!@#$%^&*()_+=';:\"" -> "!@#$%^&*()_+=';:\"bar") + ) // miss, with weird characters + listResponse4 <- testService.listRuntimes( + userInfo, + None, + None, + Map("foo" -> "not-bar") + ) // miss value + listResponse5 <- testService.listRuntimes( + userInfo, + None, + None, + Map("not-foo" -> "bar") + ) // miss key + } yield { + listResponse1.map(_.samResource).toSet shouldBe Set(samResource2) + listResponse2.map(_.samResource).toSet shouldBe Set(samResource2) + listResponse3.map(_.samResource).toSet shouldBe Set.empty + listResponse4.map(_.samResource).toSet shouldBe Set.empty + listResponse5.map(_.samResource).toSet shouldBe Set.empty + } + + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "list runtimes filtered by creator" in isolatedDbTest { + val wsmId1 = WsmResourceSamResourceId(WsmControlledResourceId(UUID.randomUUID)) + val runtimeId2 = RuntimeSamResourceId(UUID.randomUUID.toString) + val runtimeId3 = RuntimeSamResourceId(UUID.randomUUID.toString) + val runtimeId4 = RuntimeSamResourceId(UUID.randomUUID.toString) + val workspaceId1 = WorkspaceId(UUID.randomUUID) + val userInfoCreator = mockUserInfo("karen@styx.hel") + val userInfoOther = mockUserInfo("mike@heavn.io") + val samService = mock[SamService[IO]] + when( + samService.listResources(isEq(userInfoCreator.accessToken.token), isEq(RuntimeSamResource.resourceType))(any()) + ) + .thenReturn(IO.pure(List(wsmId1.resourceId, runtimeId3.resourceId))) + + val testService = makeInterp(samService = samService) + val res = for { + // runtime 1: I created, in a workspace I can read => visible + samResource1 <- IO(RuntimeSamResourceId(wsmId1.resourceId.toString)) + runtime1 <- IO( + makeCluster(1, Some(userInfoCreator.userEmail)) + .copy(samResource = samResource1, workspaceId = Some(workspaceId1)) + .save() + ) + + // runtime 2: I created, but I don't have permission => hidden + samResource2 <- IO(runtimeId2) + runtime2 <- IO( + makeCluster(2, Some(userInfoCreator.userEmail)) + .copy(samResource = samResource2, workspaceId = Some(WorkspaceId(UUID.randomUUID))) + .save() + ) + + // runtime 3: someone else created, but I can read => hidden if role=creator, else visible + samResource3 <- IO(runtimeId3) + runtime3 <- IO( + makeCluster(3, Some(userInfoOther.userEmail)) + .copy(samResource = samResource3, workspaceId = Some(workspaceId1)) + .save() + ) + + listResponseCreator <- testService.listRuntimes(userInfoCreator, None, None, Map("role" -> "creator")) + listResponseAny <- testService.listRuntimes(userInfoCreator, None, None, Map.empty) + } yield { + listResponseCreator.map(_.samResource).toSet shouldBe Set(samResource1) + listResponseAny.map(_.samResource).toSet shouldBe Set(samResource1, samResource3) + } + + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + // See https://broadworkbench.atlassian.net/browse/PROD-440 + // AoU relies on the ability for project owners to list other users' runtimes. + it should "list runtimes belonging to other users" in isolatedDbTest { + val runtimeId1 = RuntimeSamResourceId(UUID.randomUUID.toString) + val runtimeId2 = RuntimeSamResourceId(UUID.randomUUID.toString) + val workspaceId1 = WorkspaceId(UUID.randomUUID) + val userInfo = mockUserInfo("karen@styx.hel") + val samService = mock[SamService[IO]] + when(samService.listResources(isEq(userInfo.accessToken.token), isEq(RuntimeSamResource.resourceType))(any())) + .thenReturn(IO.pure(List(runtimeId1.resourceId, runtimeId2.resourceId))) + + val testService = makeInterp(samService = samService) + + // Make runtimes belonging to different users than the calling user + val res = for { + samResource1 <- IO(runtimeId1) + samResource2 <- IO(runtimeId2) + runtime1 = LeoLenses.runtimeToCreator.replace(WorkbenchEmail("different_user1@example.com"))( + makeCluster(1).copy(samResource = samResource1, workspaceId = Some(workspaceId1)) + ) + runtime2 = LeoLenses.runtimeToCreator.replace(WorkbenchEmail("different_user2@example.com"))( + makeCluster(2).copy(samResource = samResource2, workspaceId = Some(workspaceId1)) + ) + _ <- IO(runtime1.save()) + _ <- IO(runtime2.save()) + listResponse <- testService.listRuntimes(userInfo, None, None, Map.empty) + } yield listResponse.map(_.samResource).toSet shouldBe Set(samResource1, samResource2) + + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "list runtimes, rejecting invalid label parameters" in isolatedDbTest { + runtimeV2Service + .listRuntimes(userInfo, None, None, Map("_labels" -> "foo=bar;bam=yes")) + .attempt + .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + .swap + .toOption + .get + .isInstanceOf[ParseLabelsException] shouldBe true + runtimeV2Service + .listRuntimes(userInfo, None, None, Map("_labels" -> "foo=bar,bam")) + .attempt + .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + .swap + .toOption + .get + .isInstanceOf[ParseLabelsException] shouldBe true + + runtimeV2Service + .listRuntimes(userInfo, None, None, Map("_labels" -> "bogus")) + .attempt + .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + .swap + .toOption + .get + .isInstanceOf[ParseLabelsException] shouldBe true + + runtimeV2Service + .listRuntimes(userInfo, None, None, Map("_labels" -> "a,b")) + .attempt + .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + .swap + .toOption + .get + .isInstanceOf[ParseLabelsException] shouldBe true + } +} diff --git a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala index 7de59c7629..e0e7ceb2cd 100644 --- a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala +++ b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/provider/LeoProvider.scala @@ -64,6 +64,7 @@ class LeoProvider extends AnyFlatSpec with BeforeAndAfterAll with PactVerifier { mockStatusService, gcpOnlyServicesRegistry, mockAppService, + MockRuntimeV2Interp, mockAdminService, mockUserInfoDirectives, mockContentSecurityPolicyConfig, From 8a201e32205bc2374ed5b9b21fcc6636f25d3626 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Fri, 11 Jul 2025 10:17:10 -0400 Subject: [PATCH 37/43] Scalafmt --- .../leonardo/azure/AzureDiskSpec.scala | 9 ++++++- .../workbench/leonardo/CommonTestData.scala | 26 ++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala index 4a4b6a63f2..508f1cdbea 100644 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala +++ b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala @@ -9,7 +9,14 @@ import org.broadinstitute.dsde.workbench.google2.streamUntilDoneOrTimeout import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestTags.ExcludeFromJenkins import org.broadinstitute.dsde.workbench.leonardo.SSH.SSHRuntimeInfo import org.broadinstitute.dsde.workbench.leonardo.TestUser.Hermione -import org.broadinstitute.dsde.workbench.leonardo.{AzureBilling, CloudProvider, LeonardoConfig, LeonardoTestUtils, RuntimeName, SSH} +import org.broadinstitute.dsde.workbench.leonardo.{ + AzureBilling, + CloudProvider, + LeonardoConfig, + LeonardoTestUtils, + RuntimeName, + SSH +} import org.broadinstitute.dsde.workbench.service.test.CleanUp import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.{DoNotDiscover, ParallelTestExecution, Retries} diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala index e49f161cdf..99e5be35b3 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/CommonTestData.scala @@ -10,16 +10,36 @@ import com.typesafe.config.ConfigFactory import net.ceedubs.ficus.Ficus._ import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.google2.mock.BaseFakeGoogleStorage -import org.broadinstitute.dsde.workbench.google2.{DataprocRole, DiskName, MachineTypeName, NetworkName, OperationName, RegionName, SubnetworkName, ZoneName} +import org.broadinstitute.dsde.workbench.google2.{ + DataprocRole, + DiskName, + MachineTypeName, + NetworkName, + OperationName, + RegionName, + SubnetworkName, + ZoneName +} import org.broadinstitute.dsde.workbench.leonardo import org.broadinstitute.dsde.workbench.leonardo.ContainerRegistry.DockerHub -import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{BootSource, CryptoDetector, Jupyter, Proxy, RStudio, Welder} +import org.broadinstitute.dsde.workbench.leonardo.RuntimeImageType.{ + BootSource, + CryptoDetector, + Jupyter, + Proxy, + RStudio, + Welder +} import org.broadinstitute.dsde.workbench.leonardo.SamResourceId._ import org.broadinstitute.dsde.workbench.leonardo.auth.AllowlistAuthProvider import org.broadinstitute.dsde.workbench.leonardo.config._ import org.broadinstitute.dsde.workbench.leonardo.dao.MockSamDAO import org.broadinstitute.dsde.workbench.leonardo.db.ClusterRecord -import org.broadinstitute.dsde.workbench.leonardo.http.{CreateRuntimeRequest, RuntimeConfigRequest, userScriptStartupOutputUriMetadataKey} +import org.broadinstitute.dsde.workbench.leonardo.http.{ + userScriptStartupOutputUriMetadataKey, + CreateRuntimeRequest, + RuntimeConfigRequest +} import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google._ import org.broadinstitute.dsde.workbench.oauth2.mock.FakeOpenIDConnectConfiguration From 7dd092db993773ae86aee112c7ef64527e7e11cc Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 15 Jul 2025 13:42:23 -0400 Subject: [PATCH 38/43] Remove Azure swatomation tests --- .../leonardo/LeonardoAzureSuite.scala | 170 ------------- .../workbench/leonardo/LeonardoConfig.scala | 7 - .../dsde/workbench/leonardo/SSH.scala | 50 +--- .../leonardo/azure/AzureAutopauseSpec.scala | 134 ----------- .../leonardo/azure/AzureDiskSpec.scala | 227 ------------------ .../leonardo/azure/AzureRuntimeSpec.scala | 193 --------------- 6 files changed, 8 insertions(+), 773 deletions(-) delete mode 100644 automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoAzureSuite.scala delete mode 100644 automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureAutopauseSpec.scala delete mode 100644 automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala delete mode 100644 automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureRuntimeSpec.scala diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoAzureSuite.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoAzureSuite.scala deleted file mode 100644 index 80f290023e..0000000000 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoAzureSuite.scala +++ /dev/null @@ -1,170 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo - -import cats.effect.IO -import org.broadinstitute.dsde.rawls.model.AzureManagedAppCoordinates -import org.broadinstitute.dsde.workbench.leonardo.azure.{AzureAutopauseSpec, AzureDiskSpec, AzureRuntimeSpec} -import org.broadinstitute.dsde.workbench.pipeline.Pipeline.BILLING_PROJECT -import org.broadinstitute.dsde.workbench.pipeline.TestUser.Hermione -import org.broadinstitute.dsde.workbench.service.Rawls -import org.scalatest._ -import org.scalatest.freespec.FixtureAnyFreeSpecLike - -import java.util.UUID - -final case class AzureBillingProjectName(value: String) extends AnyVal -trait AzureBilling extends FixtureAnyFreeSpecLike { - this: TestSuite => - import io.circe.{parser, Decoder} - import org.broadinstitute.dsde.workbench.auth.AuthToken - import org.broadinstitute.dsde.workbench.azure.{AzureCloudContext, ManagedResourceGroupName, SubscriptionId, TenantId} - override type FixtureParam = WorkspaceResponse - val azureProjectKey = "leonardo.azureProject" - - implicit val azureManagedAppCoordinates: AzureManagedAppCoordinates = AzureManagedAppCoordinates( - UUID.fromString("fad90753-2022-4456-9b0a-c7e5b934e408"), - UUID.fromString("f557c728-871d-408c-a28b-eb6b2141a087"), - "e2e-xmx74y", - Some(UUID.fromString("997743f4-1bee-43af-90be-29ae0a47fdfc")) - ) - - override def withFixture(test: OneArgTest): Outcome = { - def runTestAndCheckOutcome(workspace: WorkspaceResponse) = - super.withFixture(test.toNoArgTest(workspace)) - - implicit val accessToken = Hermione.authToken().unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - try - sys.props.get(azureProjectKey) match { - case None => throw new RuntimeException("leonardo.azureProject system property is not set") - case Some(projectName) => - withRawlsWorkspace(AzureBillingProjectName(projectName)) { workspace => - runTestAndCheckOutcome(workspace) - } - } - catch { - case e: org.broadinstitute.dsde.workbench.service.RestException - if e.message == "Project cannot be deleted because it contains workspaces." => - println( - s"Exception occurred in test, but it is classed as a non-fatal cleanup error (likely in `withTemporaryAzureBillingProject`): $e" - ) - Succeeded - case e: Throwable => throw e - } - } - - def withRawlsWorkspace[T]( - projectName: AzureBillingProjectName - )(testCode: WorkspaceResponse => T)(implicit authToken: AuthToken): T = { - // hardcode this if you want to use a static workspace - // val workspaceName = "ddf3f5fa-a80e-4b2f-ab6e-9bd07817fad1-azure-test-workspace" - - val workspaceName = generateWorkspaceName() - - println(s"withRawlsWorkspace: Calling create rawls workspace with name ${workspaceName}") - - Rawls.workspaces.create( - projectName.value, - workspaceName, - Set.empty, - Map("disableAutomaticAppCreation" -> "true") - ) - - val response = - workspaceResponse(projectName.value, workspaceName).unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - println(s"withRawlsWorkspace: Rawls workspace get called, response: ${response}") - - testCode(response) - } - - private def workspaceResponse(projectName: String, workspaceName: String)(implicit - authToken: AuthToken - ): IO[WorkspaceResponse] = for { - responseString <- IO.pure(Rawls.workspaces.getWorkspaceDetails(projectName, workspaceName)) - json <- IO.fromEither(parser.parse(responseString)) - parsedResponse <- IO.fromEither(json.as[WorkspaceResponse]) - } yield parsedResponse - - case class WorkspaceResponse(accessLevel: Option[String], - canShare: Option[Boolean], - canCompute: Option[Boolean], - catalog: Option[Boolean], - workspace: WorkspaceDetails, - owners: Option[Set[String]], - azureContext: Option[AzureCloudContext] - ) - - implicit val azureContextDecoder: Decoder[AzureCloudContext] = Decoder.instance { c => - for { - tenantId <- c.downField("tenantId").as[String] - subscriptionId <- c.downField("subscriptionId").as[String] - resourceGroupId <- c.downField("managedResourceGroupId").as[String] - } yield AzureCloudContext(TenantId(tenantId), - SubscriptionId(subscriptionId), - ManagedResourceGroupName(resourceGroupId) - ) - } - - implicit val workspaceDetailsDecoder: Decoder[WorkspaceDetails] = Decoder.forProduct10( - "namespace", - "name", - "workspaceId", - "bucketName", - "createdDate", - "lastModified", - "createdBy", - "isLocked", - "billingAccountErrorMessage", - "errorMessage" - )(WorkspaceDetails.apply) - - implicit val workspaceResponseDecoder: Decoder[WorkspaceResponse] = Decoder.forProduct7( - "accessLevel", - "canShare", - "canCompute", - "catalog", - "workspace", - "owners", - "azureContext" - )(WorkspaceResponse.apply) - - // Note this isn't the full model available, we only need a few fields - // The full model lives in rawls here https://github.com/broadinstitute/rawls/blob/develop/model/src/main/scala/org/broadinstitute/dsde/rawls/model/WorkspaceModel.scala#L712 - case class WorkspaceDetails( - namespace: String, - name: String, - workspaceId: String, - bucketName: String, - createdDate: String, - lastModified: String, - createdBy: String, - isLocked: Boolean = false, - billingAccountErrorMessage: Option[String] = None, - errorMessage: Option[String] = None - ) - - private def generateWorkspaceName(): String = - s"${UUID.randomUUID().toString()}-azure-test-workspace" -} - -final class LeonardoAzureSuite - extends Suites( - new AzureRuntimeSpec, - new AzureDiskSpec, - new AzureAutopauseSpec - ) - with TestSuite - with AzureBilling - with ParallelTestExecution - with BeforeAndAfterAll { - override def beforeAll(): Unit = { - val res = for { - _ <- IO(println("in beforeAll for AzureBillingBeforeAndAfter")) - _ <- IO(super.beforeAll()) - _ <- IO(sys.props.put(azureProjectKey, BILLING_PROJECT)) - // hardcode this if you want to use a static billing project - // _ <- IO(sys.props.put(azureProjectKey, "tmp-billing-project-beddf71a74")) - } yield () - res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - } -} diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoConfig.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoConfig.scala index 304eb82e1d..32c256c350 100644 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoConfig.scala +++ b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/LeonardoConfig.scala @@ -36,13 +36,6 @@ object LeonardoConfig extends CommonConfig { val leonardoServiceAccountUsername = gcs.getString("leonardoServiceAccountUsername") } - object Azure { - val vmUser = azure.getString("leoVmUser") - val vmPassword = azure.getString("leoVmPassword") - val bastionName = azure.getString("bastionName") - val defaultBastionPort = azure.getInt("defaultBastionPort") - } - // TODO: this should be updated once we're able to run azure automation tests as part of CI object WSM { val wsmUri: String = "https://workspace.dsde-dev.broadinstitute.org" diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/SSH.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/SSH.scala index 60e10f2361..c9abd90f09 100644 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/SSH.scala +++ b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/SSH.scala @@ -3,14 +3,13 @@ package org.broadinstitute.dsde.workbench.leonardo import cats.effect.{IO, Resource} import com.google.cloud.oslogin.common.OsLoginProto.SshPublicKey import com.google.cloud.oslogin.v1.{ImportSshPublicKeyRequest, OsLoginServiceClient} -import org.broadinstitute.dsde.rawls.model.AzureManagedAppCoordinates -import org.typelevel.log4cats.StructuredLogger -import org.typelevel.log4cats.slf4j.Slf4jLogger import net.schmizz.sshj.SSHClient import net.schmizz.sshj.connection.channel.direct.Session import net.schmizz.sshj.transport.verification.PromiscuousVerifier import org.broadinstitute.dsde.workbench.model.WorkbenchEmail import org.broadinstitute.dsde.workbench.model.google.GoogleProject +import org.typelevel.log4cats.StructuredLogger +import org.typelevel.log4cats.slf4j.Slf4jLogger import java.nio.file.{Files, Path, Paths} import java.util.UUID @@ -27,33 +26,6 @@ case class Tunnel(pid: String, port: Int) { object SSH { val loggerIO: StructuredLogger[IO] = Slf4jLogger.getLogger[IO] - // TODO: If multiple tests need to ssh into an azure VM: add a lock of sorts, only one tunnel at a time with same port - // A bastion tunnel is needed to tunnel to an azure vm - // See: https://learn.microsoft.com/en-us/azure/bastion/native-client - def startAzureBastionTunnel(runtimeName: RuntimeName, port: Int = LeonardoConfig.Azure.defaultBastionPort)(implicit - staticTestCoordinates: AzureManagedAppCoordinates - ): Resource[IO, Tunnel] = { - val targetResourceId = - s"/subscriptions/${staticTestCoordinates.subscriptionId.toString}/resourceGroups/${staticTestCoordinates.managedResourceGroupId}/providers/Microsoft.Compute/virtualMachines/${runtimeName.asString}" - - val makeTunnel = for { - scriptPath <- IO(getClass.getClassLoader.getResource("startTunnel.sh").getPath) - process = Process( - scriptPath, - None, - "BASTION_NAME" -> LeonardoConfig.Azure.bastionName, - "RESOURCE_GROUP" -> staticTestCoordinates.managedResourceGroupId, - "RESOURCE_ID" -> targetResourceId, - "PORT" -> port.toString - ) - output <- IO(process !!) - _ <- loggerIO.info(s"Bastion tunnel start command full output:\n\t${output}") - tunnel = Tunnel(output.split('\n').last, port) - } yield tunnel - - Resource.make(makeTunnel)(tunnel => loggerIO.info("Closing tunnel") >> closeTunnel(tunnel)) - } - final case class SSHSession(session: Session, client: SSHClient) // Note that a session is a one time use resource, and only supports one command execution // This method starts an ssh session to either an azure or google runtime. @@ -72,18 +44,12 @@ object SSH { _ <- IO(client.connect(hostName, port)) _ <- loggerIO.info("Authenticating ssh client ") - _ <- - if (sshConfig.cloudProvider == CloudProvider.Azure) - IO(client.authPassword(LeonardoConfig.Azure.vmUser, LeonardoConfig.Azure.vmPassword)) - else { - for { - keyConfig <- createSSHKeys(WorkbenchEmail(LeonardoConfig.Leonardo.serviceAccountEmail), - sshConfig.googleProject.get - ) - _ <- IO(client.authPublickey(keyConfig.username, keyConfig.privateKey.toAbsolutePath.toString)) - } yield () - } - + _ <- for { + keyConfig <- createSSHKeys(WorkbenchEmail(LeonardoConfig.Leonardo.serviceAccountEmail), + sshConfig.googleProject.get + ) + _ <- IO(client.authPublickey(keyConfig.username, keyConfig.privateKey.toAbsolutePath.toString)) + } yield () _ <- loggerIO.info("Starting ssh session") session <- IO(client.startSession()) } yield SSHSession(session, client) diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureAutopauseSpec.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureAutopauseSpec.scala deleted file mode 100644 index 4b10278fbe..0000000000 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureAutopauseSpec.scala +++ /dev/null @@ -1,134 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.azure - -import org.scalatest.prop.TableDrivenPropertyChecks -import org.broadinstitute.dsde.workbench.google2.streamUntilDoneOrTimeout -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import org.broadinstitute.dsde.workbench.GeneratedLeonardoClient -import org.broadinstitute.dsde.workbench.auth.AuthToken -import org.broadinstitute.dsde.workbench.client.leonardo.model.{ - AzureDiskConfig, - ClusterStatus, - CreateAzureRuntimeRequest -} -import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestTags.ExcludeFromJenkins -import org.broadinstitute.dsde.workbench.pipeline.TestUser.Hermione -import org.scalatest.{DoNotDiscover, ParallelTestExecution, Retries} -import org.broadinstitute.dsde.workbench.service.test.CleanUp -import org.broadinstitute.dsde.workbench.leonardo.{AzureBilling, LeonardoTestUtils} - -import scala.concurrent.duration._ - -@DoNotDiscover -class AzureAutopauseSpec - extends AzureBilling - with LeonardoTestUtils - with ParallelTestExecution - with TableDrivenPropertyChecks - with Retries - with CleanUp { - - "azure runtime autopauses" taggedAs ExcludeFromJenkins in { workspaceDetails => - implicit val accessToken: IO[AuthToken] = Hermione.authToken() - - val workspaceId = workspaceDetails.workspace.workspaceId - - val labelMap: java.util.HashMap[String, String] = new java.util.HashMap[String, String]() - labelMap.put("automation", "true") - - val runtimeName = randomClusterName - val res = - for { - _ <- loggerIO.info(s"AzureAutoPauseSpec: About to create runtime") - runtimeClient <- GeneratedLeonardoClient.generateRuntimesApi - - // autopause set to 15 minutes to make sure metrics monitor call is ignored (doesn't reset the clock) - createReq = new CreateAzureRuntimeRequest() - .labels(labelMap) - .machineSize("Standard_DS1_v2") - .disk( - new AzureDiskConfig() - .name(generateAzureDiskName()) - .size(50) - .labels(labelMap) - ) - .autopauseThreshold(15) - - _ <- IO(runtimeClient.createAzureRuntime(workspaceId, runtimeName.asString, false, createReq)) - _ <- loggerIO.info(s"AzureAutoPauseSpec: Create runtime request submitted. Starting to poll GET") - - // ------------------ Waiting for Create ------------------ // - - // Verify the initial getRuntime call - callGetRuntime = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName.asString)) - - initialGetRuntimeResponse <- callGetRuntime - _ <- loggerIO.info(s"initial get runtime response $initialGetRuntimeResponse") - _ = initialGetRuntimeResponse.getStatus shouldBe ClusterStatus.CREATING - - _ <- loggerIO.info( - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} in creating status detected" - ) - - // Verify the runtime eventually becomes Running (in 25 minutes) - // will reduce to 20 once https://broadworkbench.atlassian.net/browse/WOR-1397 is merged - monitorCreateResult <- streamUntilDoneOrTimeout( - callGetRuntime, - 150, - 10 seconds, - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} did not finish creating after 25 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.RUNNING)) - - _ <- loggerIO.info( - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} create monitor result: $monitorCreateResult" - ) - _ = monitorCreateResult.getStatus() shouldBe ClusterStatus.RUNNING - - // ------------------ Waiting for Autopause ------------------ // - - // new IO for new stream - callGetRuntimeStopping = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName.asString)) - - _ <- loggerIO.info( - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} waiting to autopause" - ) - - _ <- IO.sleep(10 minutes) // sleep for 10 minutes before checking if the runtime is pausing - - // poll for another 10 minutes - monitorAutoPauseResult <- streamUntilDoneOrTimeout( - callGetRuntimeStopping, - 60, - 10 seconds, - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} did not transition to stopping after 20 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.STOPPING)) - - _ <- loggerIO.info( - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} auto-pause monitor result: $monitorAutoPauseResult" - ) - _ = monitorAutoPauseResult.getStatus() shouldBe ClusterStatus.STOPPING - - _ <- loggerIO.info( - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} waiting for runtime to stop" - ) - - // new IO for new stream - callGetRuntimeStopped = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName.asString)) - - monitorStoppingResult <- streamUntilDoneOrTimeout( - callGetRuntimeStopped, - 60, - 10 seconds, - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} did not transition to stopped after 10 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.STOPPED)) - - _ <- loggerIO.info( - s"AzureAutoPauseSpec: runtime $workspaceId/${runtimeName.asString} stopped monitor result: $monitorStoppingResult" - ) - _ = monitorStoppingResult.getStatus() shouldBe ClusterStatus.STOPPED - - _ <- IO.sleep(1 minute) // sleep for a minute before cleaning up workspace - } yield () - res.unsafeRunSync() - } -} diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala deleted file mode 100644 index 508f1cdbea..0000000000 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureDiskSpec.scala +++ /dev/null @@ -1,227 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.azure - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import org.broadinstitute.dsde.workbench.GeneratedLeonardoClient -import org.broadinstitute.dsde.workbench.auth.AuthToken -import org.broadinstitute.dsde.workbench.client.leonardo.model._ -import org.broadinstitute.dsde.workbench.google2.streamUntilDoneOrTimeout -import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestTags.ExcludeFromJenkins -import org.broadinstitute.dsde.workbench.leonardo.SSH.SSHRuntimeInfo -import org.broadinstitute.dsde.workbench.leonardo.TestUser.Hermione -import org.broadinstitute.dsde.workbench.leonardo.{ - AzureBilling, - CloudProvider, - LeonardoConfig, - LeonardoTestUtils, - RuntimeName, - SSH -} -import org.broadinstitute.dsde.workbench.service.test.CleanUp -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.{DoNotDiscover, ParallelTestExecution, Retries} - -import scala.concurrent.duration._ - -@DoNotDiscover -class AzureDiskSpec - extends AzureBilling - with LeonardoTestUtils - with ParallelTestExecution - with TableDrivenPropertyChecks - with Retries - with CleanUp { - - "create a disk, keep it on runtime delete, and then attach it to a new runtime" taggedAs ExcludeFromJenkins in { - workspaceDetails => - implicit val accessToken: IO[AuthToken] = Hermione.authToken() - - val workspaceId = workspaceDetails.workspace.workspaceId - - val labelMap: java.util.HashMap[String, String] = new java.util.HashMap[String, String]() - labelMap.put("automation", "true") - - val runtimeName = randomClusterName - val runtimeName2 = randomClusterName - val diskName = generateAzureDiskName() - val res = - for { - _ <- loggerIO.info(s"AzureDiskSpec: About to create runtime") - runtimeClient <- GeneratedLeonardoClient.generateRuntimesApi - diskClient <- GeneratedLeonardoClient.generateDisksApi - - createReq = new CreateAzureRuntimeRequest() - .labels(labelMap) - .machineSize("Standard_DS1_v2") - .disk( - new AzureDiskConfig() - .name(diskName) - .size(50) - .labels(labelMap) - ) - - _ <- IO(runtimeClient.createAzureRuntime(workspaceId, runtimeName.asString, false, createReq)) - _ <- loggerIO.info(s"AzureDiskSpec: Create runtime request submitted. Starting to poll GET") - - // Verify the initial getRuntime call - callGetRuntime = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName.asString)) - - intitialGetRuntimeResponse <- callGetRuntime - _ <- loggerIO.info(s"initial get runtime response ${intitialGetRuntimeResponse}") - _ = intitialGetRuntimeResponse.getStatus shouldBe ClusterStatus.CREATING - - _ <- loggerIO.info( - s"AzureDiskSpec: runtime ${workspaceId}/${runtimeName.asString} in creating status detected" - ) - - _ <- loggerIO.info("AzureDiskSpec: verifying get disk response") - diskId = intitialGetRuntimeResponse.getRuntimeConfig.getAzureConfig.getPersistentDiskId - getDisk = IO(diskClient.getDiskV2(diskId.toBigInteger.intValue())) - diskDuringRuntimeCreate <- getDisk - _ = diskDuringRuntimeCreate.getStatus shouldBe DiskStatus.CREATING - - _ <- loggerIO.info( - s"AzureDiskSpec: disk ${workspaceId}/${diskDuringRuntimeCreate.getId()} in creating status detected" - ) - - // Verify the runtime eventually becomes Running (in 25 minutes) - // will reduce to 20 once https://broadworkbench.atlassian.net/browse/WOR-1397 is merged - monitorCreateResult <- streamUntilDoneOrTimeout( - callGetRuntime, - 150, - 10 seconds, - s"AzureDiskSpec: runtime ${workspaceId}/${runtimeName.asString} did not finish creating after 25 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.RUNNING)) - - _ <- loggerIO.info( - s"AzureDiskSpec: runtime ${workspaceId}/${runtimeName.asString} create monitor result: $monitorCreateResult" - ) - _ = monitorCreateResult.getStatus() shouldBe ClusterStatus.RUNNING - - _ <- loggerIO.info("SSHing into first vm to add a file to the disk") - (output1, output2) <- SSH.startAzureBastionTunnel(RuntimeName(monitorCreateResult.getRuntimeName())).use { - t => - for { - _ <- loggerIO.info("executing first command to create file for first runtime") - output1 <- SSH.startSessionAndExecuteCommand( - t.hostName, - t.port, - s"echo ${LeonardoConfig.Azure.vmPassword} | sudo -S bash -c \"echo '{}' > /home/jupyter/persistent_disk/test_disk.ipynb\"", - SSHRuntimeInfo(None, CloudProvider.Azure) - ) - _ <- loggerIO.info("executing second command to get file contents for first runtime") - output2 <- SSH.startSessionAndExecuteCommand(t.hostName, - t.port, - s"cat /home/jupyter/persistent_disk/test_disk.ipynb", - SSHRuntimeInfo(None, CloudProvider.Azure) - ) - } yield (output1, output2) - } - - _ <- loggerIO.info(s"command result 1 and 2: \n\t1: ${output1}, \n\t2: ${output2}") - _ = output2.outputLines.mkString shouldBe "{}" - - _ <- loggerIO.info( - s"AzureDiskSpec: runtime ${workspaceId}/${runtimeName.asString} delete starting" - ) - - // Delete the runtime but not the disk - _ <- IO(runtimeClient.deleteAzureRuntime(workspaceId, runtimeName.asString, false)) - - _ <- loggerIO.info( - s"AzureDiskSpecAzureDiskSpec: runtime ${workspaceId}/${runtimeName.asString} delete called" - ) - - // Wait until disk is unattached - monitorGetRuntimeUntilUnattached <- streamUntilDoneOrTimeout( - callGetRuntime, - 240, - 10 seconds, - s"AzureDiskSpec: disk ${workspaceId}/${diskName} was not ready after 40 minutes" - )(implicitly, (op: GetRuntimeResponse) => op.getRuntimeConfig.getAzureConfig.getPersistentDiskId === null) - - _ <- loggerIO.info( - s"AzureDiskSpec: runtime ${workspaceId}/${runtimeName} between runtime monitor result: $monitorGetRuntimeUntilUnattached" - ) - monitorGetDisk <- getDisk - _ = monitorGetDisk.getStatus shouldBe DiskStatus.READY - _ = monitorGetDisk.getName shouldBe diskName - - _ <- loggerIO.info( - s"AzureRuntimeSpec: disk ${workspaceId}/${monitorGetDisk.getId()} in ready status detected" - ) - - _ <- loggerIO.info(s"AzureDiskSpec: About to create runtime and re-attached disk") - - createReq2 = new CreateAzureRuntimeRequest() - .labels(labelMap) - .machineSize("Standard_DS1_v2") - .disk( - new AzureDiskConfig() - .name(diskName) - .size(50) - .labels(labelMap) - ) - - _ <- loggerIO.info(s"printing createRuntimeReq: ${createReq2.toJson}") - - _ <- IO(runtimeClient.createAzureRuntime(workspaceId, runtimeName2.asString, true, createReq2)) - _ <- loggerIO.info(s"AzureDiskSpec: Create runtime2 request submitted. Starting to poll GET") - - // Verify the initial getRuntime call - _ <- IO.sleep(5 seconds) - callGetRuntime2 = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName2.asString)) - - intitialGetRuntimeResponse2 <- callGetRuntime2 - _ <- loggerIO.info(s"initial get runtime response for runtime2 ${intitialGetRuntimeResponse2}") - _ = intitialGetRuntimeResponse2.getStatus shouldBe ClusterStatus.CREATING - - _ <- loggerIO.info( - s"AzureDiskSpec: runtime2 ${workspaceId}/${runtimeName2.asString} in creating status detected" - ) - - _ <- loggerIO.info("AzureDiskSpec: verifying get disk2 response") - diskId2 = intitialGetRuntimeResponse2.getRuntimeConfig.getAzureConfig.getPersistentDiskId - getDisk2 = IO(diskClient.getDiskV2(diskId2.toBigInteger.intValue())) - diskDuringRuntimeCreate2 <- getDisk2 - _ = diskDuringRuntimeCreate2.getStatus shouldBe DiskStatus.READY - _ = diskDuringRuntimeCreate2.getName shouldBe diskName - - _ <- loggerIO.info( - s"AzureDiskSpec: disk2 ${workspaceId}/${diskDuringRuntimeCreate2.getId()} in creating status detected" - ) - - // Verify runtime 2 eventually becomes Running (in 25 minutes) - // will reduce to 20 once https://broadworkbench.atlassian.net/browse/WOR-1397 is merged - monitorCreateResult2 <- streamUntilDoneOrTimeout( - callGetRuntime2, - 150, - 10 seconds, - s"AzureDiskSpec: runtime2 ${workspaceId}/${runtimeName.asString} did not finish creating after 25 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.RUNNING)) - - _ <- loggerIO.info( - s"AzureDiskSpec: runtime2 ${workspaceId}/${runtimeName.asString} create monitor result: $monitorCreateResult2" - ) - _ = monitorCreateResult2.getStatus() shouldBe ClusterStatus.RUNNING - - disk2 <- getDisk2 - _ = disk2.getStatus() shouldBe DiskStatus.READY - _ = disk2.getId() shouldBe monitorGetDisk.getId() - - _ <- loggerIO.info("SSHing into second vm to verify disk contents") - output <- SSH.startAzureBastionTunnel(RuntimeName(monitorCreateResult2.getRuntimeName())).use { t => - for { - output <- SSH.startSessionAndExecuteCommand(t.hostName, - t.port, - s"cat /home/jupyter/persistent_disk/test_disk.ipynb", - SSHRuntimeInfo(None, CloudProvider.Azure) - ) - } yield output - } - - _ = output.outputLines.mkString shouldBe "{}" - } yield () - res.unsafeRunSync() - } -} diff --git a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureRuntimeSpec.scala b/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureRuntimeSpec.scala deleted file mode 100644 index 30fbbc1e50..0000000000 --- a/automation/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/azure/AzureRuntimeSpec.scala +++ /dev/null @@ -1,193 +0,0 @@ -package org.broadinstitute.dsde.workbench.leonardo.azure - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import org.broadinstitute.dsde.workbench.GeneratedLeonardoClient -import org.broadinstitute.dsde.workbench.auth.AuthToken -import org.broadinstitute.dsde.workbench.client.leonardo.model.{ - AzureDiskConfig, - ClusterStatus, - CreateAzureRuntimeRequest, - DiskStatus -} -import org.broadinstitute.dsde.workbench.google2.streamUntilDoneOrTimeout -import org.broadinstitute.dsde.workbench.leonardo.LeonardoTestTags.ExcludeFromJenkins -import org.broadinstitute.dsde.workbench.leonardo.{AzureBilling, LeonardoTestUtils} -import org.broadinstitute.dsde.workbench.pipeline.TestUser.Hermione -import org.broadinstitute.dsde.workbench.service.test.CleanUp -import org.http4s.headers.Authorization -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.{DoNotDiscover, ParallelTestExecution, Retries} - -import scala.concurrent.duration._ - -@DoNotDiscover -class AzureRuntimeSpec - extends AzureBilling - with LeonardoTestUtils - with ParallelTestExecution - with TableDrivenPropertyChecks - with Retries - with CleanUp { - - "create, get, delete azure runtime" taggedAs ExcludeFromJenkins in { workspaceDetails => - implicit val accessToken: IO[AuthToken] = Hermione.authToken() - implicit val authorization: IO[Authorization] = Hermione.authorization() - val workspaceId = workspaceDetails.workspace.workspaceId - - val labelMap: java.util.HashMap[String, String] = new java.util.HashMap[String, String]() - labelMap.put("automation", "true") - - val runtimeName = randomClusterName - val res = - for { - _ <- loggerIO.info(s"AzureRuntimeSpec: About to create runtime") - - runtimeClient <- GeneratedLeonardoClient.generateRuntimesApi - diskClient <- GeneratedLeonardoClient.generateDisksApi - - createReq = new CreateAzureRuntimeRequest() - .labels(labelMap) - .machineSize("Standard_DS1_v2") - .disk( - new AzureDiskConfig() - .name(generateAzureDiskName()) - .size(50) - .labels(labelMap) - ) - - _ <- IO(runtimeClient.createAzureRuntime(workspaceId, runtimeName.asString, false, createReq)) - _ <- loggerIO.info(s"AzureRuntimeSpec: Create runtime request submitted. Starting to poll GET") - - // Verify the initial getRuntime call - _ <- IO.sleep(5 seconds) - callGetRuntime = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName.asString)) - - intitialGetRuntimeResponse <- callGetRuntime - _ <- loggerIO.info(s"initial get runtime response ${intitialGetRuntimeResponse}") - _ = intitialGetRuntimeResponse.getStatus shouldBe ClusterStatus.CREATING - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} in creating status detected" - ) - - _ <- loggerIO.info("AzureRuntimeSpec: verifying get disk response") - diskId = intitialGetRuntimeResponse.getRuntimeConfig.getAzureConfig.getPersistentDiskId - getDisk = IO(diskClient.getDiskV2(diskId.toBigInteger.intValue())) - diskDuringRuntimeCreate <- getDisk - _ = diskDuringRuntimeCreate.getStatus shouldBe DiskStatus.CREATING - - _ <- loggerIO.info( - s"AzureRuntimeSpec: disk ${workspaceId}/${diskDuringRuntimeCreate.getId()} in creating status detected" - ) - - // Verify the runtime eventually becomes Running (in 25 minutes) - // will reduce to 20 once https://broadworkbench.atlassian.net/browse/WOR-1397 is merged - monitorCreateResult <- streamUntilDoneOrTimeout( - callGetRuntime, - 150, - 10 seconds, - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} did not finish creating after 25 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.RUNNING)) - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} create monitor result: $monitorCreateResult" - ) - _ = monitorCreateResult.getStatus() shouldBe ClusterStatus.RUNNING - - // Now stop the runtime - _ <- IO(runtimeClient.stopRuntimeV2(workspaceId, runtimeName.asString)) - - callGetRuntime = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName.asString)) - stoppingRuntimeResponse <- callGetRuntime - _ <- loggerIO.info(s"stopping get runtime response ${stoppingRuntimeResponse}") - _ = stoppingRuntimeResponse.getStatus shouldBe ClusterStatus.STOPPING - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} in stopping status detected" - ) - - // Verify the runtime eventually becomes Stopped - monitorStopResult <- streamUntilDoneOrTimeout( - callGetRuntime, - 60, - 10 seconds, - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} did not stop after 10 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.STOPPED)) - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} stopped monitor result: $monitorStopResult" - ) - _ = monitorStopResult.getStatus shouldBe ClusterStatus.STOPPED - - // now that the runtime is stopped, start it - _ <- IO(runtimeClient.startRuntimeV2(workspaceId, runtimeName.asString)) - - startingRuntimeResponse <- callGetRuntime - _ <- loggerIO.info(s"starting get runtime response ${startingRuntimeResponse}") - _ = startingRuntimeResponse.getStatus shouldBe ClusterStatus.STARTING - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} in starting status detected" - ) - - // Verify the runtime eventually becomes Started - monitorStartResult <- streamUntilDoneOrTimeout( - callGetRuntime, - 60, - 10 seconds, - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} did not start after 10 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.RUNNING)) - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} start monitor result: $monitorStartResult" - ) - _ = monitorStartResult.getStatus shouldBe ClusterStatus.RUNNING - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} delete starting" - ) - // Delete the runtime - _ <- IO(runtimeClient.deleteAzureRuntime(workspaceId, runtimeName.asString, true)) - - _ <- loggerIO.info( - s"AzureRuntimeSpec: about to test proxyUrl" - ) - - _ <- GeneratedLeonardoClient.client.use { c => - implicit val client = c - GeneratedLeonardoClient.testProxyUrl(monitorCreateResult) - } - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} delete called, starting to poll on deletion" - ) - - callGetRuntime2 = IO(runtimeClient.getAzureRuntime(workspaceId, runtimeName.asString)) - monitorDeleteResult <- streamUntilDoneOrTimeout( - callGetRuntime2, - 240, - 10 seconds, - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} did not finish deleting after 40 minutes" - )(implicitly, GeneratedLeonardoClient.runtimeInStateOrError(ClusterStatus.DELETED)) - - _ <- loggerIO.info( - s"AzureRuntimeSpec: runtime ${workspaceId}/${runtimeName.asString} delete monitor result: $monitorDeleteResult" - ) - _ = monitorDeleteResult.getStatus() shouldBe ClusterStatus.DELETED - - diskAfterRuntimeDelete <- getDisk - _ <- loggerIO.info( - s"AzureRuntimeSpec: disk $workspaceId/${diskAfterRuntimeDelete.getName} delete monitor result: $diskAfterRuntimeDelete" - ) - _ = diskAfterRuntimeDelete.getStatus should (be(DiskStatus.DELETED) or be(DiskStatus.DELETING)) - - _ <- loggerIO.info( - s"AzureRuntimeSpec: disk ${workspaceId}/${diskAfterRuntimeDelete.getId()} in deleted status detected" - ) - - _ <- IO.sleep(1 minute) // sleep for a minute before cleaning up workspace - } yield () - res.unsafeRunSync() - } -} From 7859d287a2fc35b6b5a60347e8b4c3819da78135 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Wed, 16 Jul 2025 10:29:15 -0400 Subject: [PATCH 39/43] Retry removing WSM, BPM clients --- project/Dependencies.scala | 7 ------- 1 file changed, 7 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 5eb7fdb347..0f3a8e3b80 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -159,14 +159,9 @@ object Dependencies { def excludeLiquibase = ExclusionRule("org.liquibase", "liquibase-core") def excludeFlagsmith = ExclusionRule("com.flagsmith", "flagsmith-java-client") - val workSpaceManagerV = "0.254.1127-SNAPSHOT" - val bpmV = "0.1.548-SNAPSHOT" - // [IA-4939] commons-text:1.9 is unsafe def excludeCommonsText = ExclusionRule("org.apache.commons", "commons-text") def tclExclusions(m: ModuleID): ModuleID = m.excludeAll(excludeSpringBoot, excludeSpringAop, excludeSpringData, excludeSpringFramework, excludeOpenCensus, excludeGoogleFindBugs, excludeBroadWorkbench, excludePostgresql, excludeSnakeyaml, excludeSlf4j, excludeCommonsText, excludeLiquibase, excludeOpenTelemetry, excludeFlagsmith) - val workspaceManager = excludeJakarta("bio.terra" % "workspace-manager-client" % workSpaceManagerV) - val bpm = excludeJakarta("bio.terra" % "billing-profile-manager-client" % bpmV) val terraCommonLib = tclExclusions(excludeJakarta("bio.terra" % "terra-common-lib" % terraCommonLibV classifier "plain")) val sam = excludeJakarta("org.broadinstitute.dsde.workbench" %% "sam-client" % samV) @@ -198,8 +193,6 @@ object Dependencies { workbenchAzure, workbenchAzureTest, logbackClassic, - workspaceManager, - bpm, terraCommonLib, sam ) From 098203666d2f5b04ed3912b7f6c9f2e9f0e9953f Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Wed, 16 Jul 2025 14:12:56 -0400 Subject: [PATCH 40/43] Don't exclude opentelemetry from TCL --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 0f3a8e3b80..3a6c8c6ecf 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -161,7 +161,7 @@ object Dependencies { // [IA-4939] commons-text:1.9 is unsafe def excludeCommonsText = ExclusionRule("org.apache.commons", "commons-text") - def tclExclusions(m: ModuleID): ModuleID = m.excludeAll(excludeSpringBoot, excludeSpringAop, excludeSpringData, excludeSpringFramework, excludeOpenCensus, excludeGoogleFindBugs, excludeBroadWorkbench, excludePostgresql, excludeSnakeyaml, excludeSlf4j, excludeCommonsText, excludeLiquibase, excludeOpenTelemetry, excludeFlagsmith) + def tclExclusions(m: ModuleID): ModuleID = m.excludeAll(excludeSpringBoot, excludeSpringAop, excludeSpringData, excludeSpringFramework, excludeOpenCensus, excludeGoogleFindBugs, excludeBroadWorkbench, excludePostgresql, excludeSnakeyaml, excludeSlf4j, excludeCommonsText, excludeLiquibase, excludeFlagsmith) val terraCommonLib = tclExclusions(excludeJakarta("bio.terra" % "terra-common-lib" % terraCommonLibV classifier "plain")) val sam = excludeJakarta("org.broadinstitute.dsde.workbench" %% "sam-client" % samV) From 7707c9ad343a398fb10a51cdbe049d60ba611ab0 Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Wed, 16 Jul 2025 17:27:45 -0400 Subject: [PATCH 41/43] Restore v2 start and stop endpoints --- http/src/main/resources/swagger/api-docs.yaml | 76 +++++++ .../http/AppDependenciesBuilder.scala | 3 +- .../leonardo/http/api/RuntimeV2Routes.scala | 53 ++++- .../http/service/RuntimeV2Service.scala | 8 + .../http/service/RuntimeV2ServiceInterp.scala | 54 ++++- .../http/service/MockRuntimeV2Interp.scala | 8 + .../service/RuntimeV2ServiceInterpSpec.scala | 204 +++++++++++++++++- 7 files changed, 400 insertions(+), 6 deletions(-) diff --git a/http/src/main/resources/swagger/api-docs.yaml b/http/src/main/resources/swagger/api-docs.yaml index c03416fb44..376bd3089d 100644 --- a/http/src/main/resources/swagger/api-docs.yaml +++ b/http/src/main/resources/swagger/api-docs.yaml @@ -553,6 +553,82 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorReport" + "/api/v2/runtimes/{workspaceId}/{name}/start": + post: + summary: Start runtime (WIP). + description: > + Start an existing Leonardo managed runtime + operationId: startRuntimeV2 + tags: + - runtimes + parameters: + - in: path + name: workspaceId + description: workspaceId + required: true + schema: + type: string + - in: path + name: name + description: runtimeName + required: true + schema: + type: string + responses: + "202": + description: Runtime start request accepted + "403": + description: User does not have permission to perform action on runtime + "404": + description: Runtime not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "500": + description: Internal Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "/api/v2/runtimes/{workspaceId}/{name}/stop": + post: + summary: Stop runtime (WIP). + description: > + Stop an existing Leonardo managed runtime + operationId: stopRuntimeV2 + tags: + - runtimes + parameters: + - in: path + name: workspaceId + description: workspaceId + required: true + schema: + type: string + - in: path + name: name + description: runtimeName + required: true + schema: + type: string + responses: + "202": + description: Runtime stop request accepted + "403": + description: User does not have permission to perform action on runtime + "404": + description: Runtime not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" + "500": + description: Internal Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorReport" ## Persistent Disk paths ## diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala index e6817ed1e0..72a32d24c5 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/AppDependenciesBuilder.scala @@ -88,7 +88,8 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu ): Resource[IO, ServicesDependencies] = { val statusService = new StatusService(baselineDependencies.samDAO, dbReference) - val runtimeV2Service = new RuntimeV2ServiceInterp[IO](baselineDependencies.samService) + val runtimeV2Service = + new RuntimeV2ServiceInterp[IO](baselineDependencies.publisherQueue, baselineDependencies.samService) val adminService = new AdminServiceInterp[IO](baselineDependencies.authProvider, baselineDependencies.publisherQueue) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala index 9c67b169de..7739b6d387 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala @@ -38,7 +38,33 @@ class RuntimeV2Routes(runtimeV2Service: RuntimeV2Service[IO], userInfoDirectives } } } - } + } ~ + pathPrefix(workspaceIdSegment) { workspaceId => + pathPrefix(runtimeNameSegmentWithValidation) { runtimeName => + path("stop") { + post { + complete( + stopRuntimeHandler( + userInfo, + workspaceId, + runtimeName + ) + ) + } + } ~ + path("start") { + post { + complete( + startRuntimeHandler( + userInfo, + workspaceId, + runtimeName + ) + ) + } + } + } + } } } } @@ -61,4 +87,29 @@ class RuntimeV2Routes(runtimeV2Service: RuntimeV2Service[IO], userInfoDirectives ) } yield StatusCodes.OK -> resp: ToResponseMarshallable + private[api] def startRuntimeHandler(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)(implicit + ev: Ask[IO, AppContext] + ): IO[ToResponseMarshallable] = + for { + ctx <- ev.ask[AppContext] + apiCall = runtimeV2Service.startRuntime(userInfo, runtimeName, workspaceId) + _ <- metrics.incrementCounter("startRuntimeV2") + resp <- ctx.span.fold(apiCall)(span => + spanResource[IO](span, "startRuntimeV2") + .use(_ => apiCall) + ) + } yield StatusCodes.Accepted -> resp: ToResponseMarshallable + + private[api] def stopRuntimeHandler(userInfo: UserInfo, workspaceId: WorkspaceId, runtimeName: RuntimeName)(implicit + ev: Ask[IO, AppContext] + ): IO[ToResponseMarshallable] = + for { + ctx <- ev.ask[AppContext] + apiCall = runtimeV2Service.stopRuntime(userInfo, runtimeName, workspaceId) + _ <- metrics.incrementCounter("stopRuntimeV2") + resp <- ctx.span.fold(apiCall)(span => + spanResource[IO](span, "stopRuntimeV2") + .use(_ => apiCall) + ) + } yield StatusCodes.Accepted -> resp: ToResponseMarshallable } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala index ff4edef6b5..19e35d09ee 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2Service.scala @@ -14,4 +14,12 @@ trait RuntimeV2Service[F[_]] { )(implicit as: Ask[F, AppContext] ): F[Vector[ListRuntimeResponse2]] + + def startRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit + as: Ask[F, AppContext] + ): F[Unit] + + def stopRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit + as: Ask[F, AppContext] + ): F[Unit] } diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala index 37588d247d..9fc0bfcc5e 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterp.scala @@ -4,17 +4,25 @@ package service import cats.Parallel import cats.effect.Async +import cats.effect.std.Queue import cats.mtl.Ask import cats.syntax.all._ import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.RuntimeSamResourceId -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService +import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamService, SamUtils} import org.broadinstitute.dsde.workbench.leonardo.db._ +import org.broadinstitute.dsde.workbench.leonardo.model.{ + RuntimeCannotBeStartedException, + RuntimeCannotBeStoppedException +} import org.broadinstitute.dsde.workbench.leonardo.model.SamResource.RuntimeSamResource +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{StartRuntimeMessage, StopRuntimeMessage} import org.broadinstitute.dsde.workbench.model.UserInfo import scala.concurrent.ExecutionContext class RuntimeV2ServiceInterp[F[_]: Parallel]( + publisherQueue: Queue[F, LeoPubsubMessage], samService: SamService[F] )(implicit F: Async[F], dbReference: DbReference[F], ec: ExecutionContext) extends RuntimeV2Service[F] { @@ -47,4 +55,48 @@ class RuntimeV2ServiceInterp[F[_]: Parallel]( .transaction } yield runtimes.toVector + + def startRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit + as: Ask[F, AppContext] + ): F[Unit] = for { + ctx <- as.ask + runtime <- getClusterRecordWithRequiredAction(userInfo, workspaceId, runtimeName, RuntimeAction.StopStartRuntime) + _ <- + if (runtime.status.isStartable) F.unit + else + F.raiseError[Unit](RuntimeCannotBeStartedException(runtime.cloudContext, runtime.runtimeName, runtime.status)) + _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.PreStarting, ctx.now).transaction + _ <- publisherQueue.offer(StartRuntimeMessage(runtime.id, Some(ctx.traceId))) + } yield () + + def stopRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit + as: Ask[F, AppContext] + ): F[Unit] = for { + ctx <- as.ask + + runtime <- getClusterRecordWithRequiredAction(userInfo, workspaceId, runtimeName, RuntimeAction.StopStartRuntime) + _ <- + if (runtime.status.isStoppable) F.unit + else + F.raiseError[Unit](RuntimeCannotBeStoppedException(runtime.cloudContext, runtime.runtimeName, runtime.status)) + _ <- clusterQuery.updateClusterStatus(runtime.id, RuntimeStatus.PreStopping, ctx.now).transaction + _ <- publisherQueue.offer(StopRuntimeMessage(runtime.id, Some(ctx.traceId))) + } yield () + + private def getClusterRecordWithRequiredAction( + userInfo: UserInfo, + workspaceId: WorkspaceId, + runtimeName: RuntimeName, + action: RuntimeAction + )(implicit as: Ask[F, AppContext]): F[ClusterRecord] = + for { + runtime <- RuntimeServiceDbQueries.getActiveRuntimeRecord(workspaceId, runtimeName).transaction + _ <- SamUtils.checkRuntimeAction(samService, + userInfo, + workspaceId, + runtimeName, + RuntimeSamResourceId(runtime.internalId), + action + ) + } yield runtime } diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala index 482c1c4244..91a681fba1 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/MockRuntimeV2Interp.scala @@ -37,4 +37,12 @@ object MockRuntimeV2Interp extends RuntimeV2Service[IO] { ) ) ) + + override def startRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit + as: Ask[IO, AppContext] + ): IO[Unit] = IO.unit + + override def stopRuntime(userInfo: UserInfo, runtimeName: RuntimeName, workspaceId: WorkspaceId)(implicit + as: Ask[IO, AppContext] + ): IO[Unit] = IO.unit } diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala index 923d5c64aa..09b15cdc4e 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/service/RuntimeV2ServiceInterpSpec.scala @@ -2,18 +2,23 @@ package org.broadinstitute.dsde.workbench.leonardo package http package service +import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers.OAuth2BearerToken import cats.effect.IO +import cats.effect.std.Queue import org.broadinstitute.dsde.workbench.azure._ import org.broadinstitute.dsde.workbench.leonardo.CommonTestData._ import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.{RuntimeSamResourceId, WsmResourceSamResourceId} import org.broadinstitute.dsde.workbench.leonardo.TestUtils.appContext -import org.broadinstitute.dsde.workbench.leonardo.dao.sam.SamService +import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamException, SamService} import org.broadinstitute.dsde.workbench.leonardo.db._ import org.broadinstitute.dsde.workbench.leonardo.model.SamResource.RuntimeSamResource import org.broadinstitute.dsde.workbench.leonardo.model._ +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage +import org.broadinstitute.dsde.workbench.leonardo.monitor.LeoPubsubMessage.{StartRuntimeMessage, StopRuntimeMessage} +import org.broadinstitute.dsde.workbench.leonardo.util.QueueFactory import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.broadinstitute.dsde.workbench.model.{UserInfo, WorkbenchEmail, WorkbenchUserId} +import org.broadinstitute.dsde.workbench.model.{TraceId, UserInfo, WorkbenchEmail, WorkbenchUserId} import org.mockito.ArgumentMatchers.{any, eq => isEq} import org.mockito.Mockito.when import org.scalatest.flatspec.AnyFlatSpec @@ -24,7 +29,9 @@ import scala.concurrent.ExecutionContext.Implicits.global class RuntimeV2ServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with TestComponent with MockitoSugar { - def makeInterp(samService: SamService[IO] = MockSamService) = new RuntimeV2ServiceInterp[IO](samService) + def makeInterp(queue: Queue[IO, LeoPubsubMessage] = QueueFactory.makePublisherQueue(), + samService: SamService[IO] = MockSamService + ) = new RuntimeV2ServiceInterp[IO](queue, samService) def setRuntimeDeleted(workspaceId: WorkspaceId, name: RuntimeName): IO[Long] = for { @@ -43,14 +50,205 @@ class RuntimeV2ServiceInterpSpec extends AnyFlatSpec with LeonardoTestSuite with val runtimeV2Service = new RuntimeV2ServiceInterp[IO]( + QueueFactory.makePublisherQueue(), MockSamService ) val runtimeV2Service2 = new RuntimeV2ServiceInterp[IO]( + QueueFactory.makePublisherQueue(), MockSamService ) + it should "publish start a runtime message properly" in isolatedDbTest { + val workspaceId = WorkspaceId(UUID.randomUUID()) + + val publisherQueue = QueueFactory.makePublisherQueue() + val azureService = makeInterp(publisherQueue) + val res = for { + ctx <- appContext.ask[AppContext] + runtime <- IO( + makeCluster(0) + .copy( + status = RuntimeStatus.Stopped, + workspaceId = Some(workspaceId), + auditInfo = auditInfo.copy(creator = userInfo.userEmail) + ) + .save() + ) + _ <- azureService + .startRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) + msg <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with + + } yield msg shouldBe Some(StartRuntimeMessage(runtime.id, Some(ctx.traceId))) + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "fail to start a runtime if permission denied" in isolatedDbTest { + // User is runtime creator, but does not have access to the workspace + val userInfo = UserInfo(OAuth2BearerToken(""), WorkbenchUserId("user"), WorkbenchEmail("email"), 0) + val workspaceId = WorkspaceId(UUID.randomUUID()) + val samService = mock[SamService[IO]] + when( + samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.StopStartRuntime))(any()) + ) + .thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) + when( + samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) + ).thenReturn(IO.unit) + val interp = makeInterp(samService = samService) + + val res = for { + runtime <- IO( + makeCluster(0) + .copy( + status = RuntimeStatus.Running, + workspaceId = Some(workspaceId), + auditInfo = auditInfo.copy(creator = userInfo.userEmail) + ) + .save() + ) + r <- interp + .startRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) + .attempt + } yield { + val exception = r.swap.toOption.get + exception.getMessage shouldBe s"email is unauthorized. If you have proper permissions to use the workspace, make sure you are also added to the billing account" + } + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "fail to start a runtime when runtime doesn't exist in DB" in isolatedDbTest { + val runtimeName = RuntimeName("clusterName1") + val workspaceId = WorkspaceId(UUID.randomUUID()) + + val res = + runtimeV2Service + .startRuntime(userInfo, runtimeName, workspaceId) + .attempt + .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + + val exception = res.swap.toOption.get + exception.isInstanceOf[RuntimeNotFoundByWorkspaceIdException] shouldBe true + exception.getMessage shouldBe s"Runtime clusterName1 not found in workspace ${workspaceId.value}" + } + + it should "fail to start a runtime when runtime is not in startable statuses" in isolatedDbTest { + val res = for { + runtime <- IO( + makeCluster(0) + .copy( + status = RuntimeStatus.Running, + workspaceId = Some(workspaceId), + auditInfo = auditInfo.copy(creator = userInfo.userEmail) + ) + .save() + ) + res <- runtimeV2Service + .startRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) + .attempt + } yield { + val exception = res.swap.toOption.get + exception.isInstanceOf[RuntimeCannotBeStartedException] shouldBe true + exception.getMessage shouldBe "Runtime Gcp/dsp-leo-test cannot be started in Running status" + } + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "publish stop a runtime message properly" in isolatedDbTest { + val workspaceId = WorkspaceId(UUID.randomUUID()) + + val publisherQueue = QueueFactory.makePublisherQueue() + val azureService = makeInterp(publisherQueue) + val res = for { + ctx <- appContext.ask[AppContext] + runtime <- IO( + makeCluster(0) + .copy( + status = RuntimeStatus.Running, + workspaceId = Some(workspaceId), + auditInfo = auditInfo.copy(creator = userInfo.userEmail) + ) + .save() + ) + _ <- azureService + .stopRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) + msg <- publisherQueue.tryTake // just to make sure there's no messages in the queue to start with + + } yield msg shouldBe Some(StopRuntimeMessage(runtime.id, Some(ctx.traceId))) + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "fail to stop a runtime if permission denied" in isolatedDbTest { + val userInfo = UserInfo(OAuth2BearerToken(""), WorkbenchUserId("user"), WorkbenchEmail("email"), 0) + val workspaceId = WorkspaceId(UUID.randomUUID()) + val samService = mock[SamService[IO]] + when( + samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.StopStartRuntime))(any()) + ) + .thenReturn(IO.raiseError(SamException.create("no access", StatusCodes.Forbidden.intValue, TraceId("")))) + when( + samService.checkAuthorized(isEq(userInfo.accessToken.token), any(), isEq(RuntimeAction.GetRuntimeStatus))(any()) + ).thenReturn(IO.unit) + val interp = makeInterp(samService = samService) + + val res = for { + runtime <- IO( + makeCluster(0) + .copy( + status = RuntimeStatus.Running, + workspaceId = Some(workspaceId), + auditInfo = auditInfo.copy(creator = userInfo.userEmail) + ) + .save() + ) + r <- interp + .stopRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) + .attempt + } yield { + val exception = r.swap.toOption.get + exception.getMessage shouldBe s"email is unauthorized. If you have proper permissions to use the workspace, make sure you are also added to the billing account" + } + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + + it should "fail to stop a runtime when runtime doesn't exist in DB" in isolatedDbTest { + val runtimeName = RuntimeName("clusterName1") + val workspaceId = WorkspaceId(UUID.randomUUID()) + + val res = + runtimeV2Service + .stopRuntime(userInfo, runtimeName, workspaceId) + .attempt + .unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + + val exception = res.swap.toOption.get + exception.isInstanceOf[RuntimeNotFoundByWorkspaceIdException] shouldBe true + exception.getMessage shouldBe s"Runtime clusterName1 not found in workspace ${workspaceId.value}" + } + + it should "fail to stop a runtime when runtime is not in startable statuses" in isolatedDbTest { + val res = for { + runtime <- IO( + makeCluster(0) + .copy( + status = RuntimeStatus.Stopped, + workspaceId = Some(workspaceId), + auditInfo = auditInfo.copy(creator = userInfo.userEmail) + ) + .save() + ) + res <- runtimeV2Service + .stopRuntime(userInfo, runtime.runtimeName, runtime.workspaceId.get) + .attempt + } yield { + val exception = res.swap.toOption.get + exception.isInstanceOf[RuntimeCannotBeStoppedException] shouldBe true + exception.getMessage shouldBe s"Runtime Gcp/dsp-leo-test/${runtime.runtimeName.asString} cannot be stopped in Stopped status" + } + res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global) + } + it should "list runtimes" in isolatedDbTest { val runtimeId1 = UUID.randomUUID.toString val runtimeId2 = UUID.randomUUID.toString From 595e0de44a942a4bda177e46107955d85a39817c Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Thu, 17 Jul 2025 10:23:32 -0400 Subject: [PATCH 42/43] Fix route structure --- .../leonardo/http/api/RuntimeV2Routes.scala | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala index 7739b6d387..494294cdd6 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/RuntimeV2Routes.scala @@ -37,34 +37,34 @@ class RuntimeV2Routes(runtimeV2Service: RuntimeV2Service[IO], userInfoDirectives ) } } - } - } ~ - pathPrefix(workspaceIdSegment) { workspaceId => - pathPrefix(runtimeNameSegmentWithValidation) { runtimeName => - path("stop") { - post { - complete( - stopRuntimeHandler( - userInfo, - workspaceId, - runtimeName - ) - ) - } - } ~ - path("start") { + } ~ + pathPrefix(workspaceIdSegment) { workspaceId => + pathPrefix(runtimeNameSegmentWithValidation) { runtimeName => + path("stop") { post { complete( - startRuntimeHandler( + stopRuntimeHandler( userInfo, workspaceId, runtimeName ) ) } - } + } ~ + path("start") { + post { + complete( + startRuntimeHandler( + userInfo, + workspaceId, + runtimeName + ) + ) + } + } + } } - } + } } } } From 94fc0ffaaa7b7a1f92d0a231e59b3ccfc474b4ab Mon Sep 17 00:00:00 2001 From: Janet Dewar Date: Tue, 22 Jul 2025 10:22:04 -0400 Subject: [PATCH 43/43] PR feedback --- .../http/BaselineDependenciesBuilder.scala | 11 ----------- .../leonardo/http/api/HttpRoutesSpec.scala | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala index e0d3a6f35a..8edf6356f8 100644 --- a/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala +++ b/http/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/http/BaselineDependenciesBuilder.scala @@ -148,17 +148,6 @@ class BaselineDependenciesBuilder { HttpDockerDAO[F](client) ) - azureRelay <- AzureRelayService.fromAzureAppRegistrationConfig(ConfigReader.appConfig.azure.appRegistration) - - azureVmService <- AzureVmService.fromAzureAppRegistrationConfig(ConfigReader.appConfig.azure.appRegistration) - - azureBatchService <- AzureBatchService.fromAzureAppRegistrationConfig( - ConfigReader.appConfig.azure.appRegistration - ) - - azureApplicationInsightsService <- AzureApplicationInsightsService.fromAzureAppRegistrationConfig( - ConfigReader.appConfig.azure.appRegistration - ) // Set up identity providers underlyingAuthCache = buildCache[AuthCacheKey, scalacache.Entry[Boolean]](samAuthConfig.authCacheMaxSize, samAuthConfig.authCacheExpiryTime diff --git a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala index f477a46ec3..a1f0a45609 100644 --- a/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala +++ b/http/src/test/scala/org/broadinstitute/dsde/workbench/leonardo/http/api/HttpRoutesSpec.scala @@ -438,6 +438,15 @@ class HttpRoutesSpec } } + it should "list runtimes v2 without a workspace or cloudContext" in { + Get("/api/v2/runtimes") ~> routes.route ~> check { + status shouldEqual StatusCodes.OK + val response = responseAs[Vector[ListRuntimeResponse2]] + response.map(_.clusterName) shouldBe Vector(RuntimeName("azureruntime1")) + validateRawCookie(header("Set-Cookie")) + } + } + "DiskRoutes" should "create a disk" in { val diskCreateRequest = CreateDiskRequest( Map("foo" -> "bar"), @@ -543,12 +552,17 @@ class HttpRoutesSpec it should "have expected azure routes when azure hosting mode is true" in { val adminRoute = "/api/admin/v2/apps/update" + val runtimeV2Route = "/api/v2/runtimes" val statusRoute = "/status" Get(adminRoute) ~> httpRoutesAzureOnly.route ~> check { status should not be StatusCodes.NotFound } + Get(runtimeV2Route) ~> httpRoutesAzureOnly.route ~> check { + status should not be StatusCodes.NotFound + } + Get(statusRoute) ~> httpRoutesAzureOnly.route ~> check { status should not be StatusCodes.NotFound }