From 9580086cc007734850400e9ff68f020e388e718d Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Thu, 17 Feb 2022 17:07:12 -0600 Subject: [PATCH 01/22] Add support for multiple paymodels --- hatchery/config.go | 24 ++-- hatchery/efs.go | 4 +- hatchery/hatchery.go | 89 +++++++++++--- hatchery/paymodels.go | 265 ++++++++++++++++++++++++++++++++++++++++++ hatchery/pods.go | 99 ---------------- 5 files changed, 359 insertions(+), 122 deletions(-) create mode 100644 hatchery/paymodels.go diff --git a/hatchery/config.go b/hatchery/config.go index d478ccd9..43966ed8 100644 --- a/hatchery/config.go +++ b/hatchery/config.go @@ -56,19 +56,29 @@ type AppConfigInfo struct { // TODO remove PayModel from config once DynamoDB contains all necessary data type PayModel struct { - Name string `json:"name"` - User string `json:"user_id"` - AWSAccountId string `json:"aws_account_id"` - Region string `json:"region"` - Ecs string `json:"ecs"` - VpcId string `json:"vpcid"` - Subnet int `json:"subnet"` + Id string `json:"bmh_workspace_id"` + Name string `json:"workspace_type"` + User string `json:"user_id"` + AWSAccountId string `json:"account_id"` + Region string `json:"region"` + Ecs string `json:"ecs"` + Subnet int `json:"subnet"` + HardLimit int `json:"hard-limit"` + SoftLimit int `json:"soft-limit"` + TotalUsage int `json:"total-usage"` + CurrentPayModel bool `json:"current_pay_model"` +} + +type AllPayModels struct { + CurrentPayModel *PayModel `json:"current_pay_model"` + PayModels []PayModel `json:"all_pay_models"` } // HatcheryConfig is the root of all the configuration type HatcheryConfig struct { UserNamespace string `json:"user-namespace"` DefaultPayModel PayModel `json:"default-pay-model"` + DisableLocalWS bool `json:"disable-local-ws"` PayModels []PayModel `json:"pay-models"` PayModelsDynamodbTable string `json:"pay-models-dynamodb-table"` SubDir string `json:"sub-dir"` diff --git a/hatchery/efs.go b/hatchery/efs.go index 3f6810c8..ab09eff6 100644 --- a/hatchery/efs.go +++ b/hatchery/efs.go @@ -63,10 +63,10 @@ func (creds *CREDS) createAccessPoint(FileSystemId string, userName string, svc if err != nil { return nil, err } - + ap := userToResourceName(userName, "service") + "-" + strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-accesspoint" if len(exResult.AccessPoints) == 0 { input := &efs.CreateAccessPointInput{ - ClientToken: aws.String(fmt.Sprintf("ap-%s", userToResourceName(userName, "pod"))), + ClientToken: aws.String(ap), FileSystemId: aws.String(FileSystemId), PosixUser: &efs.PosixUser{ Gid: aws.Int64(100), diff --git a/hatchery/hatchery.go b/hatchery/hatchery.go index bf135509..fc8f191d 100644 --- a/hatchery/hatchery.go +++ b/hatchery/hatchery.go @@ -24,6 +24,8 @@ func RegisterHatchery(mux *httptrace.ServeMux) { mux.HandleFunc("/status", status) mux.HandleFunc("/options", options) mux.HandleFunc("/paymodels", paymodels) + mux.HandleFunc("/setpaymodel", setpaymodel) + mux.HandleFunc("/allpaymodels", allpaymodels) // ECS functions mux.HandleFunc("/create-ecs-cluster", createECSCluster) @@ -55,16 +57,65 @@ func paymodels(w http.ResponseWriter, r *http.Request) { return } userName := getCurrentUserName(r) - payModel, err := getPayModelForUser(userName) + + payModel, err := getCurrentPayModel(userName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } if payModel == nil { - http.Error(w, err.Error(), http.StatusNotFound) + http.Error(w, "Current paymodel not set", http.StatusNotFound) return } + out, err := json.Marshal(payModel) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - out, err := json.Marshal(payModel) + fmt.Fprint(w, string(out)) +} + +func allpaymodels(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + userName := getCurrentUserName(r) + + payModels, err := getPayModelsForUser(userName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if payModels == nil { + http.Error(w, "No paymodel set", http.StatusNotFound) + return + } + out, err := json.Marshal(payModels) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(out)) +} + +func setpaymodel(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + userName := getCurrentUserName(r) + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, "Missing ID argument", http.StatusBadRequest) + return + } + pm, err := setCurrentPaymodel(userName, id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + out, err := json.Marshal(pm) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -76,19 +127,28 @@ func status(w http.ResponseWriter, r *http.Request) { userName := getCurrentUserName(r) accessToken := getBearerToken(r) - payModel, err := getPayModelForUser(userName) + payModel, err := getCurrentPayModel(userName) if err != nil { - Config.Logger.Printf(err.Error()) + if err != NopaymodelsError { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } var result *WorkspaceStatus - if payModel != nil && payModel.Ecs == "true" { + + if payModel.Ecs == "true" { result, err = statusEcs(r.Context(), userName, accessToken, payModel.AWSAccountId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { result, err = statusK8sPod(r.Context(), userName, accessToken, payModel) - } - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } out, err := json.Marshal(result) @@ -154,7 +214,7 @@ func launch(w http.ResponseWriter, r *http.Request) { } userName := getCurrentUserName(r) - payModel, err := getPayModelForUser(userName) + payModel, err := getCurrentPayModel(userName) if err != nil { Config.Logger.Printf(err.Error()) } @@ -166,6 +226,7 @@ func launch(w http.ResponseWriter, r *http.Request) { err = createExternalK8sPod(r.Context(), hash, userName, accessToken, *payModel) } if err != nil { + Config.Logger.Printf("error during launch: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -179,7 +240,7 @@ func terminate(w http.ResponseWriter, r *http.Request) { } accessToken := getBearerToken(r) userName := getCurrentUserName(r) - payModel, err := getPayModelForUser(userName) + payModel, err := getCurrentPayModel(userName) if err != nil { Config.Logger.Printf(err.Error()) } @@ -219,8 +280,8 @@ func getBearerToken(r *http.Request) string { // TODO: NEED TO CALL THIS FUNCTION IF IT DOESN'T EXIST!!! func createECSCluster(w http.ResponseWriter, r *http.Request) { userName := getCurrentUserName(r) - payModel, err := getPayModelForUser(userName) - if payModel == nil { + payModel, err := getCurrentPayModel(userName) + if &payModel == nil { http.Error(w, "Paymodel has not been setup for user", http.StatusNotFound) return } diff --git a/hatchery/paymodels.go b/hatchery/paymodels.go new file mode 100644 index 00000000..00a44f8e --- /dev/null +++ b/hatchery/paymodels.go @@ -0,0 +1,265 @@ +package hatchery + +import ( + "errors" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" +) + +var NopaymodelsError = errors.New("No paymodels found") + +func payModelsFromDatabase(userName string, current bool) (payModels *[]PayModel, err error) { + // query pay model data for this user from DynamoDB + sess := session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{ + Region: aws.String("us-east-1"), + }, + })) + dynamodbSvc := dynamodb.New(sess) + + filt := expression.Name("user_id").Equal(expression.Value(userName)) + filt = filt.And(expression.Name("request_status").Equal(expression.Value("active"))) + if current { + filt = filt.And(expression.Name("current_pay_model").Equal(expression.Value(true))) + } + expr, err := expression.NewBuilder().WithFilter(filt).Build() + if err != nil { + Config.Logger.Printf("Got error building expression: %s", err) + return nil, err + } + + params := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + TableName: aws.String(Config.Config.PayModelsDynamodbTable), + } + res, err := dynamodbSvc.Scan(params) + if err != nil { + Config.Logger.Printf("Query API call failed: %s", err) + return nil, err + } + + // Populate list of all available paymodels + var payModelMap []PayModel + err = dynamodbattribute.UnmarshalListOfMaps(res.Items, &payModelMap) + if err != nil { + return nil, err + } + + return &payModelMap, nil +} + +func payModelFromConfig(userName string) (pm *PayModel, err error) { + var payModel PayModel + for _, configPaymodel := range Config.PayModelMap { + if configPaymodel.User == userName { + payModel = configPaymodel + } + } + if (PayModel{} == payModel) { + return nil, NopaymodelsError + } + return &payModel, nil +} + +func getCurrentPayModel(userName string) (result *PayModel, err error) { + if Config.Config.PayModelsDynamodbTable == "" { + // fallback for backward compatibility. + // Multiple paymodels not supported + pm, err := payModelFromConfig(userName) + if err != nil { + pm, err = getDefaultPayModel() + if err != nil { + return nil, NopaymodelsError + } + } + return pm, nil + } + + payModel := PayModel{} + + pm, err := payModelsFromDatabase(userName, true) + + if len(*pm) == 0 { + pm, err := payModelFromConfig(userName) + if err != nil { + pm, err = getDefaultPayModel() + if err != nil { + return nil, nil + } + } + return pm, nil + } + if len(*pm) == 1 { + payModel = (*pm)[0] + if err != nil { + Config.Logger.Printf("Got error unmarshalling: %s", err) + return nil, err + } + } + if len(*pm) > 1 { + // TODO: Reset to zero current paymodels here. + // We don't want to be in a situation with multiple current paymodels + return nil, fmt.Errorf("multiple current paymodels set") + } + return &payModel, nil +} + +func getDefaultPayModel() (defaultPaymodel *PayModel, err error) { + var pm PayModel + if Config.Config.DefaultPayModel == pm { + return nil, fmt.Errorf("no default paymodel set") + } + return &Config.Config.DefaultPayModel, nil +} + +func getPayModelsForUser(userName string) (result *AllPayModels, err error) { + if userName == "" { + return nil, fmt.Errorf("no username sent in header") + } + PayModels := AllPayModels{} + + // Fallback to config-only if DynamoDB table is not configured + if Config.Config.PayModelsDynamodbTable == "" { + pm, err := payModelFromConfig(userName) + if err != nil { + pm, err = getDefaultPayModel() + if err != nil { + return nil, nil + } + } + if pm == nil { + return nil, NopaymodelsError + } + PayModels.CurrentPayModel = pm + return &PayModels, nil + } + + payModelMap, err := payModelsFromDatabase(userName, false) + if err != nil { + return nil, err + } + + // temporary fallback to the config to get data for users that are not + // in DynamoDB + // TODO: remove this block once we only rely on DynamoDB + payModel, err := payModelFromConfig(userName) + if err == nil { + *payModelMap = append(*payModelMap, *payModel) + } + + PayModels.PayModels = *payModelMap + + payModel, err = getCurrentPayModel(userName) + if err != nil { + return nil, err + } + + PayModels.CurrentPayModel = payModel + + return &PayModels, nil +} + +func setCurrentPaymodel(userName string, workspaceid string) (paymodel *PayModel, err error) { + sess := session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{ + Region: aws.String("us-east-1"), + }, + })) + dynamodbSvc := dynamodb.New(sess) + pm_db, err := payModelsFromDatabase(userName, false) + if err != nil { + return nil, err + } + pm_config, err := payModelFromConfig(userName) + if err != nil { + return nil, err + } + if pm_config.Id == workspaceid { + resetCurrentPaymodel(userName, dynamodbSvc) + return pm_config, nil + } + for _, pm := range *pm_db { + if pm.Id == workspaceid { + updateCurrentPaymodelInDB(userName, workspaceid, dynamodbSvc) + return &pm, nil + } + } + return nil, fmt.Errorf("No paymodel with id %s found for user %s", workspaceid, userName) +} + +func updateCurrentPaymodelInDB(userName string, workspaceid string, svc *dynamodb.DynamoDB) error { + // Reset current_pay_model for all paymodels first + err := resetCurrentPaymodel(userName, svc) + if err != nil { + return err + } + // Set paymodel with id=workspaceid to current + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: map[string]*string{ + "#CPM": aws.String("current_pay_model"), + }, + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":f": { + BOOL: aws.Bool(true), + }, + }, + Key: map[string]*dynamodb.AttributeValue{ + "user_id": { + S: aws.String(userName), + }, + "bmh_workspace_id": { + S: aws.String(workspaceid), + }, + }, + ReturnValues: aws.String("ALL_NEW"), + TableName: aws.String(Config.Config.PayModelsDynamodbTable), + UpdateExpression: aws.String("SET #CPM = :f"), + } + _, err = svc.UpdateItem(input) + if err != nil { + return err + } + return nil +} + +func resetCurrentPaymodel(userName string, svc *dynamodb.DynamoDB) error { + pm_db, err := payModelsFromDatabase(userName, false) + if err != nil { + return err + } + for _, pm := range *pm_db { + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: map[string]*string{ + "#CPM": aws.String("current_pay_model"), + }, + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":f": { + BOOL: aws.Bool(false), + }, + }, + Key: map[string]*dynamodb.AttributeValue{ + "user_id": { + S: aws.String(userName), + }, + "bmh_workspace_id": { + S: aws.String(pm.Id), + }, + }, + ReturnValues: aws.String("ALL_NEW"), + TableName: aws.String(Config.Config.PayModelsDynamodbTable), + UpdateExpression: aws.String("SET #CPM = :f"), + } + _, err := svc.UpdateItem(input) + if err != nil { + return err + } + } + return nil +} diff --git a/hatchery/pods.go b/hatchery/pods.go index 337bafdf..05e1d213 100644 --- a/hatchery/pods.go +++ b/hatchery/pods.go @@ -21,9 +21,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" - "github.com/aws/aws-sdk-go/service/dynamodb/expression" "github.com/aws/aws-sdk-go/service/eks" "sigs.k8s.io/aws-iam-authenticator/pkg/token" @@ -624,102 +621,6 @@ func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName str return pod, nil } -func getPayModelForUser(userName string) (result *PayModel, err error) { - if userName == "" { - return nil, fmt.Errorf("No username sent in header") - } - if Config.Config.PayModelsDynamodbTable == "" { - // fallback for backward compatibility - payModel := Config.Config.DefaultPayModel - for _, configPaymodel := range Config.PayModelMap { - if configPaymodel.User == userName { - payModel = configPaymodel - } - } - if (PayModel{} != payModel) { - return &payModel, nil - } - return nil, fmt.Errorf("No pay model data for username '%s'.", userName) - } - // query pay model data for this user from DynamoDB - sess := session.Must(session.NewSessionWithOptions(session.Options{ - Config: aws.Config{ - Region: aws.String("us-east-1"), - }, - })) - dynamodbSvc := dynamodb.New(sess) - - filt := expression.Name("user_id").Equal(expression.Value(userName)) - expr, err := expression.NewBuilder().WithFilter(filt).Build() - if err != nil { - Config.Logger.Printf("Got error building expression: %s", err) - return nil, err - } - - params := &dynamodb.ScanInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - FilterExpression: expr.Filter(), - TableName: aws.String(Config.Config.PayModelsDynamodbTable), - } - res, err := dynamodbSvc.Scan(params) - if err != nil { - Config.Logger.Printf("Query API call failed: %s", err) - return nil, err - } - - if len(res.Items) == 0 { - // temporary fallback to the config to get data for users that are not - // in DynamoDB - // TODO: remove this block once we only rely on DynamoDB - Config.Logger.Printf("No pay model data for username '%s' in DynamoDB. Fallback on config.", userName) - payModel := Config.Config.DefaultPayModel - for _, configPaymodel := range Config.PayModelMap { - if configPaymodel.User == userName { - payModel = configPaymodel - } - } - if (PayModel{} != payModel) { - return &payModel, nil - } - return nil, fmt.Errorf("No pay model data for username '%s'.", userName) - } - - if len(res.Items) > 1 { - Config.Logger.Printf("There is more than one pay model item in DynamoDB for username '%s'. Defaulting to the first one.", userName) - } - - // parse pay model data - payModel := PayModel{} - err = dynamodbattribute.UnmarshalMap(res.Items[0], &payModel) - if err != nil { - Config.Logger.Printf("Got error unmarshalling: %s", err) - return nil, err - } - - // temporary fallback to the config to get data that is not in DynamoDB - // TODO: remove this block once DynamoDB contains all necessary data - for _, configPaymodel := range Config.PayModelMap { - if configPaymodel.User == userName { - if payModel.Name == "" { - payModel.Name = configPaymodel.Name - } - if payModel.AWSAccountId == "" { - payModel.AWSAccountId = configPaymodel.AWSAccountId - } - if payModel.Region == "" { - payModel.Region = configPaymodel.Region - } - if payModel.Ecs == "" { - payModel.Ecs = configPaymodel.Ecs - } - break - } - } - - return &payModel, nil -} - func createLocalK8sPod(ctx context.Context, hash string, userName string, accessToken string) error { hatchApp := Config.ContainersMap[hash] From 4db579d114cfa66cdc09d220b1a205703a224f75 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Wed, 23 Feb 2022 11:50:46 -0600 Subject: [PATCH 02/22] Update --- hatchery/paymodels.go | 1 + 1 file changed, 1 insertion(+) diff --git a/hatchery/paymodels.go b/hatchery/paymodels.go index 00a44f8e..9c426cbf 100644 --- a/hatchery/paymodels.go +++ b/hatchery/paymodels.go @@ -138,6 +138,7 @@ func getPayModelsForUser(userName string) (result *AllPayModels, err error) { return nil, NopaymodelsError } PayModels.CurrentPayModel = pm + PayModels.PayModels = append(PayModels.PayModels, *pm) return &PayModels, nil } From ece59ecebd4d888d406ce2921c4f46c9111eefa5 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Thu, 24 Feb 2022 10:01:22 -0600 Subject: [PATCH 03/22] Updates --- hatchery/paymodels.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hatchery/paymodels.go b/hatchery/paymodels.go index 9c426cbf..f1ef780c 100644 --- a/hatchery/paymodels.go +++ b/hatchery/paymodels.go @@ -155,6 +155,11 @@ func getPayModelsForUser(userName string) (result *AllPayModels, err error) { *payModelMap = append(*payModelMap, *payModel) } + if len(*payModelMap) == 0 { + payModel, _ := getDefaultPayModel() + *payModelMap = append(*payModelMap, *payModel) + } + PayModels.PayModels = *payModelMap payModel, err = getCurrentPayModel(userName) From 16694414545a3ac036e47bf59cbed9da182e8a94 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Thu, 24 Feb 2022 12:49:10 -0600 Subject: [PATCH 04/22] Updates --- hatchery/ecs.go | 11 +++++------ hatchery/hatchery.go | 3 +-- hatchery/iam.go | 10 +++++++--- hatchery/transitgateway.go | 15 ++++++++++----- hatchery/vpc.go | 7 +++++-- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/hatchery/ecs.go b/hatchery/ecs.go index 5a748d42..13604b2e 100644 --- a/hatchery/ecs.go +++ b/hatchery/ecs.go @@ -339,8 +339,6 @@ func terminateEcsWorkspace(ctx context.Context, userName string, accessToken str } func launchEcsWorkspace(ctx context.Context, userName string, hash string, accessToken string, payModel PayModel) error { - // TODO: Setup EBS volume as pd - // Must create volume using SDK too.. :( roleARN := "arn:aws:iam::" + payModel.AWSAccountId + ":role/csoc_adminvm" sess := session.Must(session.NewSession(&aws.Config{ // TODO: Make this configurable @@ -486,10 +484,6 @@ func launchEcsWorkspace(ctx context.Context, userName string, hash string, acces } return err } - err = setupTransitGateway(userName) - if err != nil { - return err - } launchTask, err := svc.launchService(ctx, taskDefResult, userName, hash, payModel) if err != nil { @@ -500,6 +494,11 @@ func launchEcsWorkspace(ctx context.Context, userName string, hash string, acces return err } + err = setupTransitGateway(userName) + if err != nil { + return err + } + fmt.Printf("Launched ECS workspace service at %s for user %s\n", launchTask, userName) return nil } diff --git a/hatchery/hatchery.go b/hatchery/hatchery.go index fc8f191d..13213e91 100644 --- a/hatchery/hatchery.go +++ b/hatchery/hatchery.go @@ -142,7 +142,6 @@ func status(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - } else { result, err = statusK8sPod(r.Context(), userName, accessToken, payModel) if err != nil { @@ -226,7 +225,7 @@ func launch(w http.ResponseWriter, r *http.Request) { err = createExternalK8sPod(r.Context(), hash, userName, accessToken, *payModel) } if err != nil { - Config.Logger.Printf("error during launch: %s", err) + Config.Logger.Printf("error during launch: %-v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/hatchery/iam.go b/hatchery/iam.go index 218d7208..d340fd02 100644 --- a/hatchery/iam.go +++ b/hatchery/iam.go @@ -14,7 +14,10 @@ func (creds *CREDS) taskRole(userName string) (*string, error) { Credentials: creds.creds, Region: aws.String("us-east-1"), }))) - pm := Config.PayModelMap[userName] + pm, err := getCurrentPayModel(userName) + if err != nil { + return nil, err + } policyArn := fmt.Sprintf("arn:aws:iam::%s:policy/%s", pm.AWSAccountId, fmt.Sprintf("ws-task-policy-%s", userName)) taskRoleInput := &iam.GetRoleInput{ RoleName: aws.String(userToResourceName(userName, "pod")), @@ -96,8 +99,9 @@ func (creds *CREDS) taskRole(userName string) (*string, error) { } } -// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html -// The task execution role grants the Amazon ECS container and Fargate agents permission to make AWS API calls on your behalf. + +// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +// The task execution role grants the Amazon ECS container and Fargate agents permission to make AWS API calls on your behalf. const ecsTaskExecutionRoleName = "ecsTaskExecutionRole" const ecsTaskExecutionPolicyArn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" const ecsTaskExecutionRoleAssumeRolePolicyDocument = `{ diff --git a/hatchery/transitgateway.go b/hatchery/transitgateway.go index a957dcad..8e72b79d 100644 --- a/hatchery/transitgateway.go +++ b/hatchery/transitgateway.go @@ -98,7 +98,10 @@ func describeMainNetwork(vpcid string, svc *ec2.EC2) (*NetworkInfo, error) { } func createTransitGateway(userName string) (*string, error) { - pm := Config.PayModelMap[userName] + pm, err := getCurrentPayModel(userName) + if err != nil { + return nil, err + } sess := session.Must(session.NewSession(&aws.Config{ // TODO: Make this configurable Region: aws.String("us-east-1"), @@ -380,7 +383,10 @@ func shareTransitGateway(session *session.Session, tgwArn string, accountid stri } func setupRemoteAccount(userName string, teardown bool) error { - pm := Config.PayModelMap[userName] + pm, err := getCurrentPayModel(userName) + if err != nil { + return err + } roleARN := "arn:aws:iam::" + pm.AWSAccountId + ":role/csoc_adminvm" sess := session.Must(session.NewSession(&aws.Config{ // TODO: Make this configurable @@ -396,7 +402,7 @@ func setupRemoteAccount(userName string, teardown bool) error { vpcid := os.Getenv("GEN3_VPCID") Config.Logger.Printf("VPCID: %s", vpcid) - err := svc.acceptTGWShare() + err = svc.acceptTGWShare() if err != nil { return err } @@ -448,7 +454,7 @@ func setupRemoteAccount(userName string, teardown bool) error { } else { tgw_attachment, err = createTransitGatewayAttachments(ec2Remote, *vpc.Vpcs[0].VpcId, *exTg.TransitGateways[0].TransitGatewayId, false, &svc, userName) if err != nil { - return fmt.Errorf("Cannot create TransitGatewayAttachment: %s", err.Error()) + return fmt.Errorf("Cannot create remote TransitGatewayAttachment: %s", err.Error()) } Config.Logger.Printf("tgw_attachment: %s", *tgw_attachment) } @@ -562,7 +568,6 @@ func TGWRoutes(userName string, tgwRoutetableId *string, tgwAttachmentId *string return nil, err } } - tgRouteInput := &ec2.CreateTransitGatewayRouteInput{ TransitGatewayRouteTableId: tgwRoutetableId, DestinationCidrBlock: networkInfo.vpc.Vpcs[0].CidrBlock, diff --git a/hatchery/vpc.go b/hatchery/vpc.go index 4f703ff9..5143e713 100644 --- a/hatchery/vpc.go +++ b/hatchery/vpc.go @@ -12,7 +12,10 @@ import ( ) func setupVPC(userName string) (*string, error) { - pm := Config.PayModelMap[userName] + pm, err := getCurrentPayModel(userName) + if err != nil { + return nil, err + } roleARN := "arn:aws:iam::" + pm.AWSAccountId + ":role/csoc_adminvm" sess := session.Must(session.NewSession(&aws.Config{ @@ -31,7 +34,7 @@ func setupVPC(userName string) (*string, error) { // TODO: make base CIDR configurable? cidrstring := "192.165.0.0/12" _, IPNet, _ := net.ParseCIDR(cidrstring) - subnet, err := cidr.Subnet(IPNet, 15, pm.Subnet) + subnet, err := cidr.Subnet(IPNet, 14, pm.Subnet) if err != nil { return nil, err } From 7d10744c8ad9cfe99f4319b86783f654744eaa0a Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Thu, 3 Mar 2022 09:00:20 -0600 Subject: [PATCH 05/22] Updates --- hatchery/paymodels.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hatchery/paymodels.go b/hatchery/paymodels.go index f1ef780c..6330fad6 100644 --- a/hatchery/paymodels.go +++ b/hatchery/paymodels.go @@ -185,7 +185,9 @@ func setCurrentPaymodel(userName string, workspaceid string) (paymodel *PayModel } pm_config, err := payModelFromConfig(userName) if err != nil { - return nil, err + if err != NopaymodelsError { + return nil, err + } } if pm_config.Id == workspaceid { resetCurrentPaymodel(userName, dynamodbSvc) From 6f154f398abfea95404e7206194ed944a4abf1fc Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Thu, 3 Mar 2022 09:46:48 -0600 Subject: [PATCH 06/22] Updates --- hatchery/paymodels.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hatchery/paymodels.go b/hatchery/paymodels.go index 6330fad6..ffb76524 100644 --- a/hatchery/paymodels.go +++ b/hatchery/paymodels.go @@ -189,9 +189,11 @@ func setCurrentPaymodel(userName string, workspaceid string) (paymodel *PayModel return nil, err } } - if pm_config.Id == workspaceid { - resetCurrentPaymodel(userName, dynamodbSvc) - return pm_config, nil + if pm_config != nil { + if pm_config.Id == workspaceid { + resetCurrentPaymodel(userName, dynamodbSvc) + return pm_config, nil + } } for _, pm := range *pm_db { if pm.Id == workspaceid { From 084b32de3a4293742964c55f3573dd3e6a623d10 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Thu, 3 Mar 2022 15:10:10 -0600 Subject: [PATCH 07/22] Updates --- hatchery/config.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/hatchery/config.go b/hatchery/config.go index 43966ed8..ba299785 100644 --- a/hatchery/config.go +++ b/hatchery/config.go @@ -56,17 +56,17 @@ type AppConfigInfo struct { // TODO remove PayModel from config once DynamoDB contains all necessary data type PayModel struct { - Id string `json:"bmh_workspace_id"` - Name string `json:"workspace_type"` - User string `json:"user_id"` - AWSAccountId string `json:"account_id"` - Region string `json:"region"` - Ecs string `json:"ecs"` - Subnet int `json:"subnet"` - HardLimit int `json:"hard-limit"` - SoftLimit int `json:"soft-limit"` - TotalUsage int `json:"total-usage"` - CurrentPayModel bool `json:"current_pay_model"` + Id string `json:"bmh_workspace_id"` + Name string `json:"workspace_type"` + User string `json:"user_id"` + AWSAccountId string `json:"account_id"` + Region string `json:"region"` + Ecs string `json:"ecs"` + Subnet int `json:"subnet"` + HardLimit float32 `json:"hard-limit"` + SoftLimit float32 `json:"soft-limit"` + TotalUsage float32 `json:"total-usage"` + CurrentPayModel bool `json:"current_pay_model"` } type AllPayModels struct { From 8146dcf1c25a230f120524f1bf242a357bcadf7e Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Mon, 7 Mar 2022 12:30:33 -0600 Subject: [PATCH 08/22] fix: go lint --- hatchery/hatchery.go | 2 +- hatchery/paymodels.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/hatchery/hatchery.go b/hatchery/hatchery.go index 13213e91..3f1b12f6 100644 --- a/hatchery/hatchery.go +++ b/hatchery/hatchery.go @@ -280,7 +280,7 @@ func getBearerToken(r *http.Request) string { func createECSCluster(w http.ResponseWriter, r *http.Request) { userName := getCurrentUserName(r) payModel, err := getCurrentPayModel(userName) - if &payModel == nil { + if payModel == nil { http.Error(w, "Paymodel has not been setup for user", http.StatusNotFound) return } diff --git a/hatchery/paymodels.go b/hatchery/paymodels.go index ffb76524..7b29c86f 100644 --- a/hatchery/paymodels.go +++ b/hatchery/paymodels.go @@ -191,13 +191,19 @@ func setCurrentPaymodel(userName string, workspaceid string) (paymodel *PayModel } if pm_config != nil { if pm_config.Id == workspaceid { - resetCurrentPaymodel(userName, dynamodbSvc) + err := resetCurrentPaymodel(userName, dynamodbSvc) + if err != nil { + return nil, err + } return pm_config, nil } } for _, pm := range *pm_db { if pm.Id == workspaceid { - updateCurrentPaymodelInDB(userName, workspaceid, dynamodbSvc) + err := updateCurrentPaymodelInDB(userName, workspaceid, dynamodbSvc) + if err != nil { + return nil, err + } return &pm, nil } } From 00cd77d6ffbac451f7454cbb3ee54606e83a1796 Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Tue, 8 Mar 2022 10:03:39 -0600 Subject: [PATCH 09/22] use post to set --- hatchery/hatchery.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatchery/hatchery.go b/hatchery/hatchery.go index 3f1b12f6..c202fb6d 100644 --- a/hatchery/hatchery.go +++ b/hatchery/hatchery.go @@ -100,7 +100,7 @@ func allpaymodels(w http.ResponseWriter, r *http.Request) { } func setpaymodel(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { + if r.Method != "POST" { http.Error(w, "Not Found", http.StatusNotFound) return } From 063ee6cc5822c5192f0c30256e9b0486e637820f Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Tue, 8 Mar 2022 11:30:05 -0600 Subject: [PATCH 10/22] update doc --- openapis/openapi.yaml | 96 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/openapis/openapi.yaml b/openapis/openapi.yaml index 24078b2a..92fcb9b9 100644 --- a/openapis/openapi.yaml +++ b/openapis/openapi.yaml @@ -18,7 +18,7 @@ paths: post: tags: - workspace - summary: LaunchAWorkspace + summary: Launch a workspace operationId: launch parameters: - in: query @@ -69,7 +69,6 @@ paths: content: application/json: schema: - type: array items: $ref: '#/components/schemas/Container' 401: @@ -80,6 +79,46 @@ paths: - workspace summary: Get the current user's pay model data operationId: paymodels + responses: + 200: + description: successful operation + content: + application/json: + schema: + items: + $ref: '#/components/schemas/PayModel' + 401: + $ref: '#/components/responses/UnauthorizedError' + 404: + $ref: '#/components/responses/NotFoundError' + 500: + $ref: '#/components/responses/InternalServerError' + /allpaymodels: + get: + tags: + - workspace + summary: Get the current user's all pay model data, including the currently activated one + operationId: paymodels + responses: + 200: + description: successful operation + content: + application/json: + schema: + items: + $ref: '#/components/schemas/PayModel' + 401: + $ref: '#/components/responses/UnauthorizedError' + 404: + $ref: '#/components/responses/NotFoundError' + 500: + $ref: '#/components/responses/InternalServerError' + /setpaymodel: + POST: + tags: + - workspace + summary: Get the current user's al pay model data + operationId: paymodels components: schemas: @@ -141,6 +180,59 @@ components: state: type: object description: Details about the container's current condition + PayModel: + type: object + properties: + bmh_workspace_id: + type: string + description: Unique ID of this pay model + workspace_type: + type: string + description: Type of this pay model, can be used as the name of pay model + user_id: + type: string + description: The ID of user + account_id: + type: string + description: The ID of the provisioned AWS account for this pay model + region: + type: string + description: The region of the provisioned AWS account for this pay model + ecs: + type: string + description: Whether to launch workspace using AWS ECS for this pay model + subnet: + type: string + description: The subnet identifier + hard-limit: + type: string + description: The hard limit set for this AWS account + soft-limit: + type: string + description: The soft limit set for this AWS account + total-usage: + type: string + description: The total occurred usage so far for this AWS account + current_pay_model: + type: boolean + description: Is this pay model activated as current pay model + AllPayModels: + type: object + properties: + current_pay_model: + type: object + schema: + $ref: '#/components/schemas/PayModel' + description: The currently activated pay model associated with this user + all_pay_models: + type: array + items: + $ref: '#/components/schemas/PayModel' + description: All pay models associated with this user, including the currently activated one responses: UnauthorizedError: description: Access token is missing or invalid + NotFoundError: + description: Can't find pay model information for user + InternalServerError: + description: Can't process user's request From e249883cc250febf1cc20306ab5f0e0d62e3d512 Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Tue, 8 Mar 2022 11:41:58 -0600 Subject: [PATCH 11/22] update doc --- openapis/openapi.yaml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/openapis/openapi.yaml b/openapis/openapi.yaml index 92fcb9b9..559bcd01 100644 --- a/openapis/openapi.yaml +++ b/openapis/openapi.yaml @@ -98,7 +98,7 @@ paths: tags: - workspace summary: Get the current user's all pay model data, including the currently activated one - operationId: paymodels + operationId: allpaymodels responses: 200: description: successful operation @@ -117,8 +117,25 @@ paths: POST: tags: - workspace - summary: Get the current user's al pay model data - operationId: paymodels + summary: Set the currently activated paymodel for user + operationId: setpaymodel + parameters: + - in: query + name: id + schema: + type: string + description: The unique ID of the pay model + responses: + 200: + description: successfully set current pay model + 400: + $ref: '#/components/responses/BadRequestError' + 401: + $ref: '#/components/responses/UnauthorizedError' + 404: + $ref: '#/components/responses/NotFoundError' + 500: + $ref: '#/components/responses/InternalServerError' components: schemas: @@ -230,6 +247,8 @@ components: $ref: '#/components/schemas/PayModel' description: All pay models associated with this user, including the currently activated one responses: + BadRequestError: + description: Missing required information in request UnauthorizedError: description: Access token is missing or invalid NotFoundError: From e732a7a9ab73cecfd73d8e15acb6c58423064ce0 Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Tue, 8 Mar 2022 11:48:39 -0600 Subject: [PATCH 12/22] doc --- openapis/openapi.yaml | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/openapis/openapi.yaml b/openapis/openapi.yaml index 559bcd01..1549f54c 100644 --- a/openapis/openapi.yaml +++ b/openapis/openapi.yaml @@ -85,8 +85,7 @@ paths: content: application/json: schema: - items: - $ref: '#/components/schemas/PayModel' + $ref: '#/components/schemas/PayModel' 401: $ref: '#/components/responses/UnauthorizedError' 404: @@ -105,8 +104,7 @@ paths: content: application/json: schema: - items: - $ref: '#/components/schemas/PayModel' + $ref: '#/components/schemas/AllPayModels' 401: $ref: '#/components/responses/UnauthorizedError' 404: @@ -114,7 +112,7 @@ paths: 500: $ref: '#/components/responses/InternalServerError' /setpaymodel: - POST: + post: tags: - workspace summary: Set the currently activated paymodel for user @@ -183,63 +181,62 @@ components: type: type: string enum: [PodScheduled, Initialized, ContainersReady, Ready] - description: Name of this Pod condition + description: Name of this Pod condition status: type: string enum: ["True", "False", Unknown] - description: Indicates whether that condition is applicable + description: Indicates whether that condition is applicable ContainerState: type: object properties: name: type: string - description: Name of this container + description: Name of this container state: type: object - description: Details about the container's current condition + description: Details about the container's current condition PayModel: type: object properties: bmh_workspace_id: type: string - description: Unique ID of this pay model + description: Unique ID of this pay model workspace_type: type: string - description: Type of this pay model, can be used as the name of pay model + description: Type of this pay model, can be used as the name of pay model user_id: type: string - description: The ID of user + description: The ID of user account_id: type: string - description: The ID of the provisioned AWS account for this pay model + description: The ID of the provisioned AWS account for this pay model region: type: string - description: The region of the provisioned AWS account for this pay model + description: The region of the provisioned AWS account for this pay model ecs: type: string - description: Whether to launch workspace using AWS ECS for this pay model + description: Whether to launch workspace using AWS ECS for this pay model subnet: type: string - description: The subnet identifier + description: The subnet identifier hard-limit: type: string - description: The hard limit set for this AWS account + description: The hard limit set for this AWS account soft-limit: type: string - description: The soft limit set for this AWS account + description: The soft limit set for this AWS account total-usage: type: string - description: The total occurred usage so far for this AWS account + description: The total occurred usage so far for this AWS account current_pay_model: type: boolean - description: Is this pay model activated as current pay model + description: Is this pay model activated as current pay model AllPayModels: type: object properties: current_pay_model: type: object - schema: - $ref: '#/components/schemas/PayModel' + $ref: '#/components/schemas/PayModel' description: The currently activated pay model associated with this user all_pay_models: type: array From 97d977868298184c7dbe9928ac8e0190873e68c8 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Wed, 9 Mar 2022 14:43:38 -0600 Subject: [PATCH 13/22] change Ecs to bool --- hatchery/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatchery/config.go b/hatchery/config.go index ba299785..760c7f13 100644 --- a/hatchery/config.go +++ b/hatchery/config.go @@ -61,7 +61,7 @@ type PayModel struct { User string `json:"user_id"` AWSAccountId string `json:"account_id"` Region string `json:"region"` - Ecs string `json:"ecs"` + Ecs bool `json:"ecs"` Subnet int `json:"subnet"` HardLimit float32 `json:"hard-limit"` SoftLimit float32 `json:"soft-limit"` From 2c89e28491ee9c2a95d877c76e7e5b67e64117fb Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Wed, 9 Mar 2022 15:16:26 -0600 Subject: [PATCH 14/22] change Ecs to bool --- hatchery/hatchery.go | 6 +++--- hatchery/pods.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hatchery/hatchery.go b/hatchery/hatchery.go index c202fb6d..007eb950 100644 --- a/hatchery/hatchery.go +++ b/hatchery/hatchery.go @@ -136,7 +136,7 @@ func status(w http.ResponseWriter, r *http.Request) { } var result *WorkspaceStatus - if payModel.Ecs == "true" { + if payModel.Ecs { result, err = statusEcs(r.Context(), userName, accessToken, payModel.AWSAccountId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -219,7 +219,7 @@ func launch(w http.ResponseWriter, r *http.Request) { } if payModel == nil { err = createLocalK8sPod(r.Context(), hash, userName, accessToken) - } else if payModel.Ecs == "true" { + } else if payModel.Ecs { err = launchEcsWorkspace(r.Context(), userName, hash, accessToken, *payModel) } else { err = createExternalK8sPod(r.Context(), hash, userName, accessToken, *payModel) @@ -243,7 +243,7 @@ func terminate(w http.ResponseWriter, r *http.Request) { if err != nil { Config.Logger.Printf(err.Error()) } - if payModel != nil && payModel.Ecs == "true" { + if payModel != nil && payModel.Ecs { svc, err := terminateEcsWorkspace(r.Context(), userName, accessToken, payModel.AWSAccountId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/hatchery/pods.go b/hatchery/pods.go index 05e1d213..184e548f 100644 --- a/hatchery/pods.go +++ b/hatchery/pods.go @@ -911,7 +911,7 @@ tls: %s serviceName := userToResourceName(userName, "service") NodePort := int32(80) - if payModel.Ecs != "true" { + if payModel.Ecs != true { externalPodClient, err := NewEKSClientset(ctx, userName, payModel) if err != nil { return err From 87797210d8d8e799e112d11659a0858ab30aea18 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Wed, 9 Mar 2022 15:18:12 -0600 Subject: [PATCH 15/22] change Ecs to bool --- hatchery/pods.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatchery/pods.go b/hatchery/pods.go index 184e548f..39f5e858 100644 --- a/hatchery/pods.go +++ b/hatchery/pods.go @@ -911,7 +911,7 @@ tls: %s serviceName := userToResourceName(userName, "service") NodePort := int32(80) - if payModel.Ecs != true { + if !payModel.Ecs { externalPodClient, err := NewEKSClientset(ctx, userName, payModel) if err != nil { return err From f4e2869a40c63a304e19b1d6b8e2cb26d1c7ddea Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Fri, 8 Apr 2022 14:16:56 -0500 Subject: [PATCH 16/22] Change order of launching --- hatchery/ecs.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/hatchery/ecs.go b/hatchery/ecs.go index 13604b2e..7f5df683 100644 --- a/hatchery/ecs.go +++ b/hatchery/ecs.go @@ -54,7 +54,6 @@ func (input *CreateTaskDefinitionInput) Environment() []*ecs.KeyValuePair { } // Create ECS cluster -// TODO: Evaluate if this is still this needed.. func (sess *CREDS) launchEcsCluster(userName string) (*ecs.Cluster, error) { svc := sess.svc clusterName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-cluster" @@ -485,6 +484,11 @@ func launchEcsWorkspace(ctx context.Context, userName string, hash string, acces return err } + err = setupTransitGateway(userName) + if err != nil { + return err + } + launchTask, err := svc.launchService(ctx, taskDefResult, userName, hash, payModel) if err != nil { aerr := deleteAPIKeyWithContext(ctx, accessToken, apiKey.KeyID) @@ -493,12 +497,6 @@ func launchEcsWorkspace(ctx context.Context, userName string, hash string, acces } return err } - - err = setupTransitGateway(userName) - if err != nil { - return err - } - fmt.Printf("Launched ECS workspace service at %s for user %s\n", launchTask, userName) return nil } From db0ca13fc6988c81657eb9a3e2539114dafd1807 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Fri, 15 Apr 2022 14:07:56 -0500 Subject: [PATCH 17/22] Updates --- hatchery/alb.go | 26 ++++++++++++++++++++++++++ hatchery/ec2.go | 1 - hatchery/ecs.go | 8 +++++++- hatchery/transitgateway.go | 14 ++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/hatchery/alb.go b/hatchery/alb.go index 90aa2afa..a0d88d72 100644 --- a/hatchery/alb.go +++ b/hatchery/alb.go @@ -216,3 +216,29 @@ func (creds *CREDS) CreateLoadBalancer(userName string) (*elbv2.CreateLoadBalanc } return loadBalancer, targetGroup.TargetGroups[0].TargetGroupArn, listener, nil } + +func (creds *CREDS) terminateLoadBalancer(userName string) error { + svc := elbv2.New(session.Must(session.NewSession(&aws.Config{ + Credentials: creds.creds, + Region: aws.String("us-east-1"), + }))) + albName := truncateString(strings.ReplaceAll(userToResourceName(userName, "service")+os.Getenv("GEN3_ENDPOINT"), ".", "-")+"alb", 32) + + getInput := &elbv2.DescribeLoadBalancersInput{ + Names: []*string{aws.String(albName)}, + } + result, err := svc.DescribeLoadBalancers(getInput) + if err != nil { + return err + } + if len(result.LoadBalancers) == 1 { + delInput := &elbv2.DeleteLoadBalancerInput{ + LoadBalancerArn: result.LoadBalancers[0].LoadBalancerArn, + } + _, err := svc.DeleteLoadBalancer(delInput) + if err != nil { + return err + } + } + return nil +} diff --git a/hatchery/ec2.go b/hatchery/ec2.go index e8a86628..e5308d6e 100644 --- a/hatchery/ec2.go +++ b/hatchery/ec2.go @@ -110,7 +110,6 @@ func (creds *CREDS) describeWorkspaceNetwork(userName string) (*NetworkInfo, err } Config.Logger.Printf("Create Security Group: %s", *newSecurityGroup.GroupId) - // TODO: Make this secure. Right now it's wide open ingressRules := ec2.AuthorizeSecurityGroupIngressInput{ GroupId: newSecurityGroup.GroupId, IpPermissions: []*ec2.IpPermission{ diff --git a/hatchery/ecs.go b/hatchery/ecs.go index 7f5df683..4246b3a5 100644 --- a/hatchery/ecs.go +++ b/hatchery/ecs.go @@ -329,7 +329,13 @@ func terminateEcsWorkspace(ctx context.Context, userName string, accessToken str if err != nil { return "", err } - // TODO: Terminate ALB + target group here too + + // Terminate load balancer + err = svc.terminateLoadBalancer(userName) + if err != nil { + return "", err + } + err = teardownTransitGateway(userName) if err != nil { return "", err diff --git a/hatchery/transitgateway.go b/hatchery/transitgateway.go index 8e72b79d..737ad3ac 100644 --- a/hatchery/transitgateway.go +++ b/hatchery/transitgateway.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ram" @@ -204,6 +205,19 @@ func createTransitGatewayAttachments(svc *ec2.EC2, vpcid string, tgwid string, l } exTg, err := svc.DescribeTransitGateways(tgInput) if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case "InvalidTransitGatewayID.NotFound": + // Accept any pending invites again + sess.acceptTGWShare() + exTg, err = svc.DescribeTransitGateways(tgInput) + if err != nil { + return nil, fmt.Errorf("Cannot DescribeTransitGateways again: %s", err.Error()) + } + default: + return nil, fmt.Errorf("Cannot DescribeTransitGateways: %s", err.Error()) + } + } return nil, err } for *exTg.TransitGateways[0].State != "available" { From f68b9e4aff3d74171ca40749a7b463a6912f903d Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Wed, 4 May 2022 14:12:08 -0500 Subject: [PATCH 18/22] Updates --- hatchery/ecs.go | 2 +- hatchery/hatchery.go | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/hatchery/ecs.go b/hatchery/ecs.go index 4246b3a5..d53c1d65 100644 --- a/hatchery/ecs.go +++ b/hatchery/ecs.go @@ -200,7 +200,7 @@ func (sess *CREDS) statusEcsWorkspace(ctx context.Context, userName string, acce if err != nil { return &status, err } - + // TODO: Check TransitGatewayAttachment is not in Deleting state (Can't create new one until it's deleted). var taskDefName string if len(service.Services) > 0 { statusMessage = *service.Services[0].Status diff --git a/hatchery/hatchery.go b/hatchery/hatchery.go index 007eb950..3a84ea06 100644 --- a/hatchery/hatchery.go +++ b/hatchery/hatchery.go @@ -136,17 +136,25 @@ func status(w http.ResponseWriter, r *http.Request) { } var result *WorkspaceStatus - if payModel.Ecs { - result, err = statusEcs(r.Context(), userName, accessToken, payModel.AWSAccountId) + if payModel == nil { + result, err = statusK8sPod(r.Context(), userName, accessToken, payModel) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } else { - result, err = statusK8sPod(r.Context(), userName, accessToken, payModel) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + if payModel.Ecs { + result, err = statusEcs(r.Context(), userName, accessToken, payModel.AWSAccountId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + result, err = statusK8sPod(r.Context(), userName, accessToken, payModel) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } } From 3bc608892ae5ad5439f600cc15735d3274624e50 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Wed, 4 May 2022 14:39:30 -0500 Subject: [PATCH 19/22] Add API key to local pods --- hatchery/pods.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/hatchery/pods.go b/hatchery/pods.go index 39f5e858..1bac70f8 100644 --- a/hatchery/pods.go +++ b/hatchery/pods.go @@ -625,6 +625,22 @@ func createLocalK8sPod(ctx context.Context, hash string, userName string, access hatchApp := Config.ContainersMap[hash] var extraVars []k8sv1.EnvVar + apiKey, err := getAPIKeyWithContext(ctx, accessToken) + if err != nil { + Config.Logger.Printf("Failed to get API key for user %v, Error: %v", userName, err) + return err + } + Config.Logger.Printf("Created API key for user %v, key ID: %v", userName, apiKey.KeyID) + + extraVars = append(extraVars, k8sv1.EnvVar{ + Name: "API_KEY", + Value: apiKey.APIKey, + }) + extraVars = append(extraVars, k8sv1.EnvVar{ + Name: "API_KEY_ID", + Value: apiKey.KeyID, + }) + pod, err := buildPod(Config, &hatchApp, userName, extraVars) if err != nil { Config.Logger.Printf("Failed to configure pod for launch for user %v, Error: %v", userName, err) From 9f6a9b2741e9ca7f381ef3cef17e105f3c218f86 Mon Sep 17 00:00:00 2001 From: Mingfei Shao <2475897+mfshao@users.noreply.github.com> Date: Thu, 5 May 2022 09:15:27 -0500 Subject: [PATCH 20/22] Update golang-ci-workflow.yaml --- .github/workflows/golang-ci-workflow.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/golang-ci-workflow.yaml b/.github/workflows/golang-ci-workflow.yaml index e94f8592..251dde9e 100644 --- a/.github/workflows/golang-ci-workflow.yaml +++ b/.github/workflows/golang-ci-workflow.yaml @@ -4,6 +4,7 @@ on: push jobs: ci: + name: golang-ci runs-on: ubuntu-latest env: COVERAGE_PROFILE_OUTPUT_LOCATION: "./profile.cov" From d192d316d6d84bd5cc5bda472816a351bc0763d9 Mon Sep 17 00:00:00 2001 From: Jawad Qureshi Date: Thu, 5 May 2022 14:11:16 -0500 Subject: [PATCH 21/22] Fix linting issues --- hatchery/transitgateway.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/hatchery/transitgateway.go b/hatchery/transitgateway.go index 737ad3ac..6baf6139 100644 --- a/hatchery/transitgateway.go +++ b/hatchery/transitgateway.go @@ -209,13 +209,16 @@ func createTransitGatewayAttachments(svc *ec2.EC2, vpcid string, tgwid string, l switch aerr.Code() { case "InvalidTransitGatewayID.NotFound": // Accept any pending invites again - sess.acceptTGWShare() - exTg, err = svc.DescribeTransitGateways(tgInput) + err = sess.acceptTGWShare() if err != nil { - return nil, fmt.Errorf("Cannot DescribeTransitGateways again: %s", err.Error()) + return nil, err + } + _, err = svc.DescribeTransitGateways(tgInput) + if err != nil { + return nil, fmt.Errorf("cannot DescribeTransitGateways again: %s", err.Error()) } default: - return nil, fmt.Errorf("Cannot DescribeTransitGateways: %s", err.Error()) + return nil, fmt.Errorf("cannot DescribeTransitGateways: %s", err.Error()) } } return nil, err From 64f055f22b696db52f2d63b8b1ab4e17d2d16af8 Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Fri, 13 May 2022 13:57:36 -0500 Subject: [PATCH 22/22] dummy