From eb094f10b63969e9936627ca19e24beba9a502d8 Mon Sep 17 00:00:00 2001 From: Dejan K Date: Fri, 16 Jun 2023 12:13:26 +0200 Subject: [PATCH] Add commands for deployment targets (#216) * Add commands for deployment targets * Remove unused code * Sync with API changes * Remove useless character * Rename function, variable * Fixes after code review, using UUIDv4 * Add create_target_test, used UUIDv4 for mock * Enrich create target test * fix object rule mode: pattern->regex * Remove unused file * Use DescribeWithSecret instead of optional parameter * Better handling history cmd, object-rule parsing * Remove fmt printing --- .goreleaser.yml | 12 +- Makefile | 6 +- api/client/base_client.go | 10 +- api/client/deployment_targets_v1_alpha.go | 244 ++++++++++++ api/client/pipelines_v1_alpha.go | 10 +- api/client/projects_v1_alpha.go | 1 - api/client/workflows_v1_alpha.go | 2 +- api/models/deployment_target_v1_alpha.go | 351 ++++++++++++++++++ api/models/deployment_target_v1_alpha_test.go | 137 +++++++ api/models/job_v1_alpha.go | 24 +- api/models/project_secret_v1.go | 12 +- api/models/secret_v1_beta.go | 14 +- api/uuid/uuid.go | 42 +++ cmd/apply.go | 20 +- cmd/apply_test.go | 56 +++ cmd/create.go | 22 +- cmd/create_target.go | 204 ++++++++++ cmd/create_target_test.go | 65 ++++ cmd/create_test.go | 45 +++ cmd/debug_project.go | 3 +- cmd/delete.go | 17 + cmd/delete_test.go | 36 ++ cmd/deployment_targets/delete.go | 17 + cmd/deployment_targets/get.go | 142 +++++++ cmd/deployment_targets/rebuild.go | 17 + cmd/deployment_targets/stop.go | 17 + cmd/edit.go | 102 +++++ cmd/edit_test.go | 210 +++++++++++ cmd/get.go | 59 ++- cmd/get_test.go | 139 ++++++- cmd/utils/check.go | 4 - cmd/utils/csv_flag.go | 23 ++ cmd/utils/project.go | 4 +- cmd/utils/yaml_resource.go | 3 - go.mod | 26 +- go.sum | 16 +- 36 files changed, 2038 insertions(+), 74 deletions(-) create mode 100644 api/client/deployment_targets_v1_alpha.go create mode 100644 api/models/deployment_target_v1_alpha.go create mode 100644 api/models/deployment_target_v1_alpha_test.go create mode 100644 api/uuid/uuid.go create mode 100644 cmd/create_target.go create mode 100644 cmd/create_target_test.go create mode 100644 cmd/deployment_targets/delete.go create mode 100644 cmd/deployment_targets/get.go create mode 100644 cmd/deployment_targets/rebuild.go create mode 100644 cmd/deployment_targets/stop.go diff --git a/.goreleaser.yml b/.goreleaser.yml index f142ff6..e8a2092 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,12 +13,12 @@ builds: - amd64 - arm64 archives: - - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - replacements: - darwin: Darwin - linux: Linux - 386: i386 - amd64: x86_64 + - name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} checksum: name_template: '{{ .ProjectName }}_checksums.txt' changelog: diff --git a/Makefile b/Makefile index 217f16f..15a73a0 100644 --- a/Makefile +++ b/Makefile @@ -4,14 +4,14 @@ REL_VERSION=$(shell git rev-parse HEAD) REL_BUCKET=sem-cli-releases install.goreleaser: - curl -L https://github.com/goreleaser/goreleaser/releases/download/v1.9.1/goreleaser_Linux_x86_64.tar.gz -o /tmp/goreleaser.tar.gz + curl -L https://github.com/goreleaser/goreleaser/releases/download/v1.14.1/goreleaser_Linux_x86_64.tar.gz -o /tmp/goreleaser.tar.gz tar -xf /tmp/goreleaser.tar.gz -C /tmp sudo mv /tmp/goreleaser /usr/bin/goreleaser go.install: cd /tmp - sudo curl -O https://dl.google.com/go/go1.16.linux-amd64.tar.gz - sudo tar -xf go1.16.linux-amd64.tar.gz + sudo curl -O https://dl.google.com/go/go1.17.13.linux-amd64.tar.gz + sudo tar -xf go1.17.13.linux-amd64.tar.gz sudo mv go /usr/local cd - diff --git a/api/client/base_client.go b/api/client/base_client.go index 2be8c34..4b55860 100644 --- a/api/client/base_client.go +++ b/api/client/base_client.go @@ -83,11 +83,12 @@ func (c *BaseClient) Get(kind string, resource string) ([]byte, int, error) { func (c *BaseClient) List(kind string) ([]byte, int, error) { url := fmt.Sprintf("https://%s/api/%s/%s", c.host, c.apiVersion, kind) - log.Printf("GET %s\n", url) req, err := http.NewRequest("GET", url, nil) - + if err != nil { + return nil, 0, err + } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.authToken)) req.Header.Set("User-Agent", UserAgent) @@ -218,9 +219,10 @@ func (c *BaseClient) PostHeaders(kind string, resource []byte, headers map[strin func (c *BaseClient) Patch(kind string, name string, resource []byte) ([]byte, int, error) { url := fmt.Sprintf("https://%s/api/%s/%s/%s", c.host, c.apiVersion, kind, name) - log.Printf("PATCH %s\n", url) - req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(resource)) + if err != nil { + return nil, 0, err + } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.authToken)) diff --git a/api/client/deployment_targets_v1_alpha.go b/api/client/deployment_targets_v1_alpha.go new file mode 100644 index 0000000..80d8db6 --- /dev/null +++ b/api/client/deployment_targets_v1_alpha.go @@ -0,0 +1,244 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + + models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" +) + +type DeploymentTargetsApiV1AlphaApi struct { + BaseClient BaseClient + ResourceNameSingular string + ResourceNamePlural string +} + +const ( + DeactivateOpName = "deactivate" + ActivateOpName = "activate" +) + +func NewDeploymentTargetsV1AlphaApi() DeploymentTargetsApiV1AlphaApi { + baseClient := NewBaseClientFromConfig() + baseClient.SetApiVersion("v1alpha") + + return DeploymentTargetsApiV1AlphaApi{ + BaseClient: baseClient, + ResourceNamePlural: "deployment_targets", + ResourceNameSingular: "deployment_target", + } +} + +func (c *DeploymentTargetsApiV1AlphaApi) Describe(targetId string) (*models.DeploymentTargetV1Alpha, error) { + return c.describe(targetId, false) +} + +func (c *DeploymentTargetsApiV1AlphaApi) DescribeWithSecrets(targetId string) (*models.DeploymentTargetV1Alpha, error) { + return c.describe(targetId, true) +} + +func (c *DeploymentTargetsApiV1AlphaApi) describe(targetId string, withSecrets bool) (*models.DeploymentTargetV1Alpha, error) { + var resource string + if withSecrets { + resource = fmt.Sprintf("%s?include_secrets=true", targetId) + } else { + resource = targetId + } + body, status, err := c.BaseClient.Get(c.ResourceNamePlural, resource) + if err != nil { + return nil, fmt.Errorf("connecting to Semaphore failed '%s'", err) + } + + if status != http.StatusOK { + return nil, fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + + return models.NewDeploymentTargetV1AlphaFromJson(body) +} + +func (c *DeploymentTargetsApiV1AlphaApi) DescribeByName(targetName, projectId string) (*models.DeploymentTargetV1Alpha, error) { + targets, err := c.list(projectId, targetName) + if err != nil { + return nil, err + } + if targets == nil || len(*targets) == 0 { + return nil, fmt.Errorf("target with the name '%s' doesn't exist in the project '%s'", targetName, projectId) + } + return (*targets)[0], nil +} + +func (c *DeploymentTargetsApiV1AlphaApi) History(targetId string, historyRequest models.HistoryRequestFiltersV1Alpha) (*models.DeploymentsHistoryV1Alpha, error) { + if targetId == "" { + return nil, errors.New("target id must be provided") + } + values, err := historyRequest.ToURLValues() + if err != nil { + return nil, err + } + query := fmt.Sprintf("%s/history", targetId) + if len(values) != 0 { + query = fmt.Sprintf("%s/history?%s", targetId, values.Encode()) + } + + body, status, err := c.BaseClient.Get(c.ResourceNamePlural, query) + if err != nil { + return nil, fmt.Errorf("connecting to Semaphore failed '%s'", err) + } + + if status != http.StatusOK { + return nil, fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + + return models.NewDeploymentsHistoryV1AlphaFromJson(body) +} + +func (c *DeploymentTargetsApiV1AlphaApi) List(projectId string) (*models.DeploymentTargetListV1Alpha, error) { + kind := fmt.Sprintf("%s?project_id=%s", c.ResourceNamePlural, projectId) + body, status, err := c.BaseClient.List(kind) + if err != nil { + return nil, fmt.Errorf("connecting to Semaphore failed '%s'", err) + } + + if status != http.StatusOK { + return nil, fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + return models.NewDeploymentTargetListV1AlphaFromJson(body) +} + +func (c *DeploymentTargetsApiV1AlphaApi) list(projectId string, targetNames ...string) (*models.DeploymentTargetListV1Alpha, error) { + kind := fmt.Sprintf("%s?project_id=%s", c.ResourceNamePlural, projectId) + for _, targetName := range targetNames { + kind = fmt.Sprintf("%s&target_name=%s", kind, url.PathEscape(targetName)) + } + body, status, err := c.BaseClient.List(kind) + if err != nil { + return nil, fmt.Errorf("connecting to Semaphore failed '%s'", err) + } + + if status != http.StatusOK { + return nil, fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + return models.NewDeploymentTargetListV1AlphaFromJson(body) +} + +func (c *DeploymentTargetsApiV1AlphaApi) Delete(targetId string) error { + unique_token, err := uuid.NewUUIDv4() + if err != nil { + return fmt.Errorf("unique token generation failed: %s", err) + } + params := fmt.Sprintf("%s?unique_token=%s", targetId, unique_token) + + body, status, err := c.BaseClient.Delete(c.ResourceNamePlural, params) + if err != nil { + return err + } + + if status != http.StatusOK { + return fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + + return nil +} + +func (c *DeploymentTargetsApiV1AlphaApi) Create(createRequest *models.DeploymentTargetCreateRequestV1Alpha) (*models.DeploymentTargetV1Alpha, error) { + if createRequest == nil { + return nil, errors.New("create request must not be nil") + } + err := createRequest.LoadFiles() + if err != nil { + return nil, err + } + unique_token, err := uuid.NewUUIDv4() + if err != nil { + return nil, fmt.Errorf("unique token generation failed: %s", err) + } + createRequest.UniqueToken = unique_token.String() + json_body, err := json.Marshal(createRequest) + if err != nil { + return nil, fmt.Errorf("failed to serialize deployment target create request: %s", err) + } + + body, status, err := c.BaseClient.Post(c.ResourceNamePlural, json_body) + if err != nil { + return nil, fmt.Errorf("creating %s on Semaphore failed '%s'", c.ResourceNamePlural, err) + } + + if status != http.StatusOK { + return nil, fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + + return models.NewDeploymentTargetV1AlphaFromJson(body) +} + +func (c *DeploymentTargetsApiV1AlphaApi) Update(updateRequest *models.DeploymentTargetUpdateRequestV1Alpha) (*models.DeploymentTargetV1Alpha, error) { + if updateRequest == nil { + return nil, errors.New("update request must not be nil") + } + if updateRequest.Id == "" { + return nil, errors.New("update request id must not be empty") + } + if updateRequest.ProjectId == "" { + return nil, errors.New("update request project id must not be empty") + } + unique_token, err := uuid.NewUUIDv4() + if err != nil { + return nil, fmt.Errorf("unique token generation failed: %s", err) + } + updateRequest.UniqueToken = unique_token.String() + + json_body, err := json.Marshal(updateRequest) + if err != nil { + return nil, fmt.Errorf("failed to serialize deployment target update request: %s", err) + } + + body, status, err := c.BaseClient.Patch(c.ResourceNamePlural, updateRequest.Id, json_body) + if err != nil { + return nil, fmt.Errorf("creating %s on Semaphore failed '%s'", c.ResourceNamePlural, err) + } + + if status != http.StatusOK { + return nil, fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + + return models.NewDeploymentTargetV1AlphaFromJson(body) +} + +func (c *DeploymentTargetsApiV1AlphaApi) Activate(targetId string) (bool, error) { + return c.cordon(targetId, ActivateOpName) +} + +func (c *DeploymentTargetsApiV1AlphaApi) Deactivate(targetId string) (bool, error) { + return c.cordon(targetId, DeactivateOpName) +} + +func (c *DeploymentTargetsApiV1AlphaApi) cordon(targetId, opName string) (bool, error) { + query := fmt.Sprintf("%s/%s", targetId, opName) + + body, status, err := c.BaseClient.Patch(c.ResourceNamePlural, query, nil) + if err != nil { + return false, fmt.Errorf("creating %s on Semaphore failed '%s'", c.ResourceNamePlural, err) + } + if status != http.StatusOK { + return false, fmt.Errorf("http status %d with message \"%s\" received from upstream", status, body) + } + + response, err := models.NewCordonResponseV1AlphaFromJson(body) + if err != nil { + return false, fmt.Errorf("wrong response: %s", err) + } + if response.TargetId != targetId { + return false, fmt.Errorf("wrong target id in the response") + } + shouldBeCordoned := opName == DeactivateOpName + if response.Cordoned != shouldBeCordoned { + if response.Cordoned { + return false, fmt.Errorf("failed to activate deployment target") + } + return false, fmt.Errorf("failed to deactivate deployment target") + } + return true, nil +} diff --git a/api/client/pipelines_v1_alpha.go b/api/client/pipelines_v1_alpha.go index b0e07a6..634a254 100644 --- a/api/client/pipelines_v1_alpha.go +++ b/api/client/pipelines_v1_alpha.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" - uuid "github.com/google/uuid" models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" ) type PipelinesApiV1AlphaApi struct { @@ -78,10 +78,10 @@ func (c *PipelinesApiV1AlphaApi) PartialRebuildPpl(id string) ([]byte, error) { } func (c *PipelinesApiV1AlphaApi) ListPplByWfID(projectID, wfID string) ([]byte, error) { - detailed := fmt.Sprintf("%s?project_id=%s&wf_id=%s", c.ResourceNamePlural, projectID, wfID) + detailed := fmt.Sprintf("%s?project_id=%s&wf_id=%s", c.ResourceNamePlural, projectID, wfID) body, status, err := c.BaseClient.List(detailed) - if err != nil { + if err != nil { return nil, errors.New(fmt.Sprintf("connecting to Semaphore failed '%s'", err)) } @@ -93,10 +93,10 @@ func (c *PipelinesApiV1AlphaApi) ListPplByWfID(projectID, wfID string) ([]byte, } func (c *PipelinesApiV1AlphaApi) ListPpl(projectID string) ([]byte, error) { - detailed := fmt.Sprintf("%s?project_id=%s", c.ResourceNamePlural, projectID) + detailed := fmt.Sprintf("%s?project_id=%s", c.ResourceNamePlural, projectID) body, status, err := c.BaseClient.List(detailed) - if err != nil { + if err != nil { return nil, errors.New(fmt.Sprintf("connecting to Semaphore failed '%s'", err)) } diff --git a/api/client/projects_v1_alpha.go b/api/client/projects_v1_alpha.go index 85f16fb..774795f 100644 --- a/api/client/projects_v1_alpha.go +++ b/api/client/projects_v1_alpha.go @@ -36,7 +36,6 @@ func NewProjectV1AlphaApiWithCustomClient(client BaseClient) ProjectApiV1AlphaAp func (c *ProjectApiV1AlphaApi) ListProjects() (*models.ProjectListV1Alpha, error) { body, status, err := c.BaseClient.List(c.ResourceNamePlural) - if err != nil { return nil, errors.New(fmt.Sprintf("connecting to Semaphore failed '%s'", err)) } diff --git a/api/client/workflows_v1_alpha.go b/api/client/workflows_v1_alpha.go index 77ecea5..723de2d 100644 --- a/api/client/workflows_v1_alpha.go +++ b/api/client/workflows_v1_alpha.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" - "github.com/google/uuid" models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" ) type WorkflowApiV1AlphaApi struct { diff --git a/api/models/deployment_target_v1_alpha.go b/api/models/deployment_target_v1_alpha.go new file mode 100644 index 0000000..208bafe --- /dev/null +++ b/api/models/deployment_target_v1_alpha.go @@ -0,0 +1,351 @@ +package models + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + "strings" + "time" + + yaml "gopkg.in/yaml.v2" +) + +const ( + DeploymentTargetKindV1Alpha = "DeploymentTarget" + + ObjectRuleTypeBranchV1Alpha = "BRANCH" + ObjectRuleTypeTagV1Alpha = "TAG" + ObjectRuleTypePullRequestV1Alpha = "PR" + + ObjectRuleMatchModeAllV1Alpha = "ALL" + ObjectRuleMatchModeExactV1Alpha = "EXACT" + ObjectRuleMatchModeRegexV1Alpha = "REGEX" + + HistoryRequestCursorTypeFirstV1Alpha = "FIRST" + HistoryRequestCursorTypeAfterV1Alpha = "AFTER" + HistoryRequestCursorTypeBeforeV1Alpha = "BEFORE" +) + +var DeploymentTargetCmdAliases = []string{"deployment-target", "deployment-targets", "dt", "dts", "deployment", "deployments"} + +type DeploymentTargetListV1Alpha []*DeploymentTargetV1Alpha + +type DeploymentTargetV1Alpha struct { + ApiVersion string `json:"-" yaml:"apiVersion"` + Kind string `json:"-" yaml:"kind"` + + DeploymentTargetMetadataV1Alpha `yaml:"metadata,omitempty"` + DeploymentTargetSpecV1Alpha `yaml:"spec,omitempty"` +} + +type DeploymentTargetMetadataV1Alpha struct { + Id string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + ProjectId string `json:"project_id" yaml:"project_id"` + OrganizationId string `json:"organization_id" yaml:"organization_id"` + CreatedBy string `json:"created_by,omitempty" yaml:"created_by,omitempty"` + UpdatedBy string `json:"updated_by,omitempty" yaml:"updated_by,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty" yaml:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty" yaml:"updated_at,omitempty"` + Description string `json:"description" yaml:"description"` + Url string `json:"url" yaml:"url"` +} + +type DeploymentTargetSpecV1Alpha struct { + State string `json:"state" yaml:"state"` + StateMessage string `json:"state_message" yaml:"state_message"` + SubjectRules []*SubjectRuleV1Alpha `json:"subject_rules" yaml:"subject_rules"` + ObjectRules []*ObjectRuleV1Alpha `json:"object_rules" yaml:"object_rules"` + LastDeployment *DeploymentV1Alpha `json:"last_deployment,omitempty" yaml:"last_deployment,omitempty"` + Active bool `json:"active" yaml:"active"` + BookmarkParameter1 string `json:"bookmark_parameter1" yaml:"bookmark_parameter1"` + BookmarkParameter2 string `json:"bookmark_parameter2" yaml:"bookmark_parameter2"` + BookmarkParameter3 string `json:"bookmark_parameter3" yaml:"bookmark_parameter3"` + EnvVars *DeploymentTargetEnvVarsV1Alpha `json:"env_vars,omitempty" yaml:"env_vars,omitempty"` + Files *DeploymentTargetFilesV1Alpha `json:"files,omitempty" yaml:"files,omitempty"` +} + +type DeploymentV1Alpha struct { + Id string `json:"id" yaml:"id"` + TargetId string `json:"target_id" yaml:"target_id"` + PrevPipelineId string `json:"prev_pipeline_id" yaml:"prev_pipeline_id"` + PipelineId string `json:"pipeline_id" yaml:"pipeline_id"` + TriggeredBy string `json:"triggered_by,omitempty" yaml:"triggered_by,omitempty"` + TriggeredAt *time.Time `json:"triggered_at,omitempty" yaml:"triggered_at,omitempty"` + State string `json:"state" yaml:"state"` + StateMessage string `json:"state_message" yaml:"state_message"` + SwitchId string `json:"switch_id" yaml:"switch_id"` + TargetName string `json:"target_name" yaml:"target_name"` + EnvVars *DeploymentEnvVarsV1Alpha `json:"env_vars,omitempty" yaml:"env_vars,omitempty"` +} +type DeploymentsHistoryV1Alpha struct { + Deployments []DeploymentV1Alpha `json:"deployments,omitempty" yaml:"deployments,omitempty"` + CursorBefore int64 `json:"cursor_before,omitempty" yaml:"cursor_before,omitempty"` + CursorAfter int64 `json:"cursor_after,omitempty" yaml:"cursor_after,omitempty"` +} + +type DeploymentEnvVarsV1Alpha []*DeploymentEnvVarV1Alpha + +type DeploymentEnvVarV1Alpha struct { + Name string `json:"name" yaml:"name"` + Value string `json:"value" yaml:"value"` +} + +type SubjectRuleV1Alpha struct { + Type string `json:"type" yaml:"type"` + SubjectId string `json:"subject_id,omitempty" yaml:"subject_id,omitempty"` + GitLogin string `json:"git_login,omitempty" yaml:"git_login,omitempty"` +} + +type ObjectRuleV1Alpha struct { + Type string `json:"type" yaml:"type"` + MatchMode string `json:"match_mode" yaml:"match_mode"` + Pattern string `json:"pattern" yaml:"pattern"` +} + +type HistoryRequestFiltersV1Alpha struct { + CursorType string `json:"cursor_type,omitempty" yaml:"cursor_type,omitempty"` + CursorValue string `json:"cursor_value,omitempty" yaml:"cursor_value,omitempty"` + GitRefType string `json:"git_ref_type,omitempty" yaml:"git_ref_type,omitempty"` + GitRefLabel string `json:"git_ref_label,omitempty" yaml:"git_ref_label,omitempty"` + TriggeredBy string `json:"triggered_by,omitempty" yaml:"triggered_by,omitempty"` + Parameter1 string `json:"parameter1,omitempty" yaml:"parameter1,omitempty"` + Parameter2 string `json:"parameter2,omitempty" yaml:"parameter2,omitempty"` + Parameter3 string `json:"parameter3,omitempty" yaml:"parameter3,omitempty"` +} + +type DeploymentTargetEnvVarV1Alpha struct { + Name string `json:"name" yaml:"name"` + Value HashedContent `json:"value" yaml:"value"` +} + +type DeploymentTargetFileV1Alpha struct { + Path string `json:"path" yaml:"path"` + Content HashedContent `json:"content" yaml:"content"` + Source string `json:"-" yaml:"source"` +} + +type HashedContent string + +type DeploymentTargetFilesV1Alpha []*DeploymentTargetFileV1Alpha +type DeploymentTargetEnvVarsV1Alpha []*DeploymentTargetEnvVarV1Alpha + +type DeploymentTargetCreateRequestV1Alpha struct { + DeploymentTargetV1Alpha + UniqueToken string `json:"unique_token" yaml:"-"` +} + +type DeploymentTargetUpdateRequestV1Alpha DeploymentTargetCreateRequestV1Alpha + +type CordonResponseV1Alpha struct { + TargetId string `json:"target_id,omitempty" yaml:"target_id,omitempty"` + Cordoned bool `json:"cordoned,omitempty" yaml:"cordoned,omitempty"` +} + +func NewDeploymentTargetV1AlphaFromJson(data []byte) (*DeploymentTargetV1Alpha, error) { + dt := DeploymentTargetV1Alpha{} + err := json.Unmarshal(data, &dt) + + if err != nil { + return nil, err + } + + dt.setApiVersionAndKind() + + return &dt, nil +} + +func NewDeploymentTargetV1AlphaFromYaml(data []byte) (*DeploymentTargetV1Alpha, error) { + dt := DeploymentTargetV1Alpha{} + + err := yaml.UnmarshalStrict(data, &dt) + + if err != nil { + return nil, err + } + + dt.setApiVersionAndKind() + + return &dt, nil +} + +func (dt *DeploymentTargetV1Alpha) ToYaml() ([]byte, error) { + return yaml.Marshal(dt) +} + +func (dt *DeploymentTargetV1Alpha) ToJson() ([]byte, error) { + return json.Marshal(dt) +} + +func (dt *DeploymentTargetV1Alpha) setApiVersionAndKind() { + dt.ApiVersion = "v1alpha" + dt.Kind = DeploymentTargetKindV1Alpha +} + +func (dt *DeploymentTargetV1Alpha) LoadFiles() error { + if dt.Files == nil { + return nil + } + return dt.Files.Load() +} + +func NewDeploymentTargetListV1AlphaFromJson(data []byte) (*DeploymentTargetListV1Alpha, error) { + targetList := DeploymentTargetListV1Alpha{} + + err := json.Unmarshal(data, &targetList) + + if err != nil { + return nil, err + } + for i := range targetList { + targetList[i].setApiVersionAndKind() + } + return &targetList, nil +} + +func NewCordonResponseV1AlphaFromJson(data []byte) (*CordonResponseV1Alpha, error) { + cordonResponse := CordonResponseV1Alpha{} + + err := json.Unmarshal(data, &cordonResponse) + + if err != nil { + return nil, err + } + + return &cordonResponse, nil +} + +func NewDeploymentsHistoryV1AlphaFromJson(data []byte) (*DeploymentsHistoryV1Alpha, error) { + deployments := DeploymentsHistoryV1Alpha{} + + err := json.Unmarshal(data, &deployments) + + if err != nil { + return nil, err + } + + return &deployments, nil +} + +func (d *DeploymentsHistoryV1Alpha) ToYaml() ([]byte, error) { + return yaml.Marshal(d) +} + +func (c *HashedContent) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + *c = HashedContent(strings.TrimSuffix(string(s), " [md5]")) + return nil +} + +func (c HashedContent) MarshalYAML() (data interface{}, err error) { + return fmt.Sprintf("%s [md5]", c), nil +} + +func (r DeploymentTargetCreateRequestV1Alpha) ObjectName() string { + return fmt.Sprintf("%s/%s", DeploymentTargetKindV1Alpha, r.Name) +} + +func (r DeploymentTargetUpdateRequestV1Alpha) ObjectName() string { + return fmt.Sprintf("%s/%s", DeploymentTargetKindV1Alpha, r.Name) +} + +func (r *DeploymentTargetCreateRequestV1Alpha) ToYaml() ([]byte, error) { + return yaml.Marshal(r) +} + +func (r *DeploymentTargetCreateRequestV1Alpha) ToJson() ([]byte, error) { + return json.Marshal(r) +} + +func (r *DeploymentTargetCreateRequestV1Alpha) LoadFiles() error { + if r.Files == nil { + return nil + } + return r.Files.Load() +} + +func (r *DeploymentTargetUpdateRequestV1Alpha) LoadFiles() error { + if r.Files == nil { + return nil + } + return r.Files.Load() +} + +func (f *DeploymentTargetFilesV1Alpha) Load() error { + for _, file := range *f { + if file == nil { + continue + } + if file.Content != "" { + continue + } + err := file.LoadContent() + if err != nil { + return err + } + } + return nil +} + +func NewDeploymentTargetCreateRequestV1AlphaFromYaml(data []byte) (*DeploymentTargetCreateRequestV1Alpha, error) { + deploymentTarget := DeploymentTargetCreateRequestV1Alpha{} + + err := yaml.UnmarshalStrict(data, &deploymentTarget) + + if err != nil { + return nil, err + } + + deploymentTarget.setApiVersionAndKind() + + return &deploymentTarget, nil +} + +func NewDeploymentTargetUpdateRequestV1AlphaFromYaml(data []byte) (*DeploymentTargetUpdateRequestV1Alpha, error) { + deploymentTargetUpdate := DeploymentTargetUpdateRequestV1Alpha{} + + err := yaml.UnmarshalStrict(data, &deploymentTargetUpdate) + + if err != nil { + return nil, err + } + + deploymentTargetUpdate.setApiVersionAndKind() + if err = deploymentTargetUpdate.LoadFiles(); err != nil { + return nil, err + } + + return &deploymentTargetUpdate, nil +} + +func (f *DeploymentTargetFileV1Alpha) LoadContent() error { + content, err := ioutil.ReadFile(f.Source) + if err != nil { + return err + } + + f.Content = HashedContent(base64.StdEncoding.EncodeToString(content)) + return nil +} + +func (hr HistoryRequestFiltersV1Alpha) ToURLValues() (values url.Values, err error) { + data, err := json.Marshal(hr) + if err != nil { + return nil, err + } + var m map[string]string + err = json.Unmarshal(data, &m) + if err != nil { + return nil, err + } + values = url.Values{} + for k, v := range m { + values.Add(k, v) + } + return +} diff --git a/api/models/deployment_target_v1_alpha_test.go b/api/models/deployment_target_v1_alpha_test.go new file mode 100644 index 0000000..463e299 --- /dev/null +++ b/api/models/deployment_target_v1_alpha_test.go @@ -0,0 +1,137 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDeploymentTargetsToJson(t *testing.T) { + dt := DeploymentTargetV1Alpha{ + ApiVersion: "v1alpha", + Kind: DeploymentTargetKindV1Alpha, + DeploymentTargetMetadataV1Alpha: DeploymentTargetMetadataV1Alpha{ + Id: "1234-5678-id", + Name: "dt-name", + OrganizationId: "org-id", + ProjectId: "prj-id", + Description: "dt-description", + Url: "www.semaphore.xyz", + }, + DeploymentTargetSpecV1Alpha: DeploymentTargetSpecV1Alpha{ + Active: false, + BookmarkParameter1: "book1", + SubjectRules: []*SubjectRuleV1Alpha{{ + Type: "USER", + SubjectId: "00000000-0000-0000-0000-000000000000", + }}, + ObjectRules: []*ObjectRuleV1Alpha{ + {Type: "BRANCH", MatchMode: "EXACT", Pattern: ".*main.*"}, + }, + State: "CORDONED", + }, + } + + received_json, err := dt.ToJson() + assert.Nil(t, err) + + expected_json := `{"id":"1234-5678-id","name":"dt-name","project_id":"prj-id","organization_id":"org-id","description":"dt-description","url":"www.semaphore.xyz","state":"CORDONED","state_message":"","subject_rules":[{"type":"USER","subject_id":"00000000-0000-0000-0000-000000000000"}],"object_rules":[{"type":"BRANCH","match_mode":"EXACT","pattern":".*main.*"}],"active":false,"bookmark_parameter1":"book1","bookmark_parameter2":"","bookmark_parameter3":""}` + assert.Equal(t, expected_json, string(received_json)) +} + +func TestDeploymentTargetsToYaml(t *testing.T) { + dt := DeploymentTargetV1Alpha{ + ApiVersion: "v1alpha", + Kind: DeploymentTargetKindV1Alpha, + DeploymentTargetMetadataV1Alpha: DeploymentTargetMetadataV1Alpha{ + Id: "1234-5678-id", + Name: "dt-name", + OrganizationId: "org-id", + ProjectId: "prj-id", + Description: "dt-description", + Url: "www.semaphore.xyz", + }, + DeploymentTargetSpecV1Alpha: DeploymentTargetSpecV1Alpha{ + Active: true, + BookmarkParameter1: "book1", + SubjectRules: []*SubjectRuleV1Alpha{{ + Type: "USER", + SubjectId: "subjId1", + }}, + ObjectRules: []*ObjectRuleV1Alpha{ + {Type: "BRANCH", MatchMode: "EXACT", Pattern: ".*main.*"}, + }, + State: "USABLE", + }, + } + received_yaml, err := dt.ToYaml() + assert.Nil(t, err) + + expected_yaml := `apiVersion: v1alpha +kind: DeploymentTarget +metadata: + id: 1234-5678-id + name: dt-name + project_id: prj-id + organization_id: org-id + description: dt-description + url: www.semaphore.xyz +spec: + state: USABLE + state_message: "" + subject_rules: + - type: USER + subject_id: subjId1 + object_rules: + - type: BRANCH + match_mode: EXACT + pattern: .*main.* + active: true + bookmark_parameter1: book1 + bookmark_parameter2: "" + bookmark_parameter3: "" +` + assert.Equal(t, expected_yaml, string(received_yaml)) +} + +func TestDeploymentTargetsFromYaml(t *testing.T) { + content := `apiVersion: v1alpha +kind: DeploymentTarget +metadata: + id: 1234-5678-id + name: dt-name + organization_id: org-id + project_id: prj-id + url: www.semaphore.xyz + description: dt-description +spec: + active: true + bookmark_parameter1: book1 +` + dt, err := NewDeploymentTargetV1AlphaFromYaml([]byte(content)) + assert.Nil(t, err) + + assert.Equal(t, dt.Id, "1234-5678-id") + assert.Equal(t, dt.Name, "dt-name") + assert.Equal(t, dt.ProjectId, "prj-id") + assert.Equal(t, dt.OrganizationId, "org-id") + assert.Equal(t, dt.Url, "www.semaphore.xyz") + assert.Equal(t, dt.Description, "dt-description") + assert.True(t, dt.Active) + assert.Equal(t, dt.BookmarkParameter1, "book1") +} + +func TestDeploymentTargetsFromJSON(t *testing.T) { + content := `{"id":"1234-5678-id","name":"dt-name","project_id":"prj-id","organization_id":"org-id","description":"dt-description","url":"www.semaphore.xyz","state":"USABLE","subject_rules":[{"type":"USER","subject_id":"00000000-0000-0000-0000-000000000000"}],"object_rules":[{"type":"BRANCH","match_mode":"REGEX","pattern":".*main.*"}],"active":true,"bookmark_parameter1":"book1","env_vars":[{"name":"Var1","value":"Val1"}],"files":[{"path":"/etc/config.yml","content":"abcdefgh"}]}` + dt, err := NewDeploymentTargetV1AlphaFromJson([]byte(content)) + assert.Nil(t, err) + + assert.Equal(t, dt.Id, "1234-5678-id") + assert.Equal(t, dt.Name, "dt-name") + assert.Equal(t, dt.ProjectId, "prj-id") + assert.Equal(t, dt.OrganizationId, "org-id") + assert.Equal(t, dt.Url, "www.semaphore.xyz") + assert.Equal(t, dt.Description, "dt-description") + assert.True(t, dt.Active) + assert.Equal(t, dt.BookmarkParameter1, "book1") +} diff --git a/api/models/job_v1_alpha.go b/api/models/job_v1_alpha.go index 20ab1fe..c246bac 100644 --- a/api/models/job_v1_alpha.go +++ b/api/models/job_v1_alpha.go @@ -18,21 +18,21 @@ type JobV1AlphaMetadata struct { } type JobV1AlphaSpecSecret struct { - Name string `json:"name,omitempty" yaml: "name,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` } type JobV1AlphaSpecFile struct { - Path string `json:"path,omitempty" yaml: "path,omitempty"` - Content string `json:"content,omitempty" yaml: "content,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Content string `json:"content,omitempty" yaml:"content,omitempty"` } type JobV1AlphaSpecEnvVar struct { - Name string `json:"name,omitempty" yaml: "name,omitempty"` - Value string `json:"value,omitempty" yaml: "value,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Value string `json:"value,omitempty" yaml:"value,omitempty"` } type JobV1AlphaAgentImagePullSecret struct { - Name string `json:"name,omitempty" yaml: "name,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` } type JobV1AlphaAgentMachine struct { @@ -44,21 +44,21 @@ type JobV1AlphaAgentContainer struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` Image string `json:"image,omitempty" yaml:"image,omitempty"` Command string `json:"command,omitempty" yaml:"command,omitempty"` - EnvVars []JobV1AlphaSpecEnvVar `json:"env_vars,omitempty" yaml: "env_vars,omitempty"` - Secrets []JobV1AlphaSpecSecret `json:"secrets,omitempty" yaml: "secrets,omitempty"` + EnvVars []JobV1AlphaSpecEnvVar `json:"env_vars,omitempty" yaml:"env_vars,omitempty"` + Secrets []JobV1AlphaSpecSecret `json:"secrets,omitempty" yaml:"secrets,omitempty"` } type JobV1AlphaAgent struct { Machine JobV1AlphaAgentMachine `json:"machine,omitempty" yaml:"machine,omitempty"` Containers []JobV1AlphaAgentContainer `json:"containers,omitempty" yaml:"containers,omitempty"` - ImagePullSecrets []JobV1AlphaAgentImagePullSecret `json:"image_pull_secrets,omitempty" yaml: "image_pull_secrets,omitempty"` + ImagePullSecrets []JobV1AlphaAgentImagePullSecret `json:"image_pull_secrets,omitempty" yaml:"image_pull_secrets,omitempty"` } type JobV1AlphaSpec struct { Agent JobV1AlphaAgent `json:"agent,omitempty" yaml:"agent,omitempty"` - Files []JobV1AlphaSpecFile `json:"files,omitempty" yaml: "files,omitempty"` - EnvVars []JobV1AlphaSpecEnvVar `json:"env_vars,omitempty" yaml: "env_vars,omitempty"` - Secrets []JobV1AlphaSpecSecret `json:"secrets,omitempty" yaml: "secrets,omitempty"` + Files []JobV1AlphaSpecFile `json:"files,omitempty" yaml:"files,omitempty"` + EnvVars []JobV1AlphaSpecEnvVar `json:"env_vars,omitempty" yaml:"env_vars,omitempty"` + Secrets []JobV1AlphaSpecSecret `json:"secrets,omitempty" yaml:"secrets,omitempty"` Commands []string `json:"commands,omitempty" yaml:"commands,omitempty"` EpilogueAlwaysCommands []string `json:"epilogue_always_commands,omitempty" yaml:"epilogue_always_commands,omitempty"` EpilogueOnPassCommands []string `json:"epilogue_on_pass_commands,omitempty" yaml:"epilogue_on_pass_commands,omitempty"` diff --git a/api/models/project_secret_v1.go b/api/models/project_secret_v1.go index edbff7b..15ae006 100644 --- a/api/models/project_secret_v1.go +++ b/api/models/project_secret_v1.go @@ -27,15 +27,15 @@ type ProjectSecretV1File struct { type ProjectSecretV1Data struct { EnvVars []ProjectSecretV1EnvVar `json:"env_vars" yaml:"env_vars"` - Files []ProjectSecretV1File `json:"files" yaml: "files"` + Files []ProjectSecretV1File `json:"files" yaml:"files"` } type ProjectSecretV1Metadata struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Id string `json:"id,omitempty" yaml:"id,omitempty"` - CreateTime json.Number `json:"create_time,omitempty,string" yaml:"create_time,omitempty"` - UpdateTime json.Number `json:"update_time,omitempty,string" yaml:"update_time,omitempty"` - ProjectIdOrName string `json:"project_id_or_name,omitempty" yaml:"project_id_or_name,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Id string `json:"id,omitempty" yaml:"id,omitempty"` + CreateTime json.Number `json:"create_time,omitempty,string" yaml:"create_time,omitempty"` + UpdateTime json.Number `json:"update_time,omitempty,string" yaml:"update_time,omitempty"` + ProjectIdOrName string `json:"project_id_or_name,omitempty" yaml:"project_id_or_name,omitempty"` } func NewProjectSecretV1(name string, envVars []ProjectSecretV1EnvVar, files []ProjectSecretV1File) ProjectSecretV1 { diff --git a/api/models/secret_v1_beta.go b/api/models/secret_v1_beta.go index f2dc194..7a10c31 100644 --- a/api/models/secret_v1_beta.go +++ b/api/models/secret_v1_beta.go @@ -11,8 +11,8 @@ type SecretV1Beta struct { ApiVersion string `json:"apiVersion,omitempty" yaml:"apiVersion"` Kind string `json:"kind,omitempty" yaml:"kind"` - Metadata SecretV1BetaMetadata `json:"metadata" yaml:"metadata"` - Data SecretV1BetaData `json:"data" yaml:"data"` + Metadata SecretV1BetaMetadata `json:"metadata" yaml:"metadata"` + Data SecretV1BetaData `json:"data" yaml:"data"` OrgConfig *SecretV1BetaOrgConfig `json:"org_config,omitempty" yaml:"org_config,omitempty"` } @@ -28,7 +28,7 @@ type SecretV1BetaFile struct { type SecretV1BetaData struct { EnvVars []SecretV1BetaEnvVar `json:"env_vars" yaml:"env_vars"` - Files []SecretV1BetaFile `json:"files" yaml: "files"` + Files []SecretV1BetaFile `json:"files" yaml:"files"` } type SecretV1BetaMetadata struct { @@ -39,10 +39,10 @@ type SecretV1BetaMetadata struct { } type SecretV1BetaOrgConfig struct { - Projects_access string `json:"projects_access,omitempty" yaml:"projects_access,omitempty"` - Project_ids []string `json:"project_ids,omitempty" yaml:"project_ids,omitempty"` - Debug_access string `json:"debug_access,omitempty" yaml:"debug_access,omitempty"` - Attach_access string `json:"attach_access,omitempty" yaml:"attach_access,omitempty"` + Projects_access string `json:"projects_access,omitempty" yaml:"projects_access,omitempty"` + Project_ids []string `json:"project_ids,omitempty" yaml:"project_ids,omitempty"` + Debug_access string `json:"debug_access,omitempty" yaml:"debug_access,omitempty"` + Attach_access string `json:"attach_access,omitempty" yaml:"attach_access,omitempty"` } func NewSecretV1Beta(name string, envVars []SecretV1BetaEnvVar, files []SecretV1BetaFile) SecretV1Beta { diff --git a/api/uuid/uuid.go b/api/uuid/uuid.go new file mode 100644 index 0000000..eb981c7 --- /dev/null +++ b/api/uuid/uuid.go @@ -0,0 +1,42 @@ +package uuid + +import "github.com/google/uuid" + +type generator func() (uuid.UUID, error) + +var googleGenerator = uuid.NewUUID +var googleGeneratorV4 = uuid.NewRandom +var currentGenerator generator +var currentGeneratorV4 generator + +func NewUUID() (uuid.UUID, error) { + return currentGenerator() +} + +func NewUUIDv4() (uuid.UUID, error) { + return currentGeneratorV4() +} + +func mockGenerator() (uuid.UUID, error) { + return [16]byte{0, 2, 4, 6, 9, 11, 78, 16, 147, 21, 24, 26, 28, 30, 32, 34}, nil +} + +func IsValid(s string) bool { + _, err := uuid.Parse(s) + return err == nil +} + +func Mock() { + currentGenerator = mockGenerator + currentGeneratorV4 = mockGenerator +} + +func Unmock() { + currentGenerator = googleGenerator + currentGeneratorV4 = googleGeneratorV4 +} + +func init() { + currentGenerator = googleGenerator + currentGeneratorV4 = googleGeneratorV4 +} diff --git a/cmd/apply.go b/cmd/apply.go index a4f8ce0..ed19afb 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "io/ioutil" @@ -90,7 +91,7 @@ func RunApply(cmd *cobra.Command, args []string) { fmt.Printf("Notification '%s' updated.\n", notif.Metadata.Name) case "Project": - proj , err := models.NewProjectV1AlphaFromYaml(data) + proj, err := models.NewProjectV1AlphaFromYaml(data) utils.Check(err) @@ -101,6 +102,23 @@ func RunApply(cmd *cobra.Command, args []string) { utils.Check(err) fmt.Printf("Project '%s' updated.\n", proj.Metadata.Name) + case models.DeploymentTargetKindV1Alpha: + target, err := models.NewDeploymentTargetV1AlphaFromYaml(data) + utils.Check(err) + if target == nil { + utils.Check(errors.New("deployment target in the file is empty")) + return + } + updateRequest := &models.DeploymentTargetUpdateRequestV1Alpha{ + DeploymentTargetV1Alpha: *target, + } + utils.Check(updateRequest.LoadFiles()) + + c := client.NewDeploymentTargetsV1AlphaApi() + updatedDeploymentTarget, err := c.Update(updateRequest) + utils.Check(err) + + fmt.Printf("Deployment target '%s' ('%s') updated.\n", updatedDeploymentTarget.Id, updatedDeploymentTarget.Name) default: utils.Fail(fmt.Sprintf("Unsuported resource kind '%s'", kind)) } diff --git a/cmd/apply_test.go b/cmd/apply_test.go index 66e5e07..827d072 100644 --- a/cmd/apply_test.go +++ b/cmd/apply_test.go @@ -6,6 +6,7 @@ import ( "testing" models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" assert "github.com/stretchr/testify/assert" httpmock "gopkg.in/jarcoal/httpmock.v1" ) @@ -171,3 +172,58 @@ spec: t.Errorf("Expected the API to receive PATCH project with: %s, got: %s", expected, received) } } + +func Test__ApplyDeploymentTarget__FromYaml_Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + uuid.Mock() + defer uuid.Unmock() + + yaml_file := ` +apiVersion: v1alpha +kind: DeploymentTarget +metadata: + name: TestDT + id: a13949b7-b2f6-4286-8f26-3962d7e97828 + url: someurl321.zyx + project_id: a13949b7-b2f6-4286-8f26-000000000000 +spec: + subject_rules: + - type: ANY + - type: USER + subject_id: 00000000-0000-0000-0000-000000000000 + object_rules: + - type: BRANCH + match_mode: ALL + - type: TAG + match_mode: ALL + bookmark_parameter1: "book 1" + env_vars: + - name: X + value: 123 +` + + yaml_file_path := "/tmp/apply_dt.yaml" + ioutil.WriteFile(yaml_file_path, []byte(yaml_file), 0644) + + received := "" + + httpmock.RegisterResponder("PATCH", "https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/a13949b7-b2f6-4286-8f26-3962d7e97828", + func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + + received = string(body) + + return httpmock.NewStringResponse(200, received), nil + }, + ) + + RootCmd.SetArgs([]string{"apply", "-f", yaml_file_path}) + RootCmd.Execute() + + expected := `{"id":"a13949b7-b2f6-4286-8f26-3962d7e97828","name":"TestDT","project_id":"a13949b7-b2f6-4286-8f26-000000000000","organization_id":"","description":"","url":"someurl321.zyx","state":"","state_message":"","subject_rules":[{"type":"ANY"},{"type":"USER","subject_id":"00000000-0000-0000-0000-000000000000"}],"object_rules":[{"type":"BRANCH","match_mode":"ALL","pattern":""},{"type":"TAG","match_mode":"ALL","pattern":""}],"active":false,"bookmark_parameter1":"book 1","bookmark_parameter2":"","bookmark_parameter3":"","env_vars":[{"name":"X","value":"123"}],"unique_token":"00020406-090b-4e10-9315-181a1c1e2022"}` + + if received != expected { + t.Errorf("Expected the API to receive PATCH deployment target with: %s, got: %s", expected, received) + } +} diff --git a/cmd/create.go b/cmd/create.go index 9fd9ef8..5295939 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/base64" + "errors" "fmt" "io/ioutil" "log" @@ -28,9 +29,7 @@ var createCmd = &cobra.Command{ data, err := ioutil.ReadFile(path) utils.CheckWithMessage(err, "Failed to read from resource file.") - _, kind, err := utils.ParseYamlResourceHeaders(data) - utils.Check(err) switch kind { @@ -117,6 +116,24 @@ var createCmd = &cobra.Command{ y, err := newAgentType.ToYaml() utils.Check(err) fmt.Printf("%s", y) + case models.DeploymentTargetKindV1Alpha: + target, err := models.NewDeploymentTargetV1AlphaFromYaml(data) + utils.Check(err) + if target == nil { + utils.Check(errors.New("deployment target in the file is empty")) + return + } + createRequest := &models.DeploymentTargetCreateRequestV1Alpha{ + DeploymentTargetV1Alpha: *target, + } + utils.Check(createRequest.LoadFiles()) + c := client.NewDeploymentTargetsV1AlphaApi() + createdDeploymentTarget, err := c.Create(createRequest) + utils.Check(err) + + y, err := createdDeploymentTarget.ToYaml() + utils.Check(err) + fmt.Printf("Deployment target '%s' (%s) created:\n%s\n", createdDeploymentTarget.Id, createdDeploymentTarget.Name, y) default: utils.Fail(fmt.Sprintf("Unsupported resource kind '%s'", kind)) } @@ -230,6 +247,7 @@ func init() { createCmd.AddCommand(CreateWorkflowCmd) createCmd.AddCommand(createNotificationCmd) createCmd.AddCommand(CreateAgentTypeCmd) + createCmd.AddCommand(NewCreateDeploymentTargetCmd()) // Create Flags diff --git a/cmd/create_target.go b/cmd/create_target.go new file mode 100644 index 0000000..e737f9d --- /dev/null +++ b/cmd/create_target.go @@ -0,0 +1,204 @@ +package cmd + +import ( + "errors" + "fmt" + "regexp" + "strings" + + client "github.com/semaphoreci/cli/api/client" + models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/cmd/utils" + "github.com/spf13/cobra" +) + +var reMatchEnvVarPattern = regexp.MustCompile(`^.+=.+$`) +var reMatchFilePattern = regexp.MustCompile(`^[^: ]+:[^: ]+$`) + +func NewCreateDeploymentTargetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "deployment_targets [NAME]", + Short: "Create a deployment target.", + Long: ``, + Aliases: models.DeploymentTargetCmdAliases, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + createRequest := models.DeploymentTargetCreateRequestV1Alpha{} + createRequest.ProjectId = GetProjectID(cmd) + + fileFlags, err := cmd.Flags().GetStringArray("file") + utils.Check(err) + for _, fileFlag := range fileFlags { + if !reMatchFilePattern.MatchString(fileFlag) { + utils.Fail("The format of --file flag must be: :") + } + + flagPaths := strings.Split(fileFlag, ":") + + targetFile := &models.DeploymentTargetFileV1Alpha{ + Source: flagPaths[0], + Path: flagPaths[1], + } + err := targetFile.LoadContent() + utils.Check(err) + if createRequest.Files == nil { + createRequest.Files = &models.DeploymentTargetFilesV1Alpha{} + } + *createRequest.Files = append(*createRequest.Files, targetFile) + } + + envFlags, err := cmd.Flags().GetStringArray("env") + utils.Check(err) + for _, envFlag := range envFlags { + if !reMatchEnvVarPattern.MatchString(envFlag) { + utils.Fail("The format of -e flag must be: =") + } + + parts := strings.SplitN(envFlag, "=", 2) + if createRequest.EnvVars == nil { + createRequest.EnvVars = &models.DeploymentTargetEnvVarsV1Alpha{} + } + *createRequest.EnvVars = append(*createRequest.EnvVars, &models.DeploymentTargetEnvVarV1Alpha{ + Name: parts[0], + Value: models.HashedContent(parts[1]), + }) + } + + createRequest.Name = args[0] + createRequest.Description, err = cmd.Flags().GetString("desc") + utils.Check(err) + createRequest.Url, err = cmd.Flags().GetString("url") + utils.Check(err) + bookmarks, err := cmd.Flags().GetStringArray("bookmark") + utils.Check(err) + for i, bookmark := range bookmarks { + switch i { + case 0: + createRequest.BookmarkParameter1 = bookmark + case 1: + createRequest.BookmarkParameter2 = bookmark + case 2: + createRequest.BookmarkParameter3 = bookmark + } + } + + parsedSubjectRules, err := utils.CSVArrayFlag(cmd, "subject-rule", true) + utils.Check(err) + + createSubjectRule := func(parsedSubjRule []string) (*models.SubjectRuleV1Alpha, error) { + if len(parsedSubjRule) == 0 || len(parsedSubjRule) > 2 { + return nil, fmt.Errorf("invalid subject rule: %q, must be ANY or AUTO or in format TYPE,SUBJECT", parsedSubjRule) + } + rule := &models.SubjectRuleV1Alpha{ + Type: strings.ToUpper(strings.TrimSpace(parsedSubjRule[0])), + } + switch rule.Type { + case "ANY", "AUTO": + return rule, nil + case "USER": + if len(parsedSubjRule) == 2 { + rule.GitLogin = parsedSubjRule[1] + } else { + return nil, errors.New("invalid user subject rule: must be in format USER,GIT_LOGIN") + } + case "ROLE": + if len(parsedSubjRule) == 2 { + rule.SubjectId = parsedSubjRule[1] + } else { + return nil, errors.New(`invalid role subject rule: must be in format ROLE,ROLE_ID`) + } + default: + return nil, fmt.Errorf(`invalid subject rule type: %s, must be one of: ANY, USER, ROLE, AUTO`, rule.Type) + } + return rule, nil + } + for _, parsedSubjectRule := range parsedSubjectRules { + subjectRule, err := createSubjectRule(parsedSubjectRule) + utils.Check(err) + + createRequest.SubjectRules = append(createRequest.SubjectRules, subjectRule) + } + + objectRulesStrs, err := utils.CSVArrayFlag(cmd, "object-rule", true) + utils.Check(err) + + createObjectRule := func(parsedObjRule []string) (*models.ObjectRuleV1Alpha, error) { + if len(parsedObjRule) == 0 || len(parsedObjRule) > 3 { + return nil, fmt.Errorf("invalid object rule: %q, must be PR or TYPE,ALL or TYPE,MODE,PATTERN", parsedObjRule) + } + rule := &models.ObjectRuleV1Alpha{ + Type: strings.ToUpper(strings.TrimSpace(parsedObjRule[0])), + MatchMode: models.ObjectRuleMatchModeAllV1Alpha, + } + switch rule.Type { + case models.ObjectRuleTypePullRequestV1Alpha: + return rule, nil + case models.ObjectRuleTypeBranchV1Alpha, models.ObjectRuleTypeTagV1Alpha: + if len(parsedObjRule) == 1 { + err := fmt.Errorf("invalid object rule: must be %s,ALL or %s,EXACT, or %s,REGEX,", rule.Type, rule.Type, rule.Type) + return nil, err + } + matchMode := strings.ToUpper(strings.TrimSpace(parsedObjRule[1])) + switch matchMode { + case models.ObjectRuleMatchModeAllV1Alpha: + return rule, nil + case models.ObjectRuleMatchModeRegexV1Alpha, models.ObjectRuleMatchModeExactV1Alpha: + if len(parsedObjRule) == 2 { + if matchMode == models.ObjectRuleMatchModeRegexV1Alpha { + return nil, fmt.Errorf("invalid object rule: must be %s,REGEX,", rule.Type) + } + return nil, fmt.Errorf("invalid object rule: must be %s,EXACT,", rule.Type) + } + rule.MatchMode = matchMode + rule.Pattern = parsedObjRule[2] + return rule, nil + default: + return nil, fmt.Errorf("invalid object rule match mode: %s, must be ALL, EXACT or REGEX", matchMode) + } + default: + return nil, fmt.Errorf("invalid object rule type: %s, must be BRANCH, TAG or PR", rule.Type) + } + } + for _, parsedObjectRule := range objectRulesStrs { + objectRule, err := createObjectRule(parsedObjectRule) + utils.Check(err) + + createRequest.ObjectRules = append(createRequest.ObjectRules, objectRule) + } + + c := client.NewDeploymentTargetsV1AlphaApi() + createdTarget, err := c.Create(&createRequest) + utils.Check(err) + if createdTarget == nil { + utils.Check(errors.New("created target must not be nil")) + return + } + + fmt.Printf("Deployment target '%s' ('%s') created.\n", createdTarget.Id, createdTarget.Name) + }, + } + + cmd.Flags().StringP("desc", "d", "", "Description of deployment target") + cmd.Flags().StringP("url", "u", "", "URL of deployment target") + + cmd.Flags().StringArrayP( + "file", + "f", + []string{}, + "File mapping :, used to create a secret with file", + ) + + cmd.Flags().StringArrayP( + "env", + "e", + []string{}, + "Environment Variables given in the format VAR=VALUE", + ) + cmd.Flags().StringArrayP("bookmark", "b", []string{}, "Bookmarks for deployment target") + cmd.Flags().StringArrayP("subject-rule", "s", []string{}, "Subject rules for deployment target") + cmd.Flags().StringArrayP("object-rule", "o", []string{}, "Object rules for deployment target") + cmd.Flags().StringP("project-name", "p", "", "project name; if not specified will be inferred from git origin") + cmd.Flags().StringP("project-id", "i", "", "project id; if not specified will be inferred from git origin") + + return cmd +} diff --git a/cmd/create_target_test.go b/cmd/create_target_test.go new file mode 100644 index 0000000..fd01bb3 --- /dev/null +++ b/cmd/create_target_test.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/semaphoreci/cli/api/uuid" + assert "github.com/stretchr/testify/assert" + httpmock "gopkg.in/jarcoal/httpmock.v1" +) + +func Test__CreateDeploymentTarget__WithSubcommand__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + uuid.Mock() + defer uuid.Unmock() + + content1 := "This is some docker config" + content2 := "This is some gcloud config" + + ioutil.WriteFile("/tmp/docker", []byte(content1), 0644) + ioutil.WriteFile("/tmp/gcloud", []byte(content2), 0644) + + received := "" + + httpmock.RegisterResponder("POST", "https://org.semaphoretext.xyz/api/v1alpha/deployment_targets", + func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + + received = string(body) + + return httpmock.NewStringResponse(200, received), nil + }, + ) + + RootCmd.SetArgs([]string{ + "create", + "dt", + "-i", "00000000-0000-0000-000000000000", + "-e", "FOO=BAR", + "--env", "ZEZ=Hello World", + "--file", "/tmp/docker:.config/docker", + "-f", "/tmp/gcloud:.config/gcloud", + "-s", "any", + "--subject-rule", "user,mock_user_321", + "-s", "role,contributor", + "--object-rule", "branch,exact,main", + "-o", `tag,regex,.*feat.*`, + "-b", "book 1", + "--url", "mock_url_321.zyx", + "abc", + }) + + RootCmd.Execute() + + file1 := base64.StdEncoding.EncodeToString([]byte(content1)) + file2 := base64.StdEncoding.EncodeToString([]byte(content2)) + + expected := fmt.Sprintf(`{"id":"","name":"abc","project_id":"00000000-0000-0000-000000000000","organization_id":"","description":"","url":"mock_url_321.zyx","state":"","state_message":"","subject_rules":[{"type":"ANY"},{"type":"USER","git_login":"mock_user_321"},{"type":"ROLE","subject_id":"contributor"}],"object_rules":[{"type":"BRANCH","match_mode":"EXACT","pattern":"main"},{"type":"TAG","match_mode":"REGEX","pattern":".*feat.*"}],"active":false,"bookmark_parameter1":"book 1","bookmark_parameter2":"","bookmark_parameter3":"","env_vars":[{"name":"FOO","value":"BAR"},{"name":"ZEZ","value":"Hello World"}],"files":[{"path":".config/docker","content":"%s"},{"path":".config/gcloud","content":"%s"}],"unique_token":"00020406-090b-4e10-9315-181a1c1e2022"}`, file1, file2) + + assert.Equal(t, received, expected) +} diff --git a/cmd/create_test.go b/cmd/create_test.go index 9b3638f..22c000d 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -9,6 +9,7 @@ import ( httpmock "gopkg.in/jarcoal/httpmock.v1" models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" ) func Test__CreateProject__FromYaml__Response200(t *testing.T) { @@ -262,3 +263,47 @@ func Test__CreateAgentType__Response200(t *testing.T) { t.Errorf("Expected the API to receive POST self_hosted_agent_types with: %s, got: %s", expected, received) } } + +func Test__CreateDeploymentTarget__FromYaml__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + uuid.Mock() + defer uuid.Unmock() + + yaml_file := ` +apiVersion: v1alpha +kind: DeploymentTarget +metadata: + name: dt-name-from-yaml + organization_id: org-id + project_id: prj-id + url: www.semaphore.xyz + description: dt-description +spec: + bookmark_parameter1: book1 +` + + yaml_file_path := "/tmp/create_dt.yaml" + + ioutil.WriteFile(yaml_file_path, []byte(yaml_file), 0644) + + received := "" + + httpmock.RegisterResponder(http.MethodPost, "https://org.semaphoretext.xyz/api/v1alpha/deployment_targets", + func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + + received = string(body) + + return httpmock.NewStringResponse(200, received), nil + }, + ) + + RootCmd.SetArgs([]string{"create", "-f", yaml_file_path}) + RootCmd.Execute() + + expected := `{"id":"","name":"dt-name-from-yaml","project_id":"prj-id","organization_id":"org-id","description":"dt-description","url":"www.semaphore.xyz","state":"","state_message":"","subject_rules":null,"object_rules":null,"active":false,"bookmark_parameter1":"book1","bookmark_parameter2":"","bookmark_parameter3":"","unique_token":"00020406-090b-4e10-9315-181a1c1e2022"}` + if received != expected { + t.Errorf("Expected the API to receive POST deployment_targets: %s, got: %s", expected, received) + } +} diff --git a/cmd/debug_project.go b/cmd/debug_project.go index 9aa0dd4..5b56aad 100644 --- a/cmd/debug_project.go +++ b/cmd/debug_project.go @@ -2,10 +2,11 @@ package cmd import ( "fmt" - "github.com/semaphoreci/cli/api/models" "os" "time" + "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/cmd/jobs" "github.com/semaphoreci/cli/cmd/ssh" "github.com/semaphoreci/cli/cmd/utils" diff --git a/cmd/delete.go b/cmd/delete.go index 1cd4e84..03d890d 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -4,6 +4,8 @@ import ( "fmt" client "github.com/semaphoreci/cli/api/client" + models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/cmd/deployment_targets" "github.com/semaphoreci/cli/cmd/utils" "github.com/spf13/cobra" ) @@ -125,6 +127,19 @@ var DeleteNotificationCmd = &cobra.Command{ }, } +var deleteTargetCmd = &cobra.Command{ + Use: "deployment_target [ID]", + Short: "Delete a deployment target.", + Long: ``, + Aliases: models.DeploymentTargetCmdAliases, + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + targetId := args[0] + deployment_targets.Delete(targetId) + }, +} + func init() { RootCmd.AddCommand(deleteCmd) @@ -138,4 +153,6 @@ func init() { DeleteSecretCmd.Flags().StringP("project-id", "i", "", "project id; if specified will delete project secret, otherwise organization secret") deleteCmd.AddCommand(DeleteSecretCmd) + + deleteCmd.AddCommand(deleteTargetCmd) } diff --git a/cmd/delete_test.go b/cmd/delete_test.go index 3cc60a6..d345310 100644 --- a/cmd/delete_test.go +++ b/cmd/delete_test.go @@ -1,9 +1,12 @@ package cmd import ( + "fmt" "net/http" "testing" + "github.com/semaphoreci/cli/api/uuid" + assert "github.com/stretchr/testify/assert" httpmock "gopkg.in/jarcoal/httpmock.v1" ) @@ -94,3 +97,36 @@ func TestDeleteAgentTypeCmd__Response200(t *testing.T) { t.Error("Expected the API to receive DELETE agent_type s1-testing") } } + +func Test__DeleteDeploymentTarget__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + uuid.Mock() + defer uuid.Unmock() + + received := false + + targetId := "494b76aa-f3f0-4ecf-b5ef-c389591a01be" + unique_token, _ := uuid.NewUUID() + deleteURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s?unique_token=%s", targetId, unique_token) + httpmock.RegisterResponder(http.MethodDelete, deleteURL, + func(req *http.Request) (*http.Response, error) { + received = true + + p := `{ + "id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", + "name": "dep target test", + "url": "https://semaphoreci.xyz/target", + "project_id": "proj_id" + } + ` + + return httpmock.NewStringResponse(200, p), nil + }, + ) + + RootCmd.SetArgs([]string{"delete", "dt", targetId}) + RootCmd.Execute() + + assert.True(t, received, "Expected the API to receive DELETE deployment_targets/:id") +} diff --git a/cmd/deployment_targets/delete.go b/cmd/deployment_targets/delete.go new file mode 100644 index 0000000..56f27d4 --- /dev/null +++ b/cmd/deployment_targets/delete.go @@ -0,0 +1,17 @@ +package deployment_targets + +import ( + "fmt" + + client "github.com/semaphoreci/cli/api/client" + "github.com/semaphoreci/cli/cmd/utils" +) + +func Delete(targetId string) { + c := client.NewDeploymentTargetsV1AlphaApi() + + err := c.Delete(targetId) + utils.Check(err) + + fmt.Printf("Deployment target '%s' deleted.\n", targetId) +} diff --git a/cmd/deployment_targets/get.go b/cmd/deployment_targets/get.go new file mode 100644 index 0000000..bf92660 --- /dev/null +++ b/cmd/deployment_targets/get.go @@ -0,0 +1,142 @@ +package deployment_targets + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + "text/tabwriter" + + client "github.com/semaphoreci/cli/api/client" + "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/cmd/utils" + "github.com/spf13/cobra" +) + +func Describe(targetId string) { + c := client.NewDeploymentTargetsV1AlphaApi() + + var deploymentTarget *models.DeploymentTargetV1Alpha + var err error + if targetId != "" { + deploymentTarget, err = c.Describe(targetId) + utils.Check(err) + } else { + utils.Check(errors.New("target id or name must be provided")) + } + deploymentTargetYaml, err := deploymentTarget.ToYaml() + utils.Check(err) + + fmt.Printf("%s\n", deploymentTargetYaml) +} + +func DescribeByName(targetName, projectId string) { + c := client.NewDeploymentTargetsV1AlphaApi() + + deploymentTarget, err := c.DescribeByName(targetName, projectId) + utils.Check(err) + + deploymentTargetYaml, err := deploymentTarget.ToYaml() + utils.Check(err) + + fmt.Printf("%s\n", deploymentTargetYaml) +} + +func History(targetId string, cmd *cobra.Command) { + historyRequest := models.HistoryRequestFiltersV1Alpha{ + CursorType: models.HistoryRequestCursorTypeFirstV1Alpha, + } + var err error + historyRequest.GitRefType, err = cmd.Flags().GetString("git-ref-type") + utils.Check(err) + historyRequest.GitRefType = strings.ToLower(strings.TrimSpace(historyRequest.GitRefType)) + + historyRequest.GitRefLabel, err = cmd.Flags().GetString("git-ref-label") + utils.Check(err) + historyRequest.TriggeredBy, err = cmd.Flags().GetString("triggered-by") + utils.Check(err) + + parameters, err := cmd.Flags().GetStringArray("parameter") + utils.Check(err) + for i, parameter := range parameters { + switch i { + case 0: + historyRequest.Parameter1 = parameter + case 1: + historyRequest.Parameter2 = parameter + case 2: + historyRequest.Parameter3 = parameter + } + } + afterTimestamp, err := cmd.Flags().GetString("after") + utils.Check(err) + if afterTimestamp != "" { + _, err = strconv.ParseInt(afterTimestamp, 10, 64) + if err != nil { + utils.Check(errors.New("after timestamp must be valid UNIX time in microseconds")) + } + historyRequest.CursorType = models.HistoryRequestCursorTypeAfterV1Alpha + historyRequest.CursorValue = afterTimestamp + } + + beforeTimestamp, err := cmd.Flags().GetString("before") + utils.Check(err) + if beforeTimestamp != "" { + _, err = strconv.ParseInt(beforeTimestamp, 10, 64) + if err != nil { + utils.Check(errors.New("before timestamp must be valid UNIX time in microseconds")) + } + historyRequest.CursorType = models.HistoryRequestCursorTypeBeforeV1Alpha + historyRequest.CursorValue = beforeTimestamp + } + if afterTimestamp != "" && beforeTimestamp != "" { + utils.Check(errors.New("you can't use both after and before timestamps")) + } + c := client.NewDeploymentTargetsV1AlphaApi() + + deployments, err := c.History(targetId, historyRequest) + utils.Check(err) + + deploymentsYaml, err := deployments.ToYaml() + utils.Check(err) + + fmt.Printf("%s\n", deploymentsYaml) +} + +func HistoryByName(targetName, projectId string, cmd *cobra.Command) { + c := client.NewDeploymentTargetsV1AlphaApi() + + deploymentTarget, err := c.DescribeByName(targetName, projectId) + utils.Check(err) + + History(deploymentTarget.Id, cmd) +} + +func List(projectId string) { + c := client.NewDeploymentTargetsV1AlphaApi() + + deploymentTargetsList, err := c.List(projectId) + utils.Check(err) + + const padding = 3 + w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', 0) + defer w.Flush() + + fmt.Fprintln(w, "DEPLOYMENT TARGET ID\tNAME\tCREATION TIME (UTC)\tSTATE\tSTATUS") + if deploymentTargetsList == nil { + return + } + for _, t := range *deploymentTargetsList { + createdAt := "N/A" + if t.CreatedAt != nil { + createdAt = t.CreatedAt.Format("2006-01-02 15:04:05") + } + stateName := t.State + status := "inactive" + if t.Active { + status = "active" + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", t.Id, t.Name, createdAt, stateName, status) + } +} diff --git a/cmd/deployment_targets/rebuild.go b/cmd/deployment_targets/rebuild.go new file mode 100644 index 0000000..86fd0c7 --- /dev/null +++ b/cmd/deployment_targets/rebuild.go @@ -0,0 +1,17 @@ +package deployment_targets + +import ( + "fmt" + + "github.com/semaphoreci/cli/api/client" + "github.com/semaphoreci/cli/cmd/utils" +) + +func Rebuild(targetId string) { + client := client.NewDeploymentTargetsV1AlphaApi() + successful, err := client.Activate(targetId) + utils.Check(err) + if successful { + fmt.Printf("Target [%s] was rebuilt successfully\n", targetId) + } +} diff --git a/cmd/deployment_targets/stop.go b/cmd/deployment_targets/stop.go new file mode 100644 index 0000000..43722fa --- /dev/null +++ b/cmd/deployment_targets/stop.go @@ -0,0 +1,17 @@ +package deployment_targets + +import ( + "fmt" + + "github.com/semaphoreci/cli/api/client" + "github.com/semaphoreci/cli/cmd/utils" +) + +func Stop(targetId string) { + client := client.NewDeploymentTargetsV1AlphaApi() + successful, err := client.Deactivate(targetId) + utils.Check(err) + if successful { + fmt.Printf("Target [%s] was stopped successfully\n", targetId) + } +} diff --git a/cmd/edit.go b/cmd/edit.go index e2478ec..56abb48 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -1,10 +1,12 @@ package cmd import ( + "errors" "fmt" client "github.com/semaphoreci/cli/api/client" models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" "github.com/semaphoreci/cli/cmd/utils" "github.com/spf13/cobra" ) @@ -189,6 +191,96 @@ var EditProjectCmd = &cobra.Command{ }, } +var EditDeploymentTargetCmd = &cobra.Command{ + Use: "deployment_target [id or name]", + Short: "Edit a deployment target.", + Long: ``, + Aliases: models.DeploymentTargetCmdAliases, + Args: cobra.RangeArgs(0, 1), + + Run: func(cmd *cobra.Command, args []string) { + c := client.NewDeploymentTargetsV1AlphaApi() + targetName, err := cmd.Flags().GetString("name") + utils.Check(err) + targetId, err := cmd.Flags().GetString("id") + utils.Check(err) + if len(args) == 1 { + if uuid.IsValid(args[0]) { + targetId = args[0] + } else { + targetName = args[0] + } + } + shouldActivate, err := cmd.Flags().GetBool("activate") + utils.Check(err) + shouldDeactivate, err := cmd.Flags().GetBool("deactivate") + utils.Check(err) + + var target *models.DeploymentTargetV1Alpha + if targetId != "" { + if !shouldActivate && !shouldDeactivate { + target, err = c.DescribeWithSecrets(targetId) + } + } else if targetName != "" { + target, err = c.DescribeByName(targetName, getPrj(cmd)) + if err == nil && target != nil { + target, err = c.DescribeWithSecrets(target.Id) + } + } else { + err = errors.New("target id or target name must be provided") + } + utils.Check(err) + if target != nil { + targetId = target.Id + } + if shouldActivate { + succeeded, err := c.Activate(targetId) + utils.Check(err) + if !succeeded { + utils.Check(errors.New("the deployment target wasn't activated successfully")) + } else { + fmt.Printf("The deployment target '%s' is active.\n", targetId) + return + } + } else if shouldDeactivate { + succeeded, err := c.Deactivate(targetId) + utils.Check(err) + if !succeeded { + utils.Check(errors.New("the deployment target wasn't deactivated successfully")) + } else { + fmt.Printf("The deployment target '%s' is inactive.\n", targetId) + return + } + } + if target == nil { + utils.Check(errors.New("valid target could not be retrieved")) + } + // TODO: Load secrets for deployment target to avoid requiring clients + // to provide them every time they edit the request. + request := models.DeploymentTargetUpdateRequestV1Alpha{ + DeploymentTargetV1Alpha: *target, + } + content, err := request.ToYaml() + utils.Check(err) + + new_content, err := utils.EditYamlInEditor(request.ObjectName(), string(content)) + utils.Check(err) + + changedTarget, err := models.NewDeploymentTargetV1AlphaFromYaml([]byte(new_content)) + utils.Check(err) + utils.Check(changedTarget.LoadFiles()) + + updateRequest := &models.DeploymentTargetUpdateRequestV1Alpha{ + DeploymentTargetV1Alpha: *changedTarget, + } + + updatedTarget, err := c.Update(updateRequest) + utils.Check(err) + + fmt.Printf("Deployment target '%s' updated.\n", updatedTarget.Name) + }, +} + func init() { RootCmd.AddCommand(editCmd) @@ -200,4 +292,14 @@ func init() { editCmd.AddCommand(EditDashboardCmd) editCmd.AddCommand(EditNotificationCmd) editCmd.AddCommand(EditProjectCmd) + + EditDeploymentTargetCmd.Flags().StringP("project-name", "p", "", + "project name; if not specified will be inferred from git origin") + EditDeploymentTargetCmd.Flags().StringP("project-id", "i", "", + "project id; if not specified will be inferred from git origin") + EditDeploymentTargetCmd.Flags().StringP("name", "n", "", "target name") + EditDeploymentTargetCmd.Flags().StringP("id", "t", "", "target id") + EditDeploymentTargetCmd.Flags().BoolP("activate", "a", false, "activates/uncordon the deployment target") + EditDeploymentTargetCmd.Flags().BoolP("deactivate", "d", false, "deactivates/cordon the deployment target") + editCmd.AddCommand(EditDeploymentTargetCmd) } diff --git a/cmd/edit_test.go b/cmd/edit_test.go index 12fd393..995219c 100644 --- a/cmd/edit_test.go +++ b/cmd/edit_test.go @@ -1,11 +1,14 @@ package cmd import ( + "encoding/json" + "fmt" "io/ioutil" "net/http" "testing" models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" assert "github.com/stretchr/testify/assert" httpmock "gopkg.in/jarcoal/httpmock.v1" ) @@ -214,3 +217,210 @@ func Test__EditProject__Response200(t *testing.T) { assert.Equal(t, scheduler.At, "* * * *") assert.Equal(t, scheduler.PipelineFile, ".semaphore/cron.yml") } + +func Test__EditDeploymentTarget__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + uuid.Mock() + defer uuid.Unmock() + + targetJSON := `{ + "apiVersion": "v1alpha", + "kind": "DeploymentTarget", + "id": "bb2ba294-d4b3-48bc-90a7-12dd56e9424c", + "name": "dt-name", + "project_id": "projId1", + "organization_id": "org-id", + "description": "dt-description", + "url": "www.semaphore.xyz", + "subject_rules": [ + { + "type": "USER", + "subject_id": "00000000-0000-0000-0000-000000000000" + } + ], + "object_rules": [ + { + "type": "BRANCH", + "match_mode": "PATTERN", + "pattern": ".*main.*" + } + ], + "env_vars": [ + { + "name": "X", + "value": "123" + } + ], + "active": true, + "bookmark_parameter1": "book1" + } + ` + + var received *models.DeploymentTargetV1Alpha + + targetId := "bb2ba294-d4b3-48bc-90a7-12dd56e9424c" + targetGetURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s?include_secrets=true", targetId) + httpmock.RegisterResponder(http.MethodGet, targetGetURL, + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(200, targetJSON), nil + }, + ) + targetPatchURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s", targetId) + httpmock.RegisterResponder(http.MethodPatch, targetPatchURL, + func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + updateRequest := models.DeploymentTargetCreateRequestV1Alpha{} + json.Unmarshal(body, &updateRequest) + received = &updateRequest.DeploymentTargetV1Alpha + body, _ = json.Marshal(received) + return httpmock.NewStringResponse(200, string(body)), nil + }, + ) + + RootCmd.SetArgs([]string{"edit", "dt", targetId}) + RootCmd.Execute() + + assert.Equal(t, received.Name, "dt-name") + assert.Equal(t, received.Description, "dt-description") + assert.Equal(t, received.Url, "www.semaphore.xyz") + assert.Equal(t, len(*received.EnvVars), 1) + assert.Equal(t, *(*received.EnvVars)[0], models.DeploymentTargetEnvVarV1Alpha{Name: "X", Value: "123"}) +} + +func Test__EditDeploymentTargetByName__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + uuid.Mock() + defer uuid.Unmock() + + targetJSON := `{ + "apiVersion": "v1alpha", + "kind": "DeploymentTarget", + "id": "bb2ba294-d4b3-48bc-90a7-12dd56e9424c", + "name": "dt-name", + "project_id": "projId1", + "organization_id": "org-id", + "description": "dt-description", + "url": "www.semaphore.xyz", + "subject_rules": [ + { + "type": "USER", + "subject_id": "00000000-0000-0000-0000-000000000000" + } + ], + "object_rules": [ + { + "type": "BRANCH", + "match_mode": "PATTERN", + "pattern": ".*main.*" + } + ], + "env_vars": [ + { + "name": "X", + "value": "123" + } + ], + "active": true, + "bookmark_parameter1": "book1" + } + ` + targetsJSON := "[" + targetJSON + "]" + var received *models.DeploymentTargetV1Alpha + + targetId := "bb2ba294-d4b3-48bc-90a7-12dd56e9424c" + targetName := "dt-name" + projectId := "projId1" + targetGetURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets?project_id=%s&target_name=%s", projectId, targetName) + httpmock.RegisterResponder(http.MethodGet, targetGetURL, + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(200, targetsJSON), nil + }, + ) + targetGetByIDURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s?include_secrets=true", targetId) + httpmock.RegisterResponder(http.MethodGet, targetGetByIDURL, + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(200, targetJSON), nil + }, + ) + + targetPatchURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s", targetId) + httpmock.RegisterResponder(http.MethodPatch, targetPatchURL, + func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + + updateRequest := models.DeploymentTargetCreateRequestV1Alpha{} + json.Unmarshal(body, &updateRequest) + received = &updateRequest.DeploymentTargetV1Alpha + + body, _ = json.Marshal(received) + return httpmock.NewStringResponse(200, string(body)), nil + }, + ) + + RootCmd.SetArgs([]string{"edit", "dt", targetName, "-i", projectId}) + RootCmd.Execute() + + assert.Equal(t, received.Name, "dt-name") + assert.Equal(t, received.Description, "dt-description") + assert.Equal(t, received.Url, "www.semaphore.xyz") + assert.Equal(t, len(*received.EnvVars), 1) + assert.Equal(t, *(*received.EnvVars)[0], models.DeploymentTargetEnvVarV1Alpha{Name: "X", Value: "123"}) +} + +func Test__EditDeploymentTargetDeactivate__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + received := false + + targetId := "494b76aa-f3f0-4ecf-b5ef-c389591a01be" + patchURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s/deactivate", targetId) + httpmock.RegisterResponder(http.MethodPatch, patchURL, + func(req *http.Request) (*http.Response, error) { + received = true + + target := `{ + "target_id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", + "cordoned": true + } + ` + + return httpmock.NewStringResponse(200, target), nil + }, + ) + + RootCmd.SetArgs([]string{"edit", "dt", targetId, "-d"}) + RootCmd.Execute() + + assert.True(t, received, "Expected the API to receive PATCH deployment_targets/:id/deactivate") +} + +func Test__EditDeploymentTargetActivate__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + received := false + + targetId := "494b76aa-f3f0-4ecf-b5ef-c389591a01be" + patchURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s/activate", targetId) + httpmock.RegisterResponder(http.MethodPatch, patchURL, + func(req *http.Request) (*http.Response, error) { + received = true + + target := `{ + "target_id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", + "active": false + } + ` + + return httpmock.NewStringResponse(200, target), nil + }, + ) + + RootCmd.SetArgs([]string{"edit", "dt", targetId, "--activate"}) + RootCmd.Execute() + + assert.True(t, received, "Expected the API to receive PATCH deployment_targets/:id/activate") +} diff --git a/cmd/get.go b/cmd/get.go index 11bb735..19ce0dc 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -7,7 +7,9 @@ import ( "text/tabwriter" client "github.com/semaphoreci/cli/api/client" - "github.com/semaphoreci/cli/api/models" + models "github.com/semaphoreci/cli/api/models" + "github.com/semaphoreci/cli/api/uuid" + "github.com/semaphoreci/cli/cmd/deployment_targets" "github.com/semaphoreci/cli/cmd/pipelines" "github.com/semaphoreci/cli/cmd/utils" "github.com/semaphoreci/cli/cmd/workflows" @@ -409,6 +411,45 @@ var GetWfCmd = &cobra.Command{ }, } +var GetDTCmd = &cobra.Command{ + Use: "deployment_targets [id or name]", + Short: "Get deployment targets.", + Long: ``, + Aliases: models.DeploymentTargetCmdAliases, + Args: cobra.RangeArgs(0, 1), + + Run: func(cmd *cobra.Command, args []string) { + getHistory, err := cmd.Flags().GetBool("history") + utils.Check(err) + targetName, err := cmd.Flags().GetString("name") + utils.Check(err) + targetId, err := cmd.Flags().GetString("id") + utils.Check(err) + if len(args) == 1 { + if uuid.IsValid(args[0]) { + targetId = args[0] + } else { + targetName = args[0] + } + } + if getHistory { + if targetId != "" { + deployment_targets.History(targetId, cmd) + } else if targetName != "" { + deployment_targets.HistoryByName(targetName, getPrj(cmd), cmd) + } + } else { + if targetId != "" { + deployment_targets.Describe(targetId) + } else if targetName != "" { + deployment_targets.DescribeByName(targetName, getPrj(cmd)) + } else { + deployment_targets.List(getPrj(cmd)) + } + } + }, +} + func GetProjectID(cmd *cobra.Command) string { projectID, err := cmd.Flags().GetString("project-id") if projectID != "" { @@ -485,4 +526,20 @@ func init() { "project name; if not specified will be inferred from git origin") GetWfCmd.Flags().StringP("project-id", "i", "", "project id; if not specified will be inferred from git origin") + + getCmd.AddCommand(GetDTCmd) + GetDTCmd.Flags().StringP("project-name", "p", "", + "project name; if not specified will be inferred from git origin") + GetDTCmd.Flags().StringP("project-id", "i", "", + "project id; if not specified will be inferred from git origin") + GetDTCmd.Flags().StringP("id", "t", "", "target id") + GetDTCmd.Flags().StringP("name", "n", "", "target name") + GetDTCmd.Flags().BoolP("history", "s", false, "get deployment target history") + GetDTCmd.Flags().Lookup("history").NoOptDefVal = "true" + GetDTCmd.Flags().StringP("after", "a", "", "show deployment history after the timestamp") + GetDTCmd.Flags().StringP("before", "b", "", "show deployment history before the timestamp") + GetDTCmd.Flags().StringP("git-ref-type", "g", "", "git reference type: branch, tag, pr") + GetDTCmd.Flags().StringP("git-ref-label", "l", "", "git reference label: branch or tag name") + GetDTCmd.Flags().StringArrayP("parameter", "q", []string{}, "show deployment history of deployment targets with provided bookmark parameters") + GetDTCmd.Flags().StringP("triggered-by", "u", "", "show deployment history triggered by specific user or promotion") } diff --git a/cmd/get_test.go b/cmd/get_test.go index 9763a3c..1f9eed8 100644 --- a/cmd/get_test.go +++ b/cmd/get_test.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "net/http" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -370,13 +371,13 @@ func Test__GetPipeline__Response200(t *testing.T) { received = true p := `{ - "pipeline": { + "pipeline": { "ppl_id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", "name": "snapshot test", - "state": "done", - "result": "passed", + "state": "done", + "result": "passed", "result_reason": "test", - "error_description": "" + "error_description": "" } }` @@ -390,6 +391,132 @@ func Test__GetPipeline__Response200(t *testing.T) { assert.True(t, received, "Expected the API to receive GET pipelines/:id") } +func Test__GetDeploymentTargetByName__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + received := false + + projectId := "494b76aa-f3f0-4ecf-b5ef-c389591a01be" + targetName := "dep target test" + getURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets?project_id=%s&target_name=%s", projectId, url.PathEscape(targetName)) + + httpmock.RegisterResponder(http.MethodGet, getURL, + func(req *http.Request) (*http.Response, error) { + received = true + + p := `[{ + "id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", + "name": "dep target test", + "url": "https://semaphoreci.xyz/target", + "project_id": "proj_id" + }] + ` + + return httpmock.NewStringResponse(200, p), nil + }, + ) + + RootCmd.SetArgs([]string{"get", "dt", targetName, "-i", projectId}) + RootCmd.Execute() + + assert.True(t, received, "Expected the API to receive GET deployment_targets") +} + +func Test__GetDeploymentTarget__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + received := false + + targetId := "494b76aa-f3f0-4ecf-b5ef-c389591a01be" + getURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s", targetId) + httpmock.RegisterResponder(http.MethodGet, getURL, + func(req *http.Request) (*http.Response, error) { + received = true + + p := `{ + "id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", + "name": "dep target test", + "url": "https://semaphoreci.xyz/target", + "project_id": "proj_id" + } + ` + + return httpmock.NewStringResponse(200, p), nil + }, + ) + + RootCmd.SetArgs([]string{"get", "dt", targetId}) + RootCmd.Execute() + + assert.True(t, received, "Expected the API to receive GET deployment_targets/:id") +} + +func Test__GetDeploymentTargetsList__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + received := false + + projectId := "proj_id" + getURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets?project_id=%s", projectId) + httpmock.RegisterResponder(http.MethodGet, getURL, + func(req *http.Request) (*http.Response, error) { + received = true + + p := `[{ + "id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", + "name": "dep target test", + "url": "https://semaphoreci.xyz/target", + "project_id": "proj_id" + }] + ` + + return httpmock.NewStringResponse(200, p), nil + }, + ) + + RootCmd.SetArgs([]string{"get", "dt", "--project-id", projectId}) + RootCmd.Execute() + + assert.True(t, received, "Expected the API to receive GET deployment_targets/:id") +} + +func Test__GetDeploymentTargetHistory__Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + received := false + + targetId := "494b76aa-f3f0-4ecf-b5ef-c389591a01be" + getURL := fmt.Sprintf("https://org.semaphoretext.xyz/api/v1alpha/deployment_targets/%s/history?cursor_type=AFTER&cursor_value=123123123123&git_ref_label=main&git_ref_type=branch&triggered_by=event", targetId) + httpmock.RegisterResponder(http.MethodGet, getURL, + func(req *http.Request) (*http.Response, error) { + received = true + + p := `{"deployments":[{ + "id": "494b76aa-f3f0-4ecf-b5ef-c389591a01be", + "target_id": "target_id123", + "pipeline_id": "pipeline_id" + }]}` + return httpmock.NewStringResponse(200, p), nil + }, + ) + + RootCmd.SetArgs([]string{"get", "dt", targetId, + "--history", + "-a", "123123123123", + "--triggered-by", "event", + "--git-ref-type", "branch", + "--git-ref-label", "main", + "-p", "bookmark 1", + }) + RootCmd.Execute() + + assert.True(t, received, "Expected the API to receive GET deployment_targets/:id/history") +} + func Test__GetWorkflows__Response200(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -420,8 +547,8 @@ func Test__GetWorkflows__Response200(t *testing.T) { "project_id": "758cb945-7495-4e40-a9a1-4b3991c6a8fe", "initial_ppl_id": "92f81b82-3584-4852-ab28-4866624bed1e", "created_at": { - "seconds": 1533833523, - "nanos": 537460000 + "seconds": 1533833523, + "nanos": 537460000 } }]` diff --git a/cmd/utils/check.go b/cmd/utils/check.go index 5d29b11..ec25649 100644 --- a/cmd/utils/check.go +++ b/cmd/utils/check.go @@ -6,13 +6,11 @@ import ( "os" ) -// // Checks if an error is present. // // If it is present, it displays the error and exits with status 1. // // If you want to display a custom message use CheckWithMessage. -// func Check(err error) { if err != nil { fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) @@ -21,11 +19,9 @@ func Check(err error) { } } -// // Checks if an error is present. // // If it is present, it displays the provided message and exits with status 1. -// func CheckWithMessage(err error, message string) { if err != nil { fmt.Fprintf(os.Stderr, "error: %+v\n", message) diff --git a/cmd/utils/csv_flag.go b/cmd/utils/csv_flag.go index 21490ff..f443a02 100644 --- a/cmd/utils/csv_flag.go +++ b/cmd/utils/csv_flag.go @@ -26,3 +26,26 @@ func CSVFlag(cmd *cobra.Command, flag string) ([]string, error) { return results, err } + +func CSVArrayFlag(cmd *cobra.Command, flag string, trimSpace bool) (results [][]string, err error) { + vals, err := cmd.Flags().GetStringArray(flag) + if err != nil { + return + } + for _, val := range vals { + results = append(results, processCSVValue(val, trimSpace)) + } + return +} + +func processCSVValue(val string, trimSpace bool) (result []string) { + parts := strings.Split(val, ",") + for _, part := range parts { + if trimSpace { + part = strings.TrimSpace(part) + } + result = append(result, part) + } + + return +} diff --git a/cmd/utils/project.go b/cmd/utils/project.go index 2f116fb..9994cdd 100644 --- a/cmd/utils/project.go +++ b/cmd/utils/project.go @@ -3,11 +3,11 @@ package utils import ( "fmt" - "github.com/semaphoreci/cli/api/client" - "log" "os/exec" "strings" + + "github.com/semaphoreci/cli/api/client" ) func GetProjectId(name string) string { diff --git a/cmd/utils/yaml_resource.go b/cmd/utils/yaml_resource.go index 160a87a..abbb7fb 100644 --- a/cmd/utils/yaml_resource.go +++ b/cmd/utils/yaml_resource.go @@ -6,14 +6,11 @@ import ( "github.com/ghodss/yaml" ) -// // returns tuple (apiVersion, kind, error) -// func ParseYamlResourceHeaders(raw []byte) (string, string, error) { m := make(map[string]interface{}) err := yaml.Unmarshal(raw, &m) - if err != nil { return "", "", fmt.Errorf("Failed to parse resource; %s", err) } diff --git a/go.mod b/go.mod index daf9c39..e8c4d65 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,11 @@ module github.com/semaphoreci/cli -go 1.16 +go 1.17 require ( - github.com/BurntSushi/toml v0.3.0 // indirect github.com/ghodss/yaml v1.0.0 github.com/google/uuid v1.0.0 - github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/mitchellh/go-homedir v1.0.0 - github.com/onsi/gomega v1.4.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.2.0 github.com/stretchr/testify v1.2.2 @@ -17,3 +13,23 @@ require ( gopkg.in/jarcoal/httpmock.v1 v1.0.0-20180719183105-8007e27cdb32 gopkg.in/yaml.v2 v2.4.0 ) + +require ( + github.com/BurntSushi/toml v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/magiconair/properties v1.8.0 // indirect + github.com/mitchellh/mapstructure v1.0.0 // indirect + github.com/onsi/gomega v1.4.2 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.2.0 // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.2 // indirect + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect + golang.org/x/text v0.3.3 // indirect +) diff --git a/go.sum b/go.sum index b369ccb..5a8f61b 100644 --- a/go.sum +++ b/go.sum @@ -43,21 +43,29 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/jarcoal/httpmock.v1 v1.0.0-20180719183105-8007e27cdb32 h1:30DLrQoRqdUHslVMzxuKUnY4GKJGk1/FJtKy3yx4TKE= gopkg.in/jarcoal/httpmock.v1 v1.0.0-20180719183105-8007e27cdb32/go.mod h1:d3R+NllX3X5e0zlG1Rful3uLvsGC/Q3OHut5464DEQw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=