Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/api/environments/v3 [put] (sketch) #542

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions sherlock/internal/api/sherlock/environments_v3_upsert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package sherlock

import (
"fmt"
"github.com/broadinstitute/sherlock/sherlock/internal/authentication"
"github.com/broadinstitute/sherlock/sherlock/internal/errors"
"github.com/broadinstitute/sherlock/sherlock/internal/models"
"github.com/creasty/defaults"
"github.com/gin-gonic/gin"
"gorm.io/gorm/clause"
"net/http"
)

// environmentsV3Upsert godoc
//
// @summary Upsert a dynamic Environment
// @description Create or get a dynamic environment, depending on whether it already exists. If it already exists, you'll be marked as its owner. It refuses to work with non-dynamic environments (you can't specify a lifecycle of "static" or "template").
// @tags Environments
// @accept json
// @produce json
// @param environment body EnvironmentV3Create true "The Environment to upsert"
// @success 201 {object} EnvironmentV3
// @failure 400,403,404,407,409,500 {object} errors.ErrorResponse
// @router /api/environments/v3 [put]
func environmentsV3Upsert(ctx *gin.Context) {
user, err := authentication.MustUseUser(ctx)
if err != nil {
return
}

db, err := authentication.MustUseDB(ctx)
if err != nil {
return
}

var body EnvironmentV3Create
if err = ctx.ShouldBindJSON(&body); err != nil {
errors.AbortRequest(ctx, fmt.Errorf("(%s) request validation error: %w", errors.BadRequest, err))
return
}

if err = defaults.Set(&body); err != nil {
errors.AbortRequest(ctx, fmt.Errorf("error setting defaults for environment: %w", err))
return
}

// Even if the user didn't specify "dynamic", we would've just filled it in ourselves from
// defaults.Set(&body) above. If the user specified "static" or "template", we definitely
// don't want them using upsert, so we bail out.
if body.Lifecycle != "dynamic" {
errors.AbortRequest(ctx, fmt.Errorf("(%s) lifecycle for upsert must be dynamic", errors.BadRequest))
return
}

toUpsert, err := body.toModel(db)
if err != nil {
errors.AbortRequest(ctx, err)
return
}

// We're just starting the Gorm "chain" here, not finishing it.
// See https://gorm.io/docs/method_chaining.html
upsertTransaction := db.Where(&toUpsert)

// If we're not upserting a specific BEE, only match ones that aren't allocated to someone.
//
// Fun fact: Sherlock happens to enforce against null owners right now, so this'll always
// fail to find a match, just for the moment. That's okay!
//
// When we add pooling, I'm making a guess that we'll implement that by making owner a nullable
// field, and unallocated BEEs will just have no owner yet. Maybe we'll make a separate field
// to represent an unallocated BEE... if we do, we should update this query to match that field
// instead.
//
// What does this query do right now since owner can't be null? It means we'll always fail to
// find a match, so we'll always create a new BEE if a name isn't passed.
if toUpsert.Name == "" {
upsertTransaction = upsertTransaction.Where("owner_id IS NULL AND legacy_owner IS NULL")
}

// Regardless of whether we're adding a BEE or finding one, we always want to claim ownership of it.
upsertTransaction = upsertTransaction.Assign(&models.Environment{OwnerID: &user.ID})

// Run the upsert transaction
var result models.Environment
if err = upsertTransaction.FirstOrCreate(&result).Error; err != nil {
errors.AbortRequest(ctx, err)
return
}

// Now we load the environment, with all the associations, to return.
//
// We can reuse the same variable here because we're done with the upsert. The reason we don't
// add a preload clause to that upsert is because it is potentially a mutation operation, and
// we never want to have a preload and a mutation coexist (it can update the associations!)
if err = db.Preload(clause.Associations).First(&result, result.ID).Error; err != nil {
errors.AbortRequest(ctx, err)
return
}

ctx.JSON(http.StatusCreated, environmentFromModel(result))
}
1 change: 1 addition & 0 deletions sherlock/internal/api/sherlock/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func ConfigureRoutes(apiRouter gin.IRoutes) {

apiRouter.POST("environments/v3", environmentsV3Create)
apiRouter.GET("environments/v3", environmentsV3List)
apiRouter.PUT("environments/v3", environmentsV3Upsert)
apiRouter.DELETE("environments/v3/*selector", environmentsV3Delete)
apiRouter.PATCH("environments/v3/*selector", environmentsV3Edit)
apiRouter.GET("environments/v3/*selector", environmentsV3Get)
Expand Down
Loading