diff --git a/application/service/models.go b/application/service/models.go index c2080b5..373e969 100644 --- a/application/service/models.go +++ b/application/service/models.go @@ -67,7 +67,7 @@ type Environment struct { // Application represents an application with its associated environments. type Application struct { - ID int `json:"id"` // Unique identifier of the application + ID int64 `json:"id"` // Unique identifier of the application Name string `json:"name"` // Name of the application Envs []Environment `json:"environments,omitempty"` // List of associated environments } diff --git a/deploymentspace/handler/deployment.go b/deploymentspace/handler/deployment.go new file mode 100644 index 0000000..867a82d --- /dev/null +++ b/deploymentspace/handler/deployment.go @@ -0,0 +1,41 @@ +// Package handler is used to import data from external sources +// this package has an Add(ctx *gofr.Context) method that is used to configure deployment space +// for the environments of applications. +package handler + +import "gofr.dev/pkg/gofr" + +// Handler is responsible for handling requests related to deployment operations. +type Handler struct { + deployService DeploymentService +} + +// New initializes a new Handler instance. +// +// Parameters: +// - depSvc: An instance of DeploymentService used to manage deployment-related operations. +// +// Returns: +// - A pointer to the Handler instance. +func New(depSvc DeploymentService) *Handler { + return &Handler{ + deployService: depSvc, + } +} + +// Add processes a deployment creation request. +// +// Parameters: +// - ctx: The context object containing request and session details. +// +// Returns: +// - A success message if the deployment is created successfully. +// - An error if the deployment creation fails. +func (h *Handler) Add(ctx *gofr.Context) (any, error) { + err := h.deployService.Add(ctx) + if err != nil { + return nil, err + } + + return "Deployment Created", nil +} diff --git a/deploymentspace/handler/interface.go b/deploymentspace/handler/interface.go new file mode 100644 index 0000000..ffc4f4e --- /dev/null +++ b/deploymentspace/handler/interface.go @@ -0,0 +1,17 @@ +package handler + +import "gofr.dev/pkg/gofr" + +// DeploymentService defines the interface for deployment-related operations. +// +// It contains methods that allow adding and managing deployments. +type DeploymentService interface { + // Add creates a new deployment. + // + // Parameters: + // - ctx: The context object containing request and session details. + // + // Returns: + // - An error if the deployment creation fails. + Add(ctx *gofr.Context) error +} diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go new file mode 100644 index 0000000..7c36ea0 --- /dev/null +++ b/deploymentspace/service/deployment.go @@ -0,0 +1,219 @@ +// Package service provides structures and interfaces for managing deployment options and related operations. +package service + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd/terminal" + + "zop.dev/cli/zop/utils" +) + +var ( + + // ErrConnectingZopAPI is returned when there is an error connecting to the Zop API. + ErrConnectingZopAPI = errors.New("unable to connect to Zop API") + + // ErrGettingDeploymentOptions is returned when there is an error adding an environment. + ErrGettingDeploymentOptions = errors.New("unable to get deployment options") + + // ErrorFetchingEnvironments is returned when there is an error fetching environments for a given application. + ErrorFetchingEnvironments = errors.New("unable to fetch environments") + + // ErrUnknown is returned when an unknown error occurs while processing the request. + ErrUnknown = errors.New("unknown error occurred while processing the request") +) + +// Service represents the core service that handles cloud account and environment-related operations. +type Service struct { + cloudGet CloudAccountService + envGet EnvironmentService +} + +// New initializes a new Service instance. +// +// Parameters: +// - cloudGet: A CloudAccountService instance for retrieving cloud accounts. +// - envGet: An EnvironmentService instance for retrieving environments. +// +// Returns: +// - A pointer to the Service instance. +func New(cloudGet CloudAccountService, envGet EnvironmentService) *Service { + return &Service{ + cloudGet: cloudGet, + envGet: envGet, + } +} + +// Add handles the addition of a deployment configuration. +// +// This function selects a cloud account and environment, retrieves deployment options, +// processes the options, and submits the deployment request. +// +// Parameters: +// - ctx: The context object containing request and session details. +// +// Returns: +// - An error if any step in the process fails. +func (s *Service) Add(ctx *gofr.Context) error { + var request = make(map[string]any) + + cloudAcc, err := s.getSelectedCloudAccount(ctx) + if err != nil { + return err + } + + request["cloudAccount"] = cloudAcc + + ctx.Out.Println("Selected cloud account: ", cloudAcc.Name) + + env, err := s.getSelectedEnvironment(ctx) + if err != nil { + return err + } + + ctx.Out.Println("Selected environment"+ + ":", env.Name) + + options, err := getDeploymentSpaceOptions(ctx, cloudAcc.ID) + if err != nil { + return err + } + + request[options.Type] = options + + if er := processOptions(ctx, request, options.Path); er != nil { + return er + } + + return submitDeployment(ctx, env.ID, request) +} + +func processOptions(ctx *gofr.Context, request map[string]any, path string) error { + var ( + optionName string + sp = terminal.NewDotSpinner(ctx.Out) + api = ctx.GetHTTPService("api-service") + ) + + sp.Spin(ctx) + + resp, err := api.Get(ctx, path[1:], nil) + if err != nil { + ctx.Logger.Errorf("error connecting to zop api! %v", err) + return ErrConnectingZopAPI + } + + var option apiResponse + if er := utils.GetResponse(resp, &option); er != nil { + ctx.Logger.Errorf("error fetching deployment options! %v", er) + + return ErrGettingDeploymentOptions + } + + resp.Body.Close() + sp.Stop() + + for { + optionName = "option" + + if option.Data.Metadata != nil { + optionName = option.Data.Metadata.Name + } + + opt, er := getSelectedOption(ctx, option.Data.Option, optionName) + if er != nil { + return er + } + + sp.Spin(ctx) + + updateRequestWithOption(request, opt) + + if option.Data.Next == nil { + break + } + + params := getParameters(opt, &option) + + resp, er = api.Get(ctx, option.Data.Next.Path[1:]+params, nil) + if er != nil { + ctx.Logger.Errorf("error connecting to zop api! %v", er) + return ErrConnectingZopAPI + } + + option.Data = nil + + er = utils.GetResponse(resp, &option) + if er != nil { + ctx.Logger.Errorf("error fetching deployment options! %v", er) + return ErrGettingDeploymentOptions + } + + resp.Body.Close() + sp.Stop() + } + + return nil +} + +func updateRequestWithOption(request, opt map[string]any) { + keys := strings.Split(opt["type"].(string), ".") + current := request + + for i, key := range keys { + if i == len(keys)-1 { + current[key] = opt + break + } + + if _, exists := current[key]; !exists { + current[key] = make(map[string]any) + } + + current = current[key].(map[string]any) + } +} + +func submitDeployment(ctx *gofr.Context, envID int64, request map[string]any) error { + b, err := json.Marshal(request) + if err != nil { + return err + } + + resp, err := ctx.GetHTTPService("api-service"). + PostWithHeaders(ctx, fmt.Sprintf("environments/%d/deploymentspace", envID), nil, b, map[string]string{ + "Content-Type": "application/json", + }) + if err != nil { + return ErrConnectingZopAPI + } + + if resp.StatusCode != http.StatusCreated { + var er ErrorResponse + + err = utils.GetResponse(resp, &er) + if err != nil { + return ErrUnknown + } + + return &er + } + + return resp.Body.Close() +} + +func getParameters(opt map[string]any, options *apiResponse) string { + params := "?" + + for _, v := range options.Data.Next.Params { + params += fmt.Sprintf("&%s=%s", v, opt[v]) + } + + return params +} diff --git a/deploymentspace/service/errors.go b/deploymentspace/service/errors.go new file mode 100644 index 0000000..54975e0 --- /dev/null +++ b/deploymentspace/service/errors.go @@ -0,0 +1,28 @@ +package service + +import "fmt" + +// ErrNoItemSelected represents an error that occurs when no item of a specific type is selected. +type ErrNoItemSelected struct { + Type string +} + +// Error returns the error message for ErrNoItemSelected. +// +// This method satisfies the error interface. +// +// Returns: +// - A formatted error message indicating the type of the unselected item. +func (e *ErrNoItemSelected) Error() string { + return fmt.Sprintf("no %s selected", e.Type) +} + +type ErrorResponse struct { + Er struct { + Message string `json:"message"` + } `json:"error"` +} + +func (e *ErrorResponse) Error() string { + return e.Er.Message +} diff --git a/deploymentspace/service/interface.go b/deploymentspace/service/interface.go new file mode 100644 index 0000000..c763c3d --- /dev/null +++ b/deploymentspace/service/interface.go @@ -0,0 +1,36 @@ +package service + +import ( + "gofr.dev/pkg/gofr" + + cloudSvc "zop.dev/cli/zop/cloud/service/list" + envSvc "zop.dev/cli/zop/environment/service" +) + +// CloudAccountService defines the interface for managing cloud accounts. +// It provides methods to retrieve cloud accounts available to the user. +type CloudAccountService interface { + // GetAccounts retrieves a list of cloud accounts. + // + // Parameters: + // - ctx: The context object containing request and session details. + // + // Returns: + // - A slice of pointers to CloudAccountResponse objects. + // - An error if the retrieval fails. + GetAccounts(ctx *gofr.Context) ([]*cloudSvc.CloudAccountResponse, error) +} + +// EnvironmentService defines the interface for managing environments. +// It provides methods to list the environments associated with a user. +type EnvironmentService interface { + // List retrieves a list of environments. + // + // Parameters: + // - ctx: The context object containing request and session details. + // + // Returns: + // - A slice of Environment objects. + // - An error if the retrieval fails. + List(ctx *gofr.Context) ([]envSvc.Environment, error) +} diff --git a/deploymentspace/service/listings.go b/deploymentspace/service/listings.go new file mode 100644 index 0000000..807c1b3 --- /dev/null +++ b/deploymentspace/service/listings.go @@ -0,0 +1,145 @@ +package service + +import ( + "errors" + "fmt" + + "gofr.dev/pkg/gofr" + + cloudSvc "zop.dev/cli/zop/cloud/service/list" + envSvc "zop.dev/cli/zop/environment/service" + "zop.dev/cli/zop/utils" +) + +const ( + accListTitle = "Select the cloud account where you want to add the deployment!" + deploymentSpaceTitle = "Select the deployment space where you want to add the deployment!" +) + +var ( + // ErrUnableToRenderList is returned when the application list cannot be rendered. + ErrUnableToRenderList = errors.New("unable to render the list") + + // ErrNoOptionsFound is returned when there are no options available for selection. + ErrNoOptionsFound = errors.New("no options available for selection") +) + +func (s *Service) getSelectedCloudAccount(ctx *gofr.Context) (*cloudSvc.CloudAccountResponse, error) { + accounts, err := s.cloudGet.GetAccounts(ctx) + if err != nil { + ctx.Logger.Errorf("unable to fetch cloud accounts! %v", err) + } + + items := make([]*utils.Item, 0) + for _, acc := range accounts { + items = append(items, &utils.Item{ID: acc.ID, Name: acc.Name, Data: acc}) + } + + choice, err := utils.RenderList(accListTitle, items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of cloud accounts! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{"cloud account"} + } + + return choice.Data.(*cloudSvc.CloudAccountResponse), nil +} + +func (s *Service) getSelectedEnvironment(ctx *gofr.Context) (*envSvc.Environment, error) { + envs, err := s.envGet.List(ctx) + if err != nil { + ctx.Logger.Errorf("unable to fetch environments! %v", err) + + return nil, ErrorFetchingEnvironments + } + + items := make([]*utils.Item, 0) + + for _, env := range envs { + items = append(items, &utils.Item{ID: env.ID, Name: env.Name, Data: &env}) + } + + choice, err := utils.RenderList("Select the environment where you want to add the deployment!", items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of environments! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil { + return nil, &ErrNoItemSelected{"environment"} + } + + return choice.Data.(*envSvc.Environment), nil +} + +func getDeploymentSpaceOptions(ctx *gofr.Context, id int64) (*DeploymentSpaceOptions, error) { + resp, err := ctx.GetHTTPService("api-service"). + Get(ctx, fmt.Sprintf("cloud-accounts/%d/deployment-space/options", id), nil) + if err != nil { + ctx.Logger.Errorf("error connecting to zop api! %v", err) + + return nil, ErrConnectingZopAPI + } + + defer resp.Body.Close() + + var opts struct { + Options []*DeploymentSpaceOptions `json:"data"` + } + + err = utils.GetResponse(resp, &opts) + if err != nil { + ctx.Logger.Errorf("error fetching deployment space options! %v", err) + + return nil, ErrGettingDeploymentOptions + } + + items := make([]*utils.Item, 0) + + for _, opt := range opts.Options { + items = append(items, &utils.Item{Name: opt.Name, Data: opt}) + } + + choice, err := utils.RenderList(deploymentSpaceTitle, items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of deployment spaces! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{"deployment space"} + } + + return choice.Data.(*DeploymentSpaceOptions), nil +} + +func getSelectedOption(ctx *gofr.Context, items []map[string]any, name string) (map[string]any, error) { + listI := make([]*utils.Item, 0) + + if len(items) == 0 { + return nil, ErrNoOptionsFound + } + + for _, item := range items { + listI = append(listI, &utils.Item{Name: item["name"].(string), Data: item}) + } + + choice, err := utils.RenderList("Select the "+name, listI) + if err != nil { + ctx.Logger.Errorf("unable to render the list of environments! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{items[0]["type"].(string)} + } + + return choice.Data.(map[string]any), nil +} diff --git a/deploymentspace/service/models.go b/deploymentspace/service/models.go new file mode 100644 index 0000000..4affacd --- /dev/null +++ b/deploymentspace/service/models.go @@ -0,0 +1,37 @@ +package service + +// DeploymentSpaceOptions represents the deployment space options in the system. +// +// It includes information such as the name, path, and type of the deployment space. +type DeploymentSpaceOptions struct { + Name string `json:"name"` // The name of the deployment space. + Path string `json:"path"` // The API path to access the deployment space. + Type string `json:"type"` // The type of the deployment space. +} + +// DeploymentOption represents a list of deployment options and information about the next page, if available. +type DeploymentOption struct { + Option []map[string]any `json:"options"` // A slice of options for deployment. + Next *Next `json:"next"` // Information about the next page of options. + Metadata *metadata `json:"metadata"` // Additional metadata about the deployment options. +} + +type metadata struct { + Name string `json:"name"` +} + +// Next provides details about the subsequent page of deployment options. +// +// It includes the name, path, and query parameters for accessing the next page. +type Next struct { + Name string `json:"name"` // The name of the next page. + Path string `json:"path"` // The API path to access the next page. + Params map[string]string `json:"params"` // Query parameters required for the next page. +} + +// apiResponse represents the structure of the API response for deployment options. +// +// It contains a data field with deployment options. +type apiResponse struct { + Data *DeploymentOption `json:"data"` // The deployment option data. +} diff --git a/environment/service/env.go b/environment/service/env.go index f3dd68e..e94396f 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -12,7 +12,7 @@ import ( "zop.dev/cli/zop/utils" ) -const listTitle = "Select the application where you want to add the environment!" +const listTitle = "Select the application!" var ( // ErrUnableToRenderApps is returned when the application list cannot be rendered. @@ -63,7 +63,7 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { _, _ = fmt.Scanf("%s", &input) - err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: int64(app.ID)}) + err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: app.ID}) if err != nil { return level, err } @@ -88,6 +88,8 @@ func (s *Service) List(ctx *gofr.Context) ([]Environment, error) { return nil, err } + ctx.Out.Println("Selected application: ", app.Name) + resp, err := ctx.GetHTTPService("api-service"). Get(ctx, fmt.Sprintf("applications/%d/environments", app.ID), nil) if err != nil { diff --git a/main.go b/main.go index c468079..df94dac 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,8 @@ import ( impService "zop.dev/cli/zop/cloud/service/gcp" listSvc "zop.dev/cli/zop/cloud/service/list" impStore "zop.dev/cli/zop/cloud/store/gcp" + depHandler "zop.dev/cli/zop/deploymentspace/handler" + depSvc "zop.dev/cli/zop/deploymentspace/service" envHandler "zop.dev/cli/zop/environment/handler" envService "zop.dev/cli/zop/environment/service" ) @@ -66,5 +68,10 @@ func main() { app.SubCommand("environment add", envH.Add) app.SubCommand("environment list", envH.List) + dSvc := depSvc.New(lSvc, envSvc) + dH := depHandler.New(dSvc) + + app.SubCommand("deployment add", dH.Add) + app.Run() } diff --git a/utils/list.go b/utils/list.go index fa58938..6ae9588 100644 --- a/utils/list.go +++ b/utils/list.go @@ -1,3 +1,6 @@ +// Package utils package provides utility functions for the application. +// It provides rendering of list based on the list of items provided. +// It also provides a function to unmarshal the response from the API to the struct. package utils import ( @@ -33,7 +36,7 @@ var ( // Item represents a single item in the list. type Item struct { - ID int // ID is the unique identifier for the item. + ID int64 // ID is the unique identifier for the item. Name string // Name is the display name of the item. Data any } diff --git a/utils/response.go b/utils/response.go new file mode 100644 index 0000000..000f03f --- /dev/null +++ b/utils/response.go @@ -0,0 +1,19 @@ +package utils + +import ( + "encoding/json" + "io" + "net/http" +) + +// GetResponse reads the HTTP response body and unmarshals it into the provided interface. +func GetResponse(resp *http.Response, i any) error { + b, _ := io.ReadAll(resp.Body) + + err := json.Unmarshal(b, i) + if err != nil { + return err + } + + return nil +}