From eb3d1fdb3d3192dfcbc78041f055d0248d8ab315 Mon Sep 17 00:00:00 2001 From: Susanth <110461367+boppanasusanth@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:33:39 +0530 Subject: [PATCH 1/3] environment operations (#217) --- api/environment/env.go | 36 ++++++++ app/app.go | 2 +- internal/backend/env.go | 24 +++++ internal/backend/operation.go | 9 ++ internal/command/command_catalog.go | 3 + internal/command/commands/env.go | 111 +++++++++++++++++++++++- internal/command/commands/operation.go | 105 +++++++++++++++------- internal/command/commands/service.go | 8 +- internal/command/commands/serviceset.go | 2 +- 9 files changed, 261 insertions(+), 39 deletions(-) diff --git a/api/environment/env.go b/api/environment/env.go index 6ce515af..e42e2113 100644 --- a/api/environment/env.go +++ b/api/environment/env.go @@ -102,3 +102,39 @@ type Status struct { type DetailResponse struct { Response Env `yaml:"resp,omitempty" json:"resp,omitempty"` } + +type Operation struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Data interface{} `yaml:"data,omitempty" json:"data,omitempty"` +} + +type OperationRequest struct { + Operations []Operation `yaml:"operations,omitempty" json:"operations,omitempty"` +} + +type OperationConsent struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + IsFeedbackRequired bool `yaml:"is_feedback_required,omitempty" json:"is_feedback_required,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` +} + +type OperationValidationResponseBody struct { + Operations []OperationConsent `yaml:"operations,omitempty" json:"operations,omitempty"` +} + +type OperationValidationResponse struct { + Response OperationValidationResponseBody `yaml:"resp,omitempty" json:"resp,omitempty"` +} + +type OperationOutput struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` +} + +type OperationResponseBody struct { + Operations []OperationOutput `yaml:"operations,omitempty" json:"operations,omitempty"` +} + +type OperationResponse struct { + Response OperationResponseBody `yaml:"resp,omitempty" json:"resp,omitempty"` +} diff --git a/app/app.go b/app/app.go index 263406f2..d4448e97 100644 --- a/app/app.go +++ b/app/app.go @@ -8,5 +8,5 @@ type application struct { // App (Application) interface var App application = application{ Name: "odin", - Version: "1.3.4", + Version: "1.4.0", } diff --git a/internal/backend/env.go b/internal/backend/env.go index 3a06fe48..b0669631 100644 --- a/internal/backend/env.go +++ b/internal/backend/env.go @@ -167,3 +167,27 @@ func (e *Env) EnvTypes() (envResp.EnvTypesResponse, error) { return envResponse, err } + +func (e *Env) ValidateOperation(envName string, data envResp.OperationRequest) (envResp.OperationValidationResponse, error) { + client := newApiClient() + + response := client.actionWithRetry(path.Join(envEntity, envName)+"/operate/validate/", "GET", data) + response.Process(true) + + var validateOperationResponse envResp.OperationValidationResponse + err := json.Unmarshal(response.Body, &validateOperationResponse) + + return validateOperationResponse, err +} + +func (e *Env) OperateService(envName string, data envResp.OperationRequest) (envResp.OperationResponse, error) { + client := newApiClient() + + response := client.actionWithRetry(path.Join(envEntity, envName)+"/operate/", "PUT", data) + response.Process(true) + + var operationResponse envResp.OperationResponse + err := json.Unmarshal(response.Body, &operationResponse) + + return operationResponse, err +} diff --git a/internal/backend/operation.go b/internal/backend/operation.go index 72d6504d..8bc8851b 100644 --- a/internal/backend/operation.go +++ b/internal/backend/operation.go @@ -26,3 +26,12 @@ func (o *Operation) ListServiceOperations() ([]operationapi.Operation, error) { err := json.Unmarshal(response.Body, &listResponse) return listResponse.Response, err } + +func (o *Operation) ListEnvOperations() ([]operationapi.Operation, error) { + client := newApiClient() + response := client.actionWithRetry(path.Join("envs", "operations", "all"), "GET", nil) + response.Process(true) // process response and exit if error + var listResponse operationapi.ListOperation + err := json.Unmarshal(response.Body, &listResponse) + return listResponse.Response, err +} diff --git a/internal/command/command_catalog.go b/internal/command/command_catalog.go index e0ececd7..3d3108ce 100644 --- a/internal/command/command_catalog.go +++ b/internal/command/command_catalog.go @@ -71,6 +71,9 @@ func CommandsCatalog() map[string]cli.CommandFactory { "set env": func() (cli.Command, error) { return &commands.Env{Set: true}, nil }, + "operate env": func() (cli.Command, error) { + return &commands.Env{Operate: true}, nil + }, "list env-type": func() (cli.Command, error) { return &commands.EnvType{List: true}, nil }, diff --git a/internal/command/commands/env.go b/internal/command/commands/env.go index 2f7ac2f0..8aab4caa 100644 --- a/internal/command/commands/env.go +++ b/internal/command/commands/env.go @@ -43,9 +43,11 @@ func (e *Env) Run(args []string) int { component := flagSet.String("component", "", "component name to filter out describe environment") providerAccount := flagSet.String("account", "", "account name to provision the environment in") id := flagSet.Int("id", 0, "unique id of a changelog of an env") - filePath := flagSet.String("file", "", "file to update env") + filePath := flagSet.String("file", "", "file to update env or provide options for environment operations") data := flagSet.String("data", "", "data for updating the env") displayAll := flagSet.Bool("all", false, "whether to display all environments") + operation := flagSet.String("operation", "", "name of the operation to performed on the environment") + options := flagSet.String("options", "", "options for environment operations") err := flagSet.Parse(args) if err != nil { @@ -380,6 +382,98 @@ func (e *Env) Run(args []string) int { return 0 } + if e.Operate { + if *name == "" { + *name = utils.FetchKey(ENV_NAME_KEY) + } + + isNamePresent := len(*name) > 0 + isOptionsPresent := len(*options) > 0 + isFilePresent := len(*filePath) > 0 + isOperationPresnt := len(*operation) > 0 + + if !isNamePresent { + e.Logger.Error("--name cannot be blank") + return 1 + } + if !isOperationPresnt { + e.Logger.Error("--operation cannot be blank") + return 1 + } + if isOptionsPresent && isFilePresent { + e.Logger.Error("You can provide either --options or --file but not both") + return 1 + } + + var optionsData map[string]interface{} + + if isFilePresent { + parsedConfig, err := parseFile(*filePath) + if err != nil { + e.Logger.Error("Error while parsing service file " + *filePath + " : " + err.Error()) + return 1 + } + optionsData = parsedConfig.(map[string]interface{}) + } else if isOptionsPresent { + err = json.Unmarshal([]byte(*options), &optionsData) + if err != nil { + e.Logger.Error("Unable to parse JSON data " + err.Error()) + return 1 + } + } + + data := environment.OperationRequest{ + Operations: []environment.Operation{ + { + Name: *operation, + Data: optionsData, + }, + }, + } + + e.Logger.Info("Validating the operation: " + *operation + " on the environment: " + *name) + + validateOperateResponse, err := envClient.ValidateOperation(*name, data) + if err != nil { + e.Logger.Error(err.Error()) + return 1 + } + + for _, operation := range validateOperateResponse.Response.Operations { + if operation.IsFeedbackRequired { + consentMessage := fmt.Sprintf("\n%s", operation.Message) + allowedInputs := map[string]struct{}{"Y": {}, "n": {}} + val, err := e.Input.AskWithConstraints(consentMessage, allowedInputs) + if err != nil { + e.Logger.Error(err.Error()) + return 1 + } + if val != "Y" { + e.Logger.Info("Aborting the operation") + return 1 + } + } else { + e.Logger.Info("Validations succeeded. Proceeding...") + } + } + + operateResponse, err := envClient.OperateService(*name, data) + if err != nil { + e.Logger.Error(err.Error()) + return 1 + } + + errored := false + for _, operation := range operateResponse.Response.Operations { + e.Logger.Output(operation.Message) + } + + if errored { + return 1 + } + return 0 + } + e.Logger.Error("Not a valid command") return 127 } @@ -464,6 +558,15 @@ func (e *Env) Help() string { }) } + if e.Operate { + return commandHelper("operate", "environment", "", []Options{ + {Flag: "--name", Description: "name of environment"}, + {Flag: "--operation", Description: "name of the operation to be performed on the environment"}, + {Flag: "--options", Description: "options for the operation in JSON format"}, + {Flag: "--file", Description: "path of the file which contains the options for the operation in JSON format"}, + }) + } + return defaultHelper() } @@ -496,9 +599,15 @@ func (e *Env) Synopsis() string { if e.Set { return "Set a default env" } + if e.Update { return "update an env" } + + if e.Operate { + return "operate an environment" + } + return defaultHelper() } diff --git a/internal/command/commands/operation.go b/internal/command/commands/operation.go index 83a57d40..c1f32011 100644 --- a/internal/command/commands/operation.go +++ b/internal/command/commands/operation.go @@ -19,6 +19,7 @@ func (o *Operation) Run(args []string) int { flagSet := flag.NewFlagSet("flagSet", flag.ContinueOnError) name := flagSet.String("name", "", "name of the operation") componentType := flagSet.String("component-type", "", "component-type on which operations will be performed") + entity := flagSet.String("entity", "", "name of the entity [env|service|component]") err := flagSet.Parse(args) if err != nil { @@ -26,16 +27,30 @@ func (o *Operation) Run(args []string) int { return 1 } + isComponentOperation := false + isEnvOperation := false + isServiceOperation := false + if o.List { - isComponentTypePresent := len(*componentType) > 0 + if o.parseFlags(entity, componentType, &isEnvOperation, &isComponentOperation, &isServiceOperation) == 1 { + return 1 + } var operationList []operationapi.Operation var err error - if isComponentTypePresent { + infoMsg := "Listing all " + *entity + " operations" + outputMsg := "\nCommand to describe " + *entity + " operations" + descibeCommandMsg := "odin describe operation --name --entity " + *entity + + if isComponentOperation { operationList, err = operationClient.ListComponentTypeOperations(*componentType) - } else { + infoMsg = "Listing all component operations on component " + *componentType + descibeCommandMsg = "odin describe operation --name --entity component --component-type " + } else if isServiceOperation { operationList, err = operationClient.ListServiceOperations() + } else if isEnvOperation { + operationList, err = operationClient.ListEnvOperations() } if err != nil { @@ -43,11 +58,7 @@ func (o *Operation) Run(args []string) int { return 1 } - if isComponentTypePresent { - o.Logger.Info("Listing all operation(s)" + " on component " + *componentType) - } else { - o.Logger.Info("Listing all service operations") - } + o.Logger.Info(infoMsg) tableHeaders := []string{"Name", "Descrption"} var tableData [][]interface{} @@ -60,33 +71,37 @@ func (o *Operation) Run(args []string) int { } table.Write(tableHeaders, tableData) - if isComponentTypePresent { - o.Logger.Output("\nCommand to describe component operation(s)") - o.Logger.ItalicEmphasize("odin describe operation --name --component-type ") - } else { - o.Logger.Output("\nCommand to describe service operations") - o.Logger.ItalicEmphasize("odin describe operation --name ") - } + o.Logger.Output(outputMsg) + o.Logger.ItalicEmphasize(descibeCommandMsg) return 0 } if o.Describe { isNamePresent := len(*name) > 0 - isComponentTypePresent := len(*componentType) > 0 - if !isNamePresent { o.Logger.Error("--name cannot be blank") return 1 } + if o.parseFlags(entity, componentType, &isEnvOperation, &isComponentOperation, &isServiceOperation) == 1 { + return 1 + } + + infoMsg := "Describing the " + *entity + " operation: " + *name + errorMsg := "operation: " + *name + " is not a valid " + *entity + " operation" + var operationList []operationapi.Operation var err error - if isComponentTypePresent { + if isComponentOperation { operationList, err = operationClient.ListComponentTypeOperations(*componentType) - } else { + infoMsg = "Describing operation: " + *name + " on component " + *componentType + errorMsg = "operation: " + *name + " does not exist for the component: " + *componentType + } else if isServiceOperation { operationList, err = operationClient.ListServiceOperations() + } else if isEnvOperation { + operationList, err = operationClient.ListEnvOperations() } if err != nil { @@ -104,11 +119,7 @@ func (o *Operation) Run(args []string) int { } if operationKeys == nil { - if isComponentTypePresent { - o.Logger.Error(fmt.Sprintf("operation: %s does not exist for the component: %s", *name, *componentType)) - } else { - o.Logger.Error(fmt.Sprintf("operation: %s is not a valid service operation", *name)) - } + o.Logger.Error(errorMsg) return 1 } @@ -118,12 +129,7 @@ func (o *Operation) Run(args []string) int { return 1 } - if isComponentTypePresent { - o.Logger.Info("Describing operation: " + *name + " on component " + *componentType) - } else { - o.Logger.Info("Describing the service operation: " + *name) - } - + o.Logger.Info(infoMsg) o.Logger.Output(fmt.Sprintf("\n%s", operationKeysJson)) return 0 } @@ -131,17 +137,52 @@ func (o *Operation) Run(args []string) int { return 127 } +func (o *Operation) parseFlags(entity, componentType *string, isEnvOperation, isComponentOperation, isServiceOperation *bool) int { + isComponentTypePresent := len(*componentType) > 0 + isEntityPresent := len(*entity) > 0 + + if isEntityPresent { + if *entity == "component" { + if !isComponentTypePresent { + o.Logger.Error("--component-type cannot be blank when --entity is component") + return 1 + } + *isComponentOperation = true + } else if isComponentTypePresent { + o.Logger.Error("--component-type should be used only when --entity is component") + return 1 + } else if *entity == "env" { + *isEnvOperation = true + } else if *entity == "service" { + *isServiceOperation = true + } else { + o.Logger.Error("Unknown value for --entity. Use one of env|service|component") + return 1 + } + } else { + if isComponentTypePresent { + *isComponentOperation = true + } else { + *isServiceOperation = true + } + } + + return 0 +} + // Help : returns an explanatory string func (o *Operation) Help() string { if o.List { return commandHelper("list", "operation", "", []Options{ {Flag: "--component-type", Description: "component-type on which operations will be performed"}, + {Flag: "--entity", Description: "name of the entity [env|service|component]"}, }) } if o.Describe { return commandHelper("describe", "operation", "", []Options{ {Flag: "--name", Description: "name of the operation"}, {Flag: "--component-type", Description: "component-type on which operations will be performed"}, + {Flag: "--entity", Description: "name of the entity [env|service|component]"}, }) } return defaultHelper() @@ -150,10 +191,10 @@ func (o *Operation) Help() string { // Synopsis : returns a brief helper text for the command's verbs func (o *Operation) Synopsis() string { if o.List { - return "list all operations on service or a component-type" + return "list all operations on environment, service or a component-type" } if o.Describe { - return "describe a operation on service or a component-type" + return "describe a operation on environment, service or a component-type" } return defaultHelper() } diff --git a/internal/command/commands/service.go b/internal/command/commands/service.go index 01c19aa2..b3dbd525 100644 --- a/internal/command/commands/service.go +++ b/internal/command/commands/service.go @@ -404,7 +404,7 @@ func (s *Service) Run(args []string) int { "action": *operation, "config": optionsData, } - scalingConsent := s.askForScalingConsent(serviceName, envName, dataForScalingConsent) + scalingConsent := s.askForScalingConsent(serviceName, dataForScalingConsent) if scalingConsent == 1 { return 1 } @@ -481,7 +481,7 @@ func (s *Service) askForConsent(envName *string) int { return 0 } -func (s *Service) askForScalingConsent(serviceName *string, envName *string, data map[string]interface{}) int { +func (s *Service) askForScalingConsent(serviceName *string, data map[string]interface{}) int { componentListResponse, err := serviceClient.ScalingServiceConsent(*serviceName, data) if err != nil { s.Logger.Error(err.Error()) @@ -540,7 +540,7 @@ func (s *Service) deployReleasedService(envName *string, serviceName *string, se "action": "released_service_deploy", "config": parsedProvisioningConfig, } - scalingConsent := s.askForScalingConsent(serviceName, envName, dataForScalingConsent) + scalingConsent := s.askForScalingConsent(serviceName, dataForScalingConsent) if scalingConsent == 1 { return 1 } @@ -712,7 +712,7 @@ func (s *Service) Help() string { return commandHelper("operate", "service", "", []Options{ {Flag: "--name", Description: "name of service"}, {Flag: "--env", Description: "name of environment"}, - {Flag: "--operate", Description: "name of the operation to be performed on the service"}, + {Flag: "--operation", Description: "name of the operation to be performed on the service"}, {Flag: "--file", Description: "path of the file which contains the options for the operation in JSON format"}, {Flag: "--options", Description: "options for the operation in JSON format"}, }) diff --git a/internal/command/commands/serviceset.go b/internal/command/commands/serviceset.go index c4f6b34d..f481b610 100644 --- a/internal/command/commands/serviceset.go +++ b/internal/command/commands/serviceset.go @@ -192,7 +192,7 @@ func (s *ServiceSet) Help() string { {Flag: "--name", Description: "name of service-set to deploy"}, {Flag: "--env", Description: "name of environment to deploy service-set in"}, {Flag: "--force", Description: "forcefully deploy service-set into the Env"}, - {Flag: "--d11", Description: "config-store-namespace=config store branch/tag to use"}, + {Flag: "--d11-config-store-namespace", Description: "config store branch/tag to use"}, {Flag: "--file", Description: "json file to read temporary service-set definition "}, }) } From 9a330b3bac4e9eabf715e1924c76ba32e0ae7d21 Mon Sep 17 00:00:00 2001 From: Susanth <110461367+boppanasusanth@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:29:09 +0530 Subject: [PATCH 2/3] fixes in env operation flow (#222) --- internal/backend/env.go | 2 +- internal/command/commands/env.go | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/backend/env.go b/internal/backend/env.go index b0669631..2a5ed6e0 100644 --- a/internal/backend/env.go +++ b/internal/backend/env.go @@ -180,7 +180,7 @@ func (e *Env) ValidateOperation(envName string, data envResp.OperationRequest) ( return validateOperationResponse, err } -func (e *Env) OperateService(envName string, data envResp.OperationRequest) (envResp.OperationResponse, error) { +func (e *Env) OperateEnv(envName string, data envResp.OperationRequest) (envResp.OperationResponse, error) { client := newApiClient() response := client.actionWithRetry(path.Join(envEntity, envName)+"/operate/", "PUT", data) diff --git a/internal/command/commands/env.go b/internal/command/commands/env.go index 8aab4caa..ce86d97e 100644 --- a/internal/command/commands/env.go +++ b/internal/command/commands/env.go @@ -457,20 +457,15 @@ func (e *Env) Run(args []string) int { } } - operateResponse, err := envClient.OperateService(*name, data) + operateResponse, err := envClient.OperateEnv(*name, data) if err != nil { e.Logger.Error(err.Error()) return 1 } - errored := false for _, operation := range operateResponse.Response.Operations { e.Logger.Output(operation.Message) } - - if errored { - return 1 - } return 0 } From 4e3188d9fd4859664aa3947c5023380c4331d3c9 Mon Sep 17 00:00:00 2001 From: Susanth <110461367+boppanasusanth@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:28:55 +0530 Subject: [PATCH 3/3] fix the issue when entity is none (#223) --- internal/command/commands/operation.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/command/commands/operation.go b/internal/command/commands/operation.go index c1f32011..b2999675 100644 --- a/internal/command/commands/operation.go +++ b/internal/command/commands/operation.go @@ -162,8 +162,10 @@ func (o *Operation) parseFlags(entity, componentType *string, isEnvOperation, is } else { if isComponentTypePresent { *isComponentOperation = true + *entity = "component" } else { *isServiceOperation = true + *entity = "service" } }