Skip to content

Commit

Permalink
[WOR-1700] Billing project support API (#3058)
Browse files Browse the repository at this point in the history
  • Loading branch information
marctalbott authored Oct 2, 2024
1 parent c76c640 commit bab8a99
Show file tree
Hide file tree
Showing 12 changed files with 399 additions and 42 deletions.
133 changes: 118 additions & 15 deletions core/src/main/resources/swagger/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,36 @@ paths:
$ref: '#/components/schemas/ErrorReport'
500:
$ref: '#/components/responses/RawlsInternalError'
/api/admin/billing/{projectId}:
get:
tags:
- admin
summary: get support summary information for billing project
description: get support summary information for billing project
operationId: adminGetBillingProject
parameters:
- $ref: '#/components/parameters/billingProjectIdPathParam'
responses:
200:
description: Successful Request
content:
'application/json':
schema:
$ref: '#/components/schemas/BillingProjectAdminResponse'
403:
description: You must be an admin to call this API
content:
'application/json':
schema:
$ref: '#/components/schemas/ErrorReport'
404:
description: Billing project not found
content:
'application/json':
schema:
$ref: '#/components/schemas/ErrorReport'
500:
$ref: '#/components/responses/RawlsInternalError'
/api/admin/submissions:
get:
tags:
Expand Down Expand Up @@ -6736,6 +6766,74 @@ components:
$ref: '#/components/schemas/ProjectRole'
description: the role of the user in the project
description: an element of a list of project users and their role
BillingProjectAdminResponse:
required:
- billingProject
- workspaces
type: object
properties:
billingProject:
$ref: '#/components/schemas/RawlsBillingProject'
workspaces:
type: object
additionalProperties:
type: string
format: uuid
example:
"workspace1": "00000000-0000-0000-0000-000000000000"
"workspace2": "11111111-1111-1111-1111-111111111111"
description: "workspaces in the billing project with their IDs"
RawlsBillingProject:
required:
- projectName
- status
- invalidBillingAccount
type: object
description: internal representation of a billing project, returned by admin API only
properties:
projectName:
type: string
description: the name of the project
billingAccount:
type: string
description: the billing account to use in google projects (cloudPlatform GCP only)
invalidBillingAccount:
type: boolean
description: whether or not the billing account is usable by Terra
status:
$ref: '#/components/schemas/BillingProjectStatus'
message:
type: string
description: informational message about the project
azureManagedAppCoordinates:
$ref: '#/components/schemas/AzureManagedAppCoordinates'
cloudPlatform:
$ref: '#/components/schemas/BillingProjectCloudPlatform'
landingZoneId:
type: string
format: uuid
description: the UUID of the landing zone associated with the project (cloudPlatform AZURE only)
billingProfileId:
type: string
description: the billing profile ID associated with the project
cromwellBackend:
type: string
description: the cromwell backend to use for this billing project
servicePerimeter:
type: string
description: the name of the service perimeter for this billing project (cloudPlatform GCP only)
googleProjectNumber:
type: string
description: the google project number for this billing project (cloudPlatform GCP only)
spendReportDataset:
type: string
description: the name of the BigQuery dataset containing project spend data (cloudPlatform GCP only)
spendReportTable:
type: string
description: the name of the BigQuery table containing project spend data (cloudPlatform GCP only)
spendReportDatasetGoogleProject:
type: string
description: the name of the Google Project where the BigQuery dataset resides (cloudPlatform GCP only)
RawlsBillingProjectResponse:
required:
- projectName
Expand All @@ -6761,27 +6859,14 @@ components:
$ref: '#/components/schemas/ProjectRole'
description: the roles the caller has on the project
status:
type: string
enum:
- Creating
- Ready
- Error
- Deleting
- DeletionFailed
- AddingToPerimeter
- CreatingLandingZone
description: the status of allocating the billing project's resources.
$ref: '#/components/schemas/BillingProjectStatus'
message:
type: string
description: informational message about the project
azureManagedAppCoordinates:
$ref: '#/components/schemas/AzureManagedAppCoordinates'
cloudPlatform:
type: string
enum:
- GCP
- AZURE
- UNKNOWN
$ref: '#/components/schemas/BillingProjectCloudPlatform'
landingZoneId:
type: string
format: uuid
Expand All @@ -6794,6 +6879,24 @@ components:
description: whether this billing project supports protected data (cloudPlatform AZURE only)
organization:
$ref: '#/components/schemas/RawlsBillingProjectOrganization'
BillingProjectStatus:
type: string
enum:
- Creating
- Ready
- Error
- Deleting
- DeletionFailed
- AddingToPerimeter
- CreatingLandingZone
description: the status of allocating the billing project's resources.
BillingProjectCloudPlatform:
type: string
enum:
- GCP
- AZURE
- UNKNOWN
description: the cloud platform of the billing project
AzureManagedAppCoordinates:
required:
- tenantId
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/org/broadinstitute/dsde/rawls/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ object Boot extends IOApp with LazyLogging {
spendReportingServiceConfig
)

val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService =
new BillingAdminService(samDAO, billingRepository, workspaceRepository, _)

val bucketMigrationServiceConstructor: RawlsRequestContext => BucketMigrationService =
BucketMigrationServiceFactory.createBucketMigrationService(appConfigManager, slickDataSource, samDAO, gcsDAO)

Expand All @@ -532,6 +535,7 @@ object Boot extends IOApp with LazyLogging {
workspaceSettingServiceConstructor,
entityServiceConstructor,
userServiceConstructor,
billingAdminServiceConstructor,
genomicsServiceConstructor,
snapshotServiceConstructor,
spendReportingServiceConstructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.broadinstitute.dsde.rawls.billing

import akka.http.scaladsl.model.StatusCodes
import com.typesafe.scalalogging.LazyLogging
import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport
import org.broadinstitute.dsde.rawls.dataaccess.SamDAO
import org.broadinstitute.dsde.rawls.model.{
BillingProjectAdminResponse,
ErrorReport,
RawlsBillingProjectName,
RawlsRequestContext,
SamResourceTypeAdminActions,
SamResourceTypeNames
}
import org.broadinstitute.dsde.rawls.workspace.WorkspaceRepository

import scala.concurrent.{ExecutionContext, Future}

class BillingAdminService(samDAO: SamDAO,
billingRepository: BillingRepository,
workspaceRepository: WorkspaceRepository,
ctx: RawlsRequestContext
)(implicit protected val ec: ExecutionContext)
extends LazyLogging {

def getBillingProjectSupportSummary(
billingProjectName: RawlsBillingProjectName
): Future[BillingProjectAdminResponse] =
for {
userIsAdmin <- samDAO.admin.userHasResourceTypeAdminPermission(SamResourceTypeNames.billingProject,
SamResourceTypeAdminActions.readSummaryInformation,
ctx
)
_ = if (!userIsAdmin)
throw new RawlsExceptionWithErrorReport(
ErrorReport(StatusCodes.Forbidden, "You must be an admin to call this API.")
)

billingProjectOpt <- billingRepository.getBillingProject(billingProjectName)
billingProject = billingProjectOpt.getOrElse(
throw new RawlsExceptionWithErrorReport(
ErrorReport(StatusCodes.NotFound, s"Billing project ${billingProjectName.value} not found.")
)
)
workspaces <- workspaceRepository.listWorkspacesByBillingProject(billingProjectName)
} yield BillingProjectAdminResponse(billingProject, workspaces.map(ws => (ws.name, ws.workspaceIdAsUUID)).toMap)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.broadinstitute.dsde.workbench.model.google.GoogleModelJsonSupport._
import org.broadinstitute.dsde.workbench.model.google.{BigQueryDatasetName, BigQueryTableName, GoogleProject}
import spray.json._

import java.util.UUID
import scala.language.implicitConversions

case class RawlsBillingProjectMembership(projectName: RawlsBillingProjectName,
Expand Down Expand Up @@ -133,6 +134,8 @@ object RawlsBillingProjectResponse {
)
}

case class BillingProjectAdminResponse(billingProject: RawlsBillingProject, workspaces: Map[String, UUID])

case class RawlsBillingProjectTransfer(project: String, bucket: String, newOwnerEmail: String, newOwnerToken: String)

case class ProjectAccessUpdate(email: String, role: ProjectRole)
Expand Down Expand Up @@ -331,6 +334,10 @@ class UserAuthJsonSupport extends JsonSupport {
RawlsBillingProjectOrganization.apply
)

implicit val billingProjectAdminResponse: RootJsonFormat[BillingProjectAdminResponse] = jsonFormat2(
BillingProjectAdminResponse
)

implicit val RawlsBillingProjectResponseFormat: RootJsonFormat[RawlsBillingProjectResponse] =
jsonFormat13(RawlsBillingProjectResponse.apply)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import akka.http.scaladsl.server
import akka.http.scaladsl.server.Directives._
import io.opentelemetry.context.Context
import org.broadinstitute.dsde.rawls.RawlsException
import org.broadinstitute.dsde.rawls.billing.BillingAdminService
import org.broadinstitute.dsde.rawls.bucketMigration.{BucketMigrationService, BucketMigrationServiceImpl}
import org.broadinstitute.dsde.rawls.model.ExecutionJsonSupport._
import org.broadinstitute.dsde.rawls.model.WorkspaceJsonSupport._
Expand All @@ -34,20 +35,29 @@ trait AdminApiService extends UserInfoDirectives {
val submissionsServiceConstructor: RawlsRequestContext => SubmissionsService
val userServiceConstructor: RawlsRequestContext => UserService
val bucketMigrationServiceConstructor: RawlsRequestContext => BucketMigrationService
val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService

def adminRoutes(otelContext: Context = Context.root()): server.Route = {
requireUserInfo(Option(otelContext)) { userInfo =>
val ctx = RawlsRequestContext(userInfo, Option(otelContext))
path("admin" / "billing" / Segment) { projectId =>
delete {
entity(as[Map[String, String]]) { ownerInfo =>
complete {
userServiceConstructor(ctx)
.adminDeleteBillingProject(RawlsBillingProjectName(projectId), ownerInfo)
.map(_ => StatusCodes.NoContent)
val billingProjectName = RawlsBillingProjectName(projectId)
get {
complete {
billingAdminServiceConstructor(ctx)
.getBillingProjectSupportSummary(billingProjectName)
.map(StatusCodes.OK -> _)
}
} ~
delete {
entity(as[Map[String, String]]) { ownerInfo =>
complete {
userServiceConstructor(ctx)
.adminDeleteBillingProject(billingProjectName, ownerInfo)
.map(_ => StatusCodes.NoContent)
}
}
}
}
} ~
path("admin" / "submissions") {
get {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import com.typesafe.scalalogging.LazyLogging
import io.opentelemetry.context.Context
import io.sentry.Sentry
import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport
import org.broadinstitute.dsde.rawls.billing.BillingProjectOrchestrator
import org.broadinstitute.dsde.rawls.billing.{BillingAdminService, BillingProjectOrchestrator}
import org.broadinstitute.dsde.rawls.bucketMigration.BucketMigrationService
import org.broadinstitute.dsde.rawls.dataaccess.{ExecutionServiceCluster, SamDAO}
import org.broadinstitute.dsde.rawls.entities.EntityService
Expand All @@ -32,12 +32,7 @@ import org.broadinstitute.dsde.rawls.spendreporting.SpendReportingService
import org.broadinstitute.dsde.rawls.status.StatusService
import org.broadinstitute.dsde.rawls.submissions.SubmissionsService
import org.broadinstitute.dsde.rawls.user.UserService
import org.broadinstitute.dsde.rawls.workspace.{
MultiCloudWorkspaceService,
WorkspaceAdminService,
WorkspaceService,
WorkspaceSettingService
}
import org.broadinstitute.dsde.rawls.workspace.{MultiCloudWorkspaceService, WorkspaceAdminService, WorkspaceService, WorkspaceSettingService}
import org.broadinstitute.dsde.workbench.oauth2.OpenIDConnectConfiguration

import java.sql.{SQLException, SQLTransactionRollbackException}
Expand Down Expand Up @@ -221,6 +216,7 @@ class RawlsApiServiceImpl(val multiCloudWorkspaceServiceConstructor: RawlsReques
val workspaceSettingServiceConstructor: RawlsRequestContext => WorkspaceSettingService,
val entityServiceConstructor: RawlsRequestContext => EntityService,
val userServiceConstructor: RawlsRequestContext => UserService,
val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService,
val genomicsServiceConstructor: RawlsRequestContext => GenomicsService,
val snapshotServiceConstructor: RawlsRequestContext => SnapshotService,
val spendReportingConstructor: RawlsRequestContext => SpendReportingService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,7 @@ import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport
import org.broadinstitute.dsde.rawls.dataaccess.SlickDataSource
import org.broadinstitute.dsde.rawls.dataaccess.slick.PendingBucketDeletionRecord
import org.broadinstitute.dsde.rawls.model.Attributable.AttributeMap
import org.broadinstitute.dsde.rawls.model.{
ErrorReport,
PendingCloneWorkspaceFileTransfer,
RawlsRequestContext,
Workspace,
WorkspaceAttributeSpecs,
WorkspaceName,
WorkspaceState,
WorkspaceSubmissionStats,
WorkspaceTag
}
import org.broadinstitute.dsde.rawls.model.{ErrorReport, PendingCloneWorkspaceFileTransfer, RawlsBillingProjectName, RawlsRequestContext, Workspace, WorkspaceAttributeSpecs, WorkspaceName, WorkspaceState, WorkspaceSubmissionStats, WorkspaceTag}
import org.broadinstitute.dsde.rawls.model.WorkspaceState.WorkspaceState
import org.broadinstitute.dsde.rawls.util.TracingUtils.traceDBIOWithParent
import org.joda.time.DateTime
Expand Down Expand Up @@ -57,6 +47,10 @@ class WorkspaceRepository(dataSource: SlickDataSource) {
_.workspaceQuery.listV2WorkspacesByIds(workspaceIds, attributeSpecs)
}

def listWorkspacesByBillingProject(billingProjectName: RawlsBillingProjectName): Future[Seq[Workspace]] = dataSource.inTransaction {
_.workspaceQuery.listWithBillingProject(billingProjectName)
}

def createWorkspace(workspace: Workspace): Future[Workspace] =
dataSource.inTransaction { access =>
access.workspaceQuery.createOrUpdate(workspace)
Expand Down
Loading

0 comments on commit bab8a99

Please sign in to comment.