From dba46aac9b66251be616b4c7113781b039905c64 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Mon, 26 Feb 2024 09:29:43 +0100 Subject: [PATCH] feat: context commands can now be used with `--org-id` parameter. Refactored the context api abstraction. You can now access function to request contexts directly or use an abstraction that handles server without v2 API. --- .gitignore | 1 + api/api.go | 78 ++-- api/context.go | 34 -- api/context/context.go | 87 +++++ api/context/gql.go | 253 ++++++++++++ api/context/gql_client.go | 85 ++++ api/context/rest.go | 247 ++++++++++++ api/context/rest_client.go | 107 +++++ api/context_graphql.go | 390 ------------------- api/context_rest.go | 620 ----------------------------- api/context_rest_test.go | 98 ----- api/context_test.go | 213 ---------- api/rest/client.go | 16 +- api/schedule_rest.go | 21 +- clitest/clitest.go | 24 -- cmd/.circleci/update_check.yml | 1 - cmd/context.go | 249 +++++++----- cmd/context_test.go | 686 ++++++++++++++++++++++++++++----- cmd/namespace_test.go | 31 +- cmd/orb.go | 9 +- md_docs/md_docs.go | 10 +- 21 files changed, 1618 insertions(+), 1642 deletions(-) delete mode 100644 api/context.go create mode 100644 api/context/context.go create mode 100644 api/context/gql.go create mode 100644 api/context/gql_client.go create mode 100644 api/context/rest.go create mode 100644 api/context/rest_client.go delete mode 100644 api/context_graphql.go delete mode 100644 api/context_rest.go delete mode 100644 api/context_rest_test.go delete mode 100644 api/context_test.go delete mode 100644 cmd/.circleci/update_check.yml diff --git a/.gitignore b/.gitignore index b9c95955f..a20b0ca1a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ stage/ bin/golangci-lint integration_tests/tmp *.exe +cmd/.circleci diff --git a/api/api.go b/api/api.go index 846626465..6ef521bf5 100644 --- a/api/api.go +++ b/api/api.go @@ -27,6 +27,17 @@ const ( Remove ) +type ErrorWithMessage struct { + Message string `json:"message"` +} + +func (e ErrorWithMessage) Error() string { + if e.Message != "" { + return e.Message + } + return "unknown error" +} + // GQLErrorsCollection is a slice of errors returned by the GraphQL server. // Each error is made up of a GQLResponseError type. type GQLErrorsCollection []GQLResponseError @@ -185,13 +196,6 @@ type RenameNamespaceResponse struct { } } -// GetOrganizationResponse type wraps the GQL response for fetching an organization and ID. -type GetOrganizationResponse struct { - Organization struct { - ID string - } -} - // WhoamiResponse type matches the data shape of the GQL response for the current user type WhoamiResponse struct { Me struct { @@ -744,30 +748,54 @@ func CreateNamespaceWithOwnerID(cl *graphql.Client, name string, ownerID string) return &response, nil } -func getOrganization(cl *graphql.Client, organizationName string, organizationVcs string) (*GetOrganizationResponse, error) { - var response GetOrganizationResponse +type GetOrganizationParams struct { + OrgName string + VCSType string + OrgID string +} - query := `query($organizationName: String!, $organizationVcs: VCSType!) { - organization( - name: $organizationName - vcsType: $organizationVcs - ) { - id - } - }` +// GetOrganizationResponse type wraps the GQL response for fetching an organization and ID. +type GetOrganizationResponse struct { + Organization struct { + ID string + Name string + VCSType string + } +} - request := graphql.NewRequest(query) - request.SetToken(cl.Token) +func GetOrganization(cl *graphql.Client, params GetOrganizationParams) (*GetOrganizationResponse, error) { + if params.OrgID == "" && (params.VCSType == "" || params.OrgName == "") { + return nil, errors.New("need to define either org-id or vcs-type and org name") + } - request.Var("organizationName", organizationName) - request.Var("organizationVcs", strings.ToUpper(organizationVcs)) + var request *graphql.Request + if params.OrgID != "" { + request = graphql.NewRequest(`query($orgId: UUID!) { + organization(id: $orgId) { + id + name + vcsType + } +}`) + request.Var("orgId", params.OrgID) + } else { + request = graphql.NewRequest(`query($orgName: String!, $vcsType: VCSType!) { + organization(name: $orgName, vcsType: $vcsType) { + id + name + vcsType + } +}`) + request.Var("orgName", params.OrgName) + request.Var("vcsType", params.VCSType) + } + var response GetOrganizationResponse + request.SetToken(cl.Token) err := cl.Run(request, &response) - if err != nil { - return nil, errors.Wrapf(err, "Unable to find organization %s of vcs-type %s", organizationName, organizationVcs) + return nil, err } - return &response, nil } @@ -857,7 +885,7 @@ mutation($id: UUID!) { // CreateNamespace creates (reserves) a namespace for an organization func CreateNamespace(cl *graphql.Client, name string, organizationName string, organizationVcs string) (*CreateNamespaceResponse, error) { - getOrgResponse, getOrgError := getOrganization(cl, organizationName, organizationVcs) + getOrgResponse, getOrgError := GetOrganization(cl, GetOrganizationParams{OrgName: organizationName, VCSType: organizationVcs}) if getOrgError != nil { return nil, errors.Wrap(organizationNotFound(organizationName, organizationVcs), getOrgError.Error()) diff --git a/api/context.go b/api/context.go deleted file mode 100644 index b8e1895fb..000000000 --- a/api/context.go +++ /dev/null @@ -1,34 +0,0 @@ -package api - -import ( - "time" -) - -// An EnvironmentVariable has a Variable, a ContextID (its owner), and a -// CreatedAt date. -type EnvironmentVariable struct { - Variable string `json:"variable"` - ContextID string `json:"context_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// A Context is the owner of EnvironmentVariables. -type Context struct { - CreatedAt time.Time `json:"created_at"` - ID string `json:"id"` - Name string `json:"name"` -} - -// ContextInterface is the interface to interact with contexts and environment -// variables. -type ContextInterface interface { - Contexts(vcs, org string) (*[]Context, error) - ContextByName(vcs, org, name string) (*Context, error) - DeleteContext(contextID string) error - CreateContext(vcs, org, name string) error - CreateContextWithOrgID(orgID *string, name string) error - EnvironmentVariables(contextID string) (*[]EnvironmentVariable, error) - CreateEnvironmentVariable(contextID, variable, value string) error - DeleteEnvironmentVariable(contextID, variable string) error -} diff --git a/api/context/context.go b/api/context/context.go new file mode 100644 index 000000000..e6929a150 --- /dev/null +++ b/api/context/context.go @@ -0,0 +1,87 @@ +package context + +import ( + "errors" + "fmt" + "time" + + "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/settings" +) + +// An EnvironmentVariable has a Variable, a ContextID (its owner), and a +// CreatedAt date. +type EnvironmentVariable struct { + Variable string `json:"variable"` + ContextID string `json:"context_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// A Context is the owner of EnvironmentVariables. +type Context struct { + CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + Name string `json:"name"` +} + +// ContextInterface is the interface to interact with contexts and environment +// variables. +type ContextInterface interface { + Contexts() ([]Context, error) + ContextByName(name string) (Context, error) + CreateContext(name string) error + DeleteContext(contextID string) error + EnvironmentVariables(contextID string) ([]EnvironmentVariable, error) + CreateEnvironmentVariable(contextID, variable, value string) error + DeleteEnvironmentVariable(contextID, variable string) error +} + +func NewContextClient(config *settings.Config, orgID, vcsType, orgName string) ContextInterface { + restClient := restClient{ + client: rest.NewFromConfig(config.Host, config), + orgID: orgID, + vcsType: vcsType, + orgName: orgName, + } + + if config.Host == "https://circleci.com" { + return restClient + } + if err := IsRestAPIAvailable(restClient); err != nil { + fmt.Printf("err = %+v\n", err) + return &gqlClient{ + client: graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug), + orgID: orgID, + vcsType: vcsType, + orgName: orgName, + } + } + return restClient +} + +func IsRestAPIAvailable(c restClient) error { + u, err := c.client.BaseURL.Parse("openapi.json") + if err != nil { + return err + } + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return err + } + + var resp struct { + Paths struct { + ContextEndpoint interface{} `json:"/context"` + } + } + if _, err := c.client.DoRequest(req, &resp); err != nil { + return err + } + if resp.Paths.ContextEndpoint == nil { + return errors.New("No context endpoint exists") + } + + return nil +} diff --git a/api/context/gql.go b/api/context/gql.go new file mode 100644 index 000000000..2b26f8e63 --- /dev/null +++ b/api/context/gql.go @@ -0,0 +1,253 @@ +package context + +import ( + "fmt" + "strings" + "time" + + "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/pkg/errors" +) + +type ListContextsWithGQLParams struct { + OrgID string + OrgName string + VCSType string +} + +type contextsQueryResponse struct { + Organization struct { + Id string + Contexts struct { + Edges []struct { + Node struct { + ID string + Name string + CreatedAt string + } + } + } + } +} + +func ListContextsWithGQL(c *graphql.Client, params ListContextsWithGQLParams) ([]Context, error) { + if params.OrgID == "" && (params.OrgName == "" || params.VCSType == "") { + return nil, fmt.Errorf("to list context, need either org ID or couple vcs/orgName but got neither") + } + useOrgID := params.OrgID != "" + if !useOrgID && params.VCSType != "github" && params.VCSType != "bitbucket" { + return nil, fmt.Errorf("only github and bitbucket vcs type are available, got: %s", params.VCSType) + } + var query string + if useOrgID { + query = `query ContextsQuery($orgId: ID!) { + organization(id: $orgId) { + id + contexts { + edges { + node { + id + name + createdAt + } + } + } + } +}` + } else { + query = `query ContextsQuery($name: String!, $vcsType: VCSType) { + organization(name: $name, vcsType: $vcsType) { + id + contexts { + edges { + node { + id + name + createdAt + } + } + } + } +}` + } + request := graphql.NewRequest(query) + if useOrgID { + request.Var("orgId", params.OrgID) + } else { + request.Var("name", params.OrgName) + request.Var("vcsType", strings.ToUpper(params.VCSType)) + } + request.SetToken(c.Token) + + var response contextsQueryResponse + err := c.Run(request, &response) + if err != nil { + return nil, errors.Wrapf(err, "failed to load context list") + } + contexts := make([]Context, len(response.Organization.Contexts.Edges)) + for i, edge := range response.Organization.Contexts.Edges { + context := edge.Node + created_at, err := time.Parse(time.RFC3339, context.CreatedAt) + if err != nil { + return nil, err + } + contexts[i] = Context{ + Name: context.Name, + ID: context.ID, + CreatedAt: created_at, + } + } + + return contexts, nil +} + +type CreateContextWithGQLParams struct { + OwnerId string `json:"ownerId"` + OwnerType string `json:"ownerType"` + ContextName string `json:"contextName"` +} + +func CreateContextWithGQL(c *graphql.Client, params CreateContextWithGQLParams) (Context, error) { + query := `mutation CreateContext($input: CreateContextInput!) { + createContext(input: $input) { + error { + type + } + context { + createdAt + id + name + } + } +}` + request := graphql.NewRequest(query) + request.SetToken(c.Token) + request.Var("input", params) + + var response struct { + CreateContext struct { + Error *struct { + Type string + } + Context Context + } + } + if err := c.Run(request, &response); err != nil { + return Context{}, err + } + if response.CreateContext.Error.Type != "" { + return Context{}, fmt.Errorf("Error creating context: %s", response.CreateContext.Error.Type) + } + + return response.CreateContext.Context, nil +} + +func DeleteContextWithGQL(c *graphql.Client, contextID string) error { + query := `mutation DeleteContext($contextId: UUID) { + deleteContext(input: { contextId: $contextId }) { + clientMutationId + } +}` + request := graphql.NewRequest(query) + request.SetToken(c.Token) + + request.Var("contextId", contextID) + + var response struct { + } + + err := c.Run(request, &response) + + return errors.Wrap(err, "failed to delete context") +} + +func ListEnvVarsWithGQL(c *graphql.Client, contextID string) ([]EnvironmentVariable, error) { + query := `query Context($id: ID!) { + context(id: $id) { + resources { + variable + createdAt + updatedAt + } + } +}` + request := graphql.NewRequest(query) + request.SetToken(c.Token) + request.Var("id", contextID) + var resp struct { + Context struct { + Resources []EnvironmentVariable + } + } + err := c.Run(request, &resp) + + if err != nil { + return nil, err + } + for _, ev := range resp.Context.Resources { + ev.ContextID = contextID + } + return resp.Context.Resources, nil +} + +type CreateEnvVarWithGQLParams struct { + ContextID string `json:"contextId"` + Variable string `json:"variable"` + Value string `json:"value"` +} + +func CreateEnvVarWithGQL(c *graphql.Client, params CreateEnvVarWithRestParams) error { + query := `mutation CreateEnvVar($input: StoreEnvironmentVariableInput!) { + storeEnvironmentVariable(input: $input) { + error { + type + } + } +}` + request := graphql.NewRequest(query) + request.SetToken(c.Token) + request.Var("input", params) + + var response struct { + StoreEnvironmentVariable struct { + Error struct { + Type string + } + } + } + + if err := c.Run(request, &response); err != nil { + return errors.Wrap(err, "failed to store environment variable in context") + } + + if response.StoreEnvironmentVariable.Error.Type != "" { + return fmt.Errorf("Error storing environment variable: %s", response.StoreEnvironmentVariable.Error.Type) + } + + return nil +} + +type DeleteEnvVarWithGQLParams struct { + ContextID string `json:"contextId"` + Variable string `json:"variable"` +} + +func DeleteEnvVarWithGQL(c *graphql.Client, params DeleteEnvVarWithRestParams) error { + query := `mutation DeleteEnvVar($input: RemoveEnvironmentVariableInput!) { + removeEnvironmentVariable(input: $input) { + context { + id + } + } +}` + request := graphql.NewRequest(query) + request.SetToken(c.Token) + request.Var("input", params) + + var response struct { + RemoveEnvironmentVariable struct{ Context struct{ Id string } } + } + + err := c.Run(request, &response) + return errors.Wrap(err, "failed to delete environment variable") +} diff --git a/api/context/gql_client.go b/api/context/gql_client.go new file mode 100644 index 000000000..97bfa101e --- /dev/null +++ b/api/context/gql_client.go @@ -0,0 +1,85 @@ +package context + +import ( + "errors" + + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/graphql" +) + +type gqlClient struct { + client *graphql.Client + + orgID string + vcsType string + orgName string +} + +func (c *gqlClient) ensureOrgIDIsDefined() error { + if c.orgID != "" { + return nil + } + if c.vcsType == "" || c.orgName == "" { + return errors.New("need org id or vcs type and org name to be defined") + } + org, err := api.GetOrganization(c.client, api.GetOrganizationParams{OrgName: c.orgName, VCSType: c.vcsType}) + if err != nil { + return err + } + c.orgID = org.Organization.ID + return nil +} + +func (c *gqlClient) Contexts() ([]Context, error) { + return ListContextsWithGQL(c.client, ListContextsWithGQLParams{ + OrgID: c.orgID, + VCSType: c.vcsType, + OrgName: c.orgName, + }) +} + +func (c *gqlClient) ContextByName(name string) (Context, error) { + contexts, err := ListContextsWithGQL(c.client, ListContextsWithGQLParams{ + OrgID: c.orgID, + VCSType: c.vcsType, + OrgName: c.orgName, + }) + if err != nil { + return Context{}, err + } + + for _, context := range contexts { + if context.Name == name { + return context, nil + } + } + return Context{}, errors.New("No context found with that name") +} + +func (c *gqlClient) CreateContext(name string) error { + if err := c.ensureOrgIDIsDefined(); err != nil { + return err + } + _, err := CreateContextWithGQL(c.client, CreateContextWithGQLParams{ + OwnerId: c.orgID, + OwnerType: "ORGANIZATION", + ContextName: name, + }) + return err +} + +func (c *gqlClient) DeleteContext(contextID string) error { + return DeleteContextWithGQL(c.client, contextID) +} + +func (c *gqlClient) EnvironmentVariables(contextID string) ([]EnvironmentVariable, error) { + return ListEnvVarsWithGQL(c.client, contextID) +} + +func (c *gqlClient) CreateEnvironmentVariable(contextID, variable, value string) error { + return CreateEnvVarWithGQL(c.client, CreateEnvVarWithRestParams{contextID, variable, value}) +} + +func (c *gqlClient) DeleteEnvironmentVariable(contextID, variable string) error { + return DeleteEnvVarWithGQL(c.client, DeleteEnvVarWithRestParams{contextID, variable}) +} diff --git a/api/context/rest.go b/api/context/rest.go new file mode 100644 index 000000000..c82c3f5bb --- /dev/null +++ b/api/context/rest.go @@ -0,0 +1,247 @@ +package context + +import ( + "fmt" + "net/url" + + "github.com/CircleCI-Public/circleci-cli/api/rest" +) + +type ListContextsWithRestParams struct { + OwnerID string + OwnerSlug string + OwnerType string + PageToken string +} + +type ListContextsResponse struct { + Items []Context `json:"items"` + NextPageToken string `json:"next_page_token"` +} + +// List all contexts for an owner. +// Binding to https://circleci.com/docs/api/v2/index.html#operation/listContexts +func ListContextsWithRest(client *rest.Client, params ListContextsWithRestParams) (ListContextsResponse, error) { + queryURL, err := client.BaseURL.Parse("context") + if err != nil { + return ListContextsResponse{}, err + } + urlParams := url.Values{} + if params.OwnerID != "" { + urlParams.Add("owner-id", params.OwnerID) + } + if params.OwnerSlug != "" { + urlParams.Add("owner-slug", params.OwnerSlug) + } + if params.OwnerType != "" { + urlParams.Add("owner-type", params.OwnerType) + } + if params.PageToken != "" { + urlParams.Add("page-token", params.PageToken) + } + queryURL.RawQuery = urlParams.Encode() + req, err := client.NewRequest("GET", queryURL, nil) + if err != nil { + return ListContextsResponse{}, err + } + + resp := ListContextsResponse{} + if _, err := client.DoRequest(req, &resp); err != nil { + return ListContextsResponse{}, err + } + return resp, nil +} + +// Gets all pages of ListContexts +func ListAllContextsWithRest(client *rest.Client, params ListContextsWithRestParams) ([]Context, error) { + contexts := []Context{} + for { + resp, err := ListContextsWithRest(client, params) + if err != nil { + return nil, err + } + + contexts = append(contexts, resp.Items...) + + if resp.NextPageToken == "" { + break + } + + params.PageToken = resp.NextPageToken + } + return contexts, nil +} + +type CreateContextWithRestParams struct { + Name string `json:"name"` + Owner struct { + Type string `json:"type"` + Id string `json:"id,omitempty"` + Slug string `json:"slug,omitempty"` + } `json:"owner"` +} + +var orgOwnerType = "organization" + +// Creates a new context. +// Binding to https://circleci.com/docs/api/v2/index.html#operation/createContext +func CreateContextWithRest(client *rest.Client, params CreateContextWithRestParams) (Context, error) { + if params.Owner.Id == "" && params.Owner.Slug == "" { + return Context{}, fmt.Errorf("to create a context, need either org ID or org slug, received none") + } + if params.Owner.Type == "" { + params.Owner.Type = orgOwnerType + } + if params.Owner.Type != orgOwnerType && params.Owner.Type != "account" { + return Context{}, fmt.Errorf("only owner.type values allowed to create a context are \"organization\" or \"account\", received: %s", params.Owner.Type) + } + if params.Owner.Id == "" && params.Owner.Slug != "" && params.Owner.Type == "account" { + return Context{}, fmt.Errorf("when creating a context, owner.type with value \"account\" is only allowed when using owner.id and not when using owner.slug") + } + + u, err := client.BaseURL.Parse("context") + if err != nil { + return Context{}, err + } + req, err := client.NewRequest("POST", u, ¶ms) + if err != nil { + return Context{}, err + } + + var context Context + if _, err := client.DoRequest(req, &context); err != nil { + return context, err + } + + return context, nil +} + +type DeleteContextWithRestResponse struct { + Message string `json:"message"` +} + +func DeleteContextWithRest(client *rest.Client, contextID string) (DeleteContextWithRestResponse, error) { + u, err := client.BaseURL.Parse(fmt.Sprintf("context/%s", contextID)) + if err != nil { + return DeleteContextWithRestResponse{}, err + } + req, err := client.NewRequest("DELETE", u, nil) + if err != nil { + return DeleteContextWithRestResponse{}, err + } + + resp := DeleteContextWithRestResponse{} + if _, err = client.DoRequest(req, &resp); err != nil { + return DeleteContextWithRestResponse{}, err + } + return resp, nil +} + +type ListEnvVarsWithRestParams struct { + ContextID string + PageToken string +} + +type ListEnvVarsResponse struct { + Items []EnvironmentVariable `json:"items"` + NextPageToken string `json:"next_page_token"` +} + +func ListEnvVarsWithRest(client *rest.Client, params ListEnvVarsWithRestParams) (ListEnvVarsResponse, error) { + u, err := client.BaseURL.Parse(fmt.Sprintf("context/%s/environment-variable", params.ContextID)) + if err != nil { + return ListEnvVarsResponse{}, err + } + qs := u.Query() + if params.PageToken != "" { + qs.Add("page-token", params.PageToken) + } + u.RawQuery = qs.Encode() + + req, err := client.NewRequest("GET", u, nil) + if err != nil { + return ListEnvVarsResponse{}, err + } + + resp := ListEnvVarsResponse{} + if _, err := client.DoRequest(req, &resp); err != nil { + return ListEnvVarsResponse{}, err + } + return resp, nil +} + +func ListAllEnvVarsWithRest(client *rest.Client, params ListEnvVarsWithRestParams) ([]EnvironmentVariable, error) { + envVars := []EnvironmentVariable{} + for { + resp, err := ListEnvVarsWithRest(client, params) + if err != nil { + return nil, err + } + + envVars = append(envVars, resp.Items...) + + if resp.NextPageToken == "" { + break + } + + params.PageToken = resp.NextPageToken + } + return envVars, nil +} + +type CreateEnvVarWithRestParams struct { + ContextID string + Name string + Value string +} + +func CreateEnvVarWithRest(client *rest.Client, params CreateEnvVarWithRestParams) (EnvironmentVariable, error) { + u, err := client.BaseURL.Parse(fmt.Sprintf("context/%s/environment-variable/%s", params.ContextID, params.Name)) + if err != nil { + return EnvironmentVariable{}, err + } + + body := struct { + Value string `json:"value"` + }{ + Value: params.Value, + } + req, err := client.NewRequest("PUT", u, &body) + if err != nil { + return EnvironmentVariable{}, err + } + + resp := EnvironmentVariable{} + if _, err := client.DoRequest(req, &resp); err != nil { + return EnvironmentVariable{}, err + } + return resp, nil +} + +type DeleteEnvVarWithRestParams struct { + ContextID string + Name string +} + +type DeleteEnvVarWithRestResponse struct { + Message string `json:"message"` +} + +func DeleteEnvVarWithRest(client *rest.Client, params DeleteEnvVarWithRestParams) (DeleteContextWithRestResponse, error) { + u, err := client.BaseURL.Parse(fmt.Sprintf("context/%s/environment-variable/%s", params.ContextID, params.Name)) + if err != nil { + return DeleteContextWithRestResponse{}, err + } + + req, err := client.NewRequest("DELETE", u, nil) + if err != nil { + return DeleteContextWithRestResponse{}, err + } + + resp := DeleteContextWithRestResponse{} + if _, err := client.DoRequest(req, &resp); err != nil { + return DeleteContextWithRestResponse{}, err + } + + return resp, nil +} diff --git a/api/context/rest_client.go b/api/context/rest_client.go new file mode 100644 index 000000000..5d7c1f9c4 --- /dev/null +++ b/api/context/rest_client.go @@ -0,0 +1,107 @@ +package context + +import ( + "fmt" + + "github.com/CircleCI-Public/circleci-cli/api/rest" +) + +type restClient struct { + client *rest.Client + + orgID string + vcsType string + orgName string +} + +func toSlug(vcs, org string) string { + slug := fmt.Sprintf("%s/%s", vcs, org) + return slug +} + +// createListContextsParams is a helper to create ListContextsParams from the content of the ContextRestClient +func (c restClient) createListContextsParams() (ListContextsWithRestParams, error) { + params := ListContextsWithRestParams{} + if c.orgID != "" { + params.OwnerID = c.orgID + } else if c.vcsType != "" && c.orgName != "" { + params.OwnerSlug = toSlug(c.vcsType, c.orgName) + } else { + return params, fmt.Errorf("to list context, need either org ID or couple vcs/orgName but got neither") + } + return params, nil +} + +func (c restClient) Contexts() ([]Context, error) { + params, err := c.createListContextsParams() + if err != nil { + return nil, err + } + return ListAllContextsWithRest(c.client, params) +} + +func (c restClient) ContextByName(name string) (Context, error) { + params, err := c.createListContextsParams() + if err != nil { + return Context{}, err + } + + for { + resp, err := ListContextsWithRest(c.client, params) + if err != nil { + return Context{}, err + } + + for _, context := range resp.Items { + if context.Name == name { + return context, nil + } + } + + if resp.NextPageToken == "" { + break + } + + params.PageToken = resp.NextPageToken + } + return Context{}, fmt.Errorf("context with name %s not found", name) +} + +func (c restClient) CreateContext(name string) error { + params := CreateContextWithRestParams{ + Name: name, + } + params.Owner.Type = "organization" + if c.orgID != "" { + params.Owner.Id = c.orgID + } else if c.vcsType != "" && c.orgName != "" { + params.Owner.Slug = toSlug(c.vcsType, c.orgName) + } else { + return fmt.Errorf("need either org ID or vcs type and org name to create a context, received none") + } + _, err := CreateContextWithRest(c.client, params) + return err +} + +func (c restClient) DeleteContext(contextID string) error { + _, err := DeleteContextWithRest(c.client, contextID) + return err +} + +func (c restClient) EnvironmentVariables(contextID string) ([]EnvironmentVariable, error) { + return ListAllEnvVarsWithRest(c.client, ListEnvVarsWithRestParams{ContextID: contextID}) +} + +func (c restClient) CreateEnvironmentVariable(contextID, variable, value string) error { + _, err := CreateEnvVarWithRest(c.client, CreateEnvVarWithRestParams{ + ContextID: contextID, + Name: variable, + Value: value, + }) + return err +} + +func (c restClient) DeleteEnvironmentVariable(contextID, variable string) error { + _, err := DeleteEnvVarWithRest(c.client, DeleteEnvVarWithRestParams{ContextID: contextID, Name: variable}) + return err +} diff --git a/api/context_graphql.go b/api/context_graphql.go deleted file mode 100644 index b1d2a2c02..000000000 --- a/api/context_graphql.go +++ /dev/null @@ -1,390 +0,0 @@ -// Go functions that expose the Context-related calls in the GraphQL API. -package api - -import ( - "fmt" - "net/http" - "strings" - "time" - - "github.com/CircleCI-Public/circleci-cli/api/graphql" - "github.com/pkg/errors" -) - -type GraphQLContextClient struct { - Client *graphql.Client -} - -type circleCIContext struct { - ID string - Name string - CreatedAt string - Groups struct { - } -} - -type contextsQueryResponse struct { - Organization struct { - Id string - Contexts struct { - Edges []struct { - Node circleCIContext - } - } - } -} - -func improveVcsTypeError(err error) error { - if responseErrors, ok := err.(graphql.ResponseErrorsCollection); ok { - if len(responseErrors) > 0 { - details := responseErrors[0].Extensions - if details.EnumType == "VCSType" { - allowedValues := strings.ToLower(strings.Join(details.AllowedValues[:], ", ")) - return fmt.Errorf("Invalid vcs-type '%s' provided, expected one of %s", strings.ToLower(details.Value), allowedValues) - } - } - } - return err -} - -// CreateContext creates a new Context in the supplied organization. -func (c *GraphQLContextClient) CreateContext(vcsType, orgName, contextName string) error { - cl := c.Client - - org, err := getOrganization(cl, orgName, vcsType) - if err != nil { - return err - } - - err = c.CreateContextWithOrgID(&org.Organization.ID, contextName) - if err != nil { - return err - } - - return nil -} - -// CreateContextWithOrgID creates a new Context in the supplied organization. -func (c *GraphQLContextClient) CreateContextWithOrgID(orgID *string, contextName string) error { - cl := c.Client - - query := ` - mutation CreateContext($input: CreateContextInput!) { - createContext(input: $input) { - ...CreateButton - } - } - - fragment CreateButton on CreateContextPayload { - error { - type - } - } - - ` - - var input struct { - OwnerId string `json:"ownerId"` - OwnerType string `json:"ownerType"` - ContextName string `json:"contextName"` - } - - input.OwnerId = *orgID - input.OwnerType = "ORGANIZATION" - input.ContextName = contextName - - request := graphql.NewRequest(query) - request.SetToken(cl.Token) - request.Var("input", input) - - var response struct { - CreateContext struct { - Error struct { - Type string - } - } - } - - if err := cl.Run(request, &response); err != nil { - return improveVcsTypeError(err) - } - - if response.CreateContext.Error.Type != "" { - return fmt.Errorf("Error creating context: %s", response.CreateContext.Error.Type) - } - return nil -} - -// ContextByName returns the Context in the given organization with the given -// name. -func (c *GraphQLContextClient) ContextByName(vcs, org, name string) (*Context, error) { - contexts, err := c.Contexts(vcs, org) - if err != nil { - return nil, err - } - for _, c := range *contexts { - if c.Name == name { - return &c, nil - } - } - return nil, errors.New("No context found with that name") -} - -// EnvironmentVariables returns all of the environment variables in this -// context. -func (c *GraphQLContextClient) EnvironmentVariables(contextID string) (*[]EnvironmentVariable, error) { - cl := c.Client - query := ` - query Context($id: ID!) { - context(id: $id) { - resources { - variable - createdAt - } - } - }` - request := graphql.NewRequest(query) - request.SetToken(cl.Token) - request.Var("id", contextID) - var resp struct { - Context struct { - Resources []EnvironmentVariable - } - } - err := cl.Run(request, &resp) - - if err != nil { - return nil, err - } - for _, ev := range resp.Context.Resources { - ev.ContextID = contextID - } - return &resp.Context.Resources, nil -} - -// Contexts returns all of the Contexts owned by this organization. -func (c *GraphQLContextClient) Contexts(vcsType, orgName string) (*[]Context, error) { - cl := c.Client - // In theory we can lookup the organization by name and its contexts in - // the same query, but using separate requests to circumvent a bug in - // the API - org, err := getOrganization(cl, orgName, vcsType) - - if err != nil { - return nil, err - } - - query := ` - query ContextsQuery($orgId: ID!) { - organization(id: $orgId) { - id - contexts { - edges { - node { - ...Context - } - } - } - } - } - - fragment Context on Context { - id - name - createdAt - groups { - edges { - node { - ...SecurityGroups - } - } - } - resources { - ...EnvVars - } - } - - fragment EnvVars on EnvironmentVariable { - variable - createdAt - truncatedValue - } - - fragment SecurityGroups on Group { - id - name - } - ` - - request := graphql.NewRequest(query) - request.SetToken(cl.Token) - - request.Var("orgId", org.Organization.ID) - - var response contextsQueryResponse - err = cl.Run(request, &response) - if err != nil { - return nil, errors.Wrapf(improveVcsTypeError(err), "failed to load context list") - } - var contexts []Context - for _, edge := range response.Organization.Contexts.Edges { - context := edge.Node - created_at, err := time.Parse(time.RFC3339, context.CreatedAt) - if err != nil { - return nil, err - } - contexts = append(contexts, Context{ - Name: context.Name, - ID: context.ID, - CreatedAt: created_at, - }) - } - - return &contexts, nil -} - -// DeleteEnvironmentVariable deletes the environment variable from the context. -// It returns an error if one occurred. It does not return an error if the -// environment variable did not exist. -func (c *GraphQLContextClient) DeleteEnvironmentVariable(contextId, variableName string) error { - cl := c.Client - query := ` - mutation DeleteEnvVar($input: RemoveEnvironmentVariableInput!) { - removeEnvironmentVariable(input: $input) { - context { - id - resources { - ...EnvVars - } - } - } - } - - fragment EnvVars on EnvironmentVariable { - variable - createdAt - truncatedValue - }` - - var input struct { - ContextId string `json:"contextId"` - Variable string `json:"variable"` - } - - input.ContextId = contextId - input.Variable = variableName - - request := graphql.NewRequest(query) - request.SetToken(cl.Token) - request.Var("input", input) - - var response struct { - RemoveEnvironmentVariable struct { - Context circleCIContext - } - } - - err := cl.Run(request, &response) - return errors.Wrap(improveVcsTypeError(err), "failed to delete environment variable") -} - -// CreateEnvironmentVariable creates a new environment variable in the given -// context. Note that the GraphQL API does not support upsert, so an error will -// be returned if the env var already exists. -func (c *GraphQLContextClient) CreateEnvironmentVariable(contextId, variableName, secretValue string) error { - cl := c.Client - query := ` - mutation CreateEnvVar($input: StoreEnvironmentVariableInput!) { - storeEnvironmentVariable(input: $input) { - context { - id - resources { - ...EnvVars - } - } - ...CreateEnvVarButton - } - } - - fragment EnvVars on EnvironmentVariable { - variable - createdAt - truncatedValue - } - - fragment CreateEnvVarButton on StoreEnvironmentVariablePayload { - error { - type - } - }` - - request := graphql.NewRequest(query) - request.SetToken(cl.Token) - - var input struct { - ContextId string `json:"contextId"` - Variable string `json:"variable"` - Value string `json:"value"` - } - - input.ContextId = contextId - input.Variable = variableName - input.Value = secretValue - - request.Var("input", input) - - var response struct { - StoreEnvironmentVariable struct { - Context circleCIContext - Error struct { - Type string - } - } - } - - if err := cl.Run(request, &response); err != nil { - return errors.Wrap(improveVcsTypeError(err), "failed to store environment variable in context") - } - - if response.StoreEnvironmentVariable.Error.Type != "" { - return fmt.Errorf("Error storing environment variable: %s", response.StoreEnvironmentVariable.Error.Type) - } - - return nil -} - -// DeleteContext will delete the context with the given ID. -func (c *GraphQLContextClient) DeleteContext(contextId string) error { - cl := c.Client - query := ` - mutation DeleteContext($input: DeleteContextInput!) { - deleteContext(input: $input) { - clientMutationId - } - }` - - request := graphql.NewRequest(query) - request.SetToken(cl.Token) - - var input struct { - ContextId string `json:"contextId"` - } - - input.ContextId = contextId - request.Var("input", input) - - var response struct { - } - - err := cl.Run(request, &response) - - return errors.Wrap(improveVcsTypeError(err), "failed to delete context") -} - -// NewContextGraphqlClient returns a new client satisfying the -// api.ContextInterface interface via the GraphQL API. -func NewContextGraphqlClient(httpClient *http.Client, host, endpoint, token string, debug bool) *GraphQLContextClient { - return &GraphQLContextClient{ - Client: graphql.NewClient(httpClient, host, endpoint, token, debug), - } -} diff --git a/api/context_rest.go b/api/context_rest.go deleted file mode 100644 index 17b2ff21d..000000000 --- a/api/context_rest.go +++ /dev/null @@ -1,620 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - - "github.com/CircleCI-Public/circleci-cli/api/header" - "github.com/CircleCI-Public/circleci-cli/settings" - "github.com/CircleCI-Public/circleci-cli/version" - "github.com/pkg/errors" -) - -// ContextRestClient communicates with the CircleCI REST API to ask questions -// about contexts. It satisfies api.ContextInterface. -type ContextRestClient struct { - token string - server string - client *http.Client -} - -type listEnvironmentVariablesResponse struct { - Items []EnvironmentVariable - NextPageToken *string `json:"next_page_token"` - client *ContextRestClient - params *listEnvironmentVariablesParams -} - -type listContextsResponse struct { - Items []Context - NextPageToken *string `json:"next_page_token"` - client *ContextRestClient - params *listContextsParams -} - -type errorResponse struct { - Message *string `json:"message"` -} - -type listContextsParams struct { - OwnerID *string - OwnerSlug *string - OwnerType *string - PageToken *string -} - -type listEnvironmentVariablesParams struct { - ContextID *string - PageToken *string -} - -func toSlug(vcs, org string) *string { - slug := fmt.Sprintf("%s/%s", vcs, org) - return &slug -} - -// DeleteEnvironmentVariable deletes the environment variable in the context. It -// does not return an error if the environment variable did not exist. -func (c *ContextRestClient) DeleteEnvironmentVariable(contextID, variable string) error { - req, err := c.newDeleteEnvironmentVariableRequest(contextID, variable) - if err != nil { - return err - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return err - } - - if resp.StatusCode != 200 { - var dest errorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return err - } - return errors.New(*dest.Message) - } - return nil -} - -func (c *ContextRestClient) CreateContextWithOrgID(orgID *string, name string) error { - req, err := c.newCreateContextRequestWithOrgID(orgID, name) - if err != nil { - return err - } - - resp, err := c.client.Do(req) - - if err != nil { - return err - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return err - } - if resp.StatusCode != 200 { - var dest errorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return err - } - return errors.New(*dest.Message) - } - var dest Context - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return err - } - return nil -} - -// CreateContext creates a new context in the supplied organization. -func (c *ContextRestClient) CreateContext(vcs, org, name string) error { - req, err := c.newCreateContextRequest(vcs, org, name) - if err != nil { - return err - } - - resp, err := c.client.Do(req) - - if err != nil { - return err - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return err - } - if resp.StatusCode != 200 { - var dest errorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return err - } - return errors.New(*dest.Message) - } - var dest Context - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return err - } - return nil -} - -// CreateEnvironmentVariable creates OR UPDATES an environment variable. -func (c *ContextRestClient) CreateEnvironmentVariable(contextID, variable, value string) error { - req, err := c.newCreateEnvironmentVariableRequest(contextID, variable, value) - if err != nil { - return err - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return err - } - if resp.StatusCode != 200 { - var dest errorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return err - } - return errors.New(*dest.Message) - } - return nil -} - -// DeleteContext deletes the context with the given ID. -func (c *ContextRestClient) DeleteContext(contextID string) error { - req, err := c.newDeleteContextRequest(contextID) - - if err != nil { - return err - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return err - } - if resp.StatusCode != 200 { - var dest errorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return err - } - return errors.New(*dest.Message) - } - return nil -} - -// EnvironmentVariables returns all of the environment variables owned by the -// given context. Note that pagination is not currently supported - we get all -// pages of env vars and return them all. -func (c *ContextRestClient) EnvironmentVariables(contextID string) (*[]EnvironmentVariable, error) { - envVars, error := c.listAllEnvironmentVariables( - &listEnvironmentVariablesParams{ - ContextID: &contextID, - }, - ) - return &envVars, error -} - -// Contexts returns all of the contexts owned by the given org. Note that -// pagination is not currently supported - we get all pages of contexts and -// return them all. -func (c *ContextRestClient) Contexts(vcs, org string) (*[]Context, error) { - contexts, error := c.listAllContexts( - &listContextsParams{ - OwnerSlug: toSlug(vcs, org), - }, - ) - return &contexts, error -} - -// ContextByName finds a single context by its name and returns it. -func (c *ContextRestClient) ContextByName(vcs, org, name string) (*Context, error) { - params := &listContextsParams{ - OwnerSlug: toSlug(vcs, org), - } - for { - resp, err := c.listContexts(params) - if err != nil { - return nil, err - } - for _, context := range resp.Items { - if context.Name == name { - return &context, nil - } - } - if resp.NextPageToken == nil { - return nil, fmt.Errorf("Cannot find context named '%s'", name) - } - params.PageToken = resp.NextPageToken - } -} - -func (c *ContextRestClient) listAllEnvironmentVariables(params *listEnvironmentVariablesParams) (envVars []EnvironmentVariable, err error) { - var resp *listEnvironmentVariablesResponse - for { - resp, err = c.listEnvironmentVariables(params) - if err != nil { - return nil, err - } - - envVars = append(envVars, resp.Items...) - - if resp.NextPageToken == nil { - break - } - - params.PageToken = resp.NextPageToken - } - return envVars, nil -} - -func (c *ContextRestClient) listAllContexts(params *listContextsParams) (contexts []Context, err error) { - var resp *listContextsResponse - for { - resp, err = c.listContexts(params) - if err != nil { - return nil, err - } - - contexts = append(contexts, resp.Items...) - - if resp.NextPageToken == nil { - break - } - - params.PageToken = resp.NextPageToken - } - return contexts, nil -} - -func (c *ContextRestClient) listEnvironmentVariables(params *listEnvironmentVariablesParams) (*listEnvironmentVariablesResponse, error) { - req, err := c.newListEnvironmentVariablesRequest(params) - if err != nil { - return nil, err - } - - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return nil, err - } - if resp.StatusCode != 200 { - var dest errorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - return nil, errors.New(*dest.Message) - - } - dest := listEnvironmentVariablesResponse{ - client: c, - params: params, - } - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - return &dest, nil -} - -func (c *ContextRestClient) listContexts(params *listContextsParams) (*listContextsResponse, error) { - req, err := c.newListContextsRequest(params) - if err != nil { - return nil, err - } - - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return nil, err - } - if resp.StatusCode != 200 { - var dest errorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - - } - return nil, errors.New(*dest.Message) - - } - - dest := listContextsResponse{ - client: c, - params: params, - } - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - return &dest, nil -} - -// newCreateContextRequest posts a new context creation with orgname and vcs type using a slug -func (c *ContextRestClient) newCreateContextRequest(vcs, org, name string) (*http.Request, error) { - var err error - queryURL, err := url.Parse(c.server) - if err != nil { - return nil, err - } - queryURL, err = queryURL.Parse("context") - if err != nil { - return nil, err - } - - var bodyReader io.Reader - - var body = struct { - Name string `json:"name"` - Owner struct { - Slug *string `json:"slug,omitempty"` - } `json:"owner"` - }{ - Name: name, - Owner: struct { - Slug *string `json:"slug,omitempty"` - }{ - - Slug: toSlug(vcs, org), - }, - } - buf, err := json.Marshal(body) - - if err != nil { - return nil, err - } - - bodyReader = bytes.NewReader(buf) - - return c.newHTTPRequest("POST", queryURL.String(), bodyReader) -} - -// newCreateContextRequestWithOrgID posts a new context creation with an orgID -func (c *ContextRestClient) newCreateContextRequestWithOrgID(orgID *string, name string) (*http.Request, error) { - var err error - queryURL, err := url.Parse(c.server) - if err != nil { - return nil, err - } - queryURL, err = queryURL.Parse("context") - if err != nil { - return nil, err - } - - var bodyReader io.Reader - - var body = struct { - Name string `json:"name"` - Owner struct { - ID *string `json:"id,omitempty"` - } `json:"owner"` - }{ - Name: name, - Owner: struct { - ID *string `json:"id,omitempty"` - }{ - - ID: orgID, - }, - } - buf, err := json.Marshal(body) - - if err != nil { - return nil, err - } - - bodyReader = bytes.NewReader(buf) - - return c.newHTTPRequest("POST", queryURL.String(), bodyReader) -} - -func (c *ContextRestClient) newCreateEnvironmentVariableRequest(contextID, variable, value string) (*http.Request, error) { - var err error - queryURL, err := url.Parse(c.server) - if err != nil { - return nil, err - } - queryURL, err = queryURL.Parse(fmt.Sprintf("context/%s/environment-variable/%s", contextID, variable)) - if err != nil { - return nil, err - } - - var bodyReader io.Reader - body := struct { - Value string `json:"value"` - }{ - Value: value, - } - buf, err := json.Marshal(body) - - if err != nil { - return nil, err - } - - bodyReader = bytes.NewReader(buf) - - return c.newHTTPRequest("PUT", queryURL.String(), bodyReader) -} - -func (c *ContextRestClient) newDeleteEnvironmentVariableRequest(contextID, name string) (*http.Request, error) { - var err error - queryURL, err := url.Parse(c.server) - if err != nil { - return nil, err - } - queryURL, err = queryURL.Parse(fmt.Sprintf("context/%s/environment-variable/%s", contextID, name)) - if err != nil { - return nil, err - } - return c.newHTTPRequest("DELETE", queryURL.String(), nil) -} - -func (c *ContextRestClient) newDeleteContextRequest(contextID string) (*http.Request, error) { - var err error - queryURL, err := url.Parse(c.server) - if err != nil { - return nil, err - } - queryURL, err = queryURL.Parse(fmt.Sprintf("context/%s", contextID)) - if err != nil { - return nil, err - } - return c.newHTTPRequest("DELETE", queryURL.String(), nil) -} - -func (c *ContextRestClient) newListEnvironmentVariablesRequest(params *listEnvironmentVariablesParams) (*http.Request, error) { - var err error - queryURL, err := url.Parse(c.server) - if err != nil { - return nil, err - } - queryURL, err = queryURL.Parse(fmt.Sprintf("context/%s/environment-variable", *params.ContextID)) - if err != nil { - return nil, err - } - urlParams := url.Values{} - if params.PageToken != nil { - urlParams.Add("page-token", *params.PageToken) - } - queryURL.RawQuery = urlParams.Encode() - - return c.newHTTPRequest("GET", queryURL.String(), nil) -} - -func (c *ContextRestClient) newListContextsRequest(params *listContextsParams) (*http.Request, error) { - var err error - queryURL, err := url.Parse(c.server) - if err != nil { - return nil, err - } - queryURL, err = queryURL.Parse("context") - if err != nil { - return nil, err - } - - urlParams := url.Values{} - if params.OwnerID != nil { - urlParams.Add("owner-id", *params.OwnerID) - } - if params.OwnerSlug != nil { - urlParams.Add("owner-slug", *params.OwnerSlug) - } - if params.OwnerType != nil { - urlParams.Add("owner-type", *params.OwnerType) - } - if params.PageToken != nil { - urlParams.Add("page-token", *params.PageToken) - } - - queryURL.RawQuery = urlParams.Encode() - - return c.newHTTPRequest("GET", queryURL.String(), nil) -} - -func (c *ContextRestClient) newHTTPRequest(method, url string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, url, body) - if err != nil { - return nil, err - } - if c.token != "" { - req.Header.Add("circle-token", c.token) - } - req.Header.Add("Accept", "application/json") - req.Header.Add("Content-Type", "application/json") - req.Header.Add("User-Agent", version.UserAgent()) - commandStr := header.GetCommandStr() - if commandStr != "" { - req.Header.Add("Circleci-Cli-Command", commandStr) - } - return req, nil -} - -// EnsureExists verifies that the REST API exists and has the necessary -// endpoints to interact with contexts and env vars. -func (c *ContextRestClient) EnsureExists() error { - queryURL, err := url.Parse(c.server) - if err != nil { - return err - } - queryURL, err = queryURL.Parse("openapi.json") - if err != nil { - return err - } - req, err := c.newHTTPRequest("GET", queryURL.String(), nil) - if err != nil { - return err - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - if resp.StatusCode != 200 { - return errors.New("API v2 test request failed.") - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return err - } - var respBody struct { - Paths struct { - ContextEndpoint interface{} `json:"/context"` - } - } - if err := json.Unmarshal(bodyBytes, &respBody); err != nil { - return err - } - - if respBody.Paths.ContextEndpoint == nil { - return errors.New("No context endpoint exists") - } - - return nil -} - -// NewContextRestClient returns a new client satisfying the api.ContextInterface -// interface via the REST API. -func NewContextRestClient(config settings.Config) (*ContextRestClient, error) { - serverURL, err := config.ServerURL() - if err != nil { - return nil, err - } - - client := &ContextRestClient{ - token: config.Token, - server: serverURL.String(), - client: config.HTTPClient, - } - - return client, nil -} diff --git a/api/context_rest_test.go b/api/context_rest_test.go deleted file mode 100644 index 005c64908..000000000 --- a/api/context_rest_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package api - -import ( - "fmt" - "io" - "net/http" - - "github.com/CircleCI-Public/circleci-cli/settings" - "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/ghttp" -) - -type MockRequestResponse struct { - Request string - Status int - Response string - ErrorResponse string -} - -// Uses Ginkgo http handler to mock out http requests and make assertions off the results. -// If ErrorResponse is defined in the passed handler it will override the Response. -func appendRESTPostHandler(server *ghttp.Server, combineHandlers ...MockRequestResponse) { - for _, handler := range combineHandlers { - responseBody := handler.Response - if handler.ErrorResponse != "" { - responseBody = handler.ErrorResponse - } - - server.AppendHandlers( - ghttp.CombineHandlers( - ghttp.VerifyRequest("POST", "/api/v2/context"), - ghttp.VerifyContentType("application/json"), - func(w http.ResponseWriter, req *http.Request) { - body, err := io.ReadAll(req.Body) - Expect(err).ShouldNot(HaveOccurred()) - err = req.Body.Close() - Expect(err).ShouldNot(HaveOccurred()) - Expect(handler.Request).Should(MatchJSON(body), "JSON Mismatch") - }, - ghttp.RespondWith(handler.Status, responseBody), - ), - ) - } -} - -func getContextRestClient(server *ghttp.Server) (*ContextRestClient, error) { - client := &http.Client{} - - return NewContextRestClient(settings.Config{ - RestEndpoint: "api/v2", - Host: server.URL(), - HTTPClient: client, - Token: "token", - }) -} - -var _ = ginkgo.Describe("Context Rest Tests", func() { - ginkgo.It("Should handle a successful request with createContextWithOrgID", func() { - server := ghttp.NewServer() - - defer server.Close() - - name := "name" - orgID := "497f6eca-6276-4993-bfeb-53cbbbba6f08" - client, err := getContextRestClient(server) - Expect(err).To(BeNil()) - - appendRESTPostHandler(server, MockRequestResponse{ - Status: http.StatusOK, - Request: fmt.Sprintf(`{"name": "%s","owner":{"id":"%s"}}`, name, orgID), - Response: fmt.Sprintf(`{"id": "%s", "name": "%s", "created_at": "2015-09-21T17:29:21.042Z" }`, orgID, name), - }) - - err = client.CreateContextWithOrgID(&orgID, name) - Expect(err).To(BeNil()) - }) - - ginkgo.It("Should handle an error request with createContextWithOrgID", func() { - server := ghttp.NewServer() - - defer server.Close() - - name := "name" - orgID := "497f6eca-6276-4993-bfeb-53cbbbba6f08" - client, err := getContextRestClient(server) - Expect(err).To(BeNil()) - - appendRESTPostHandler(server, MockRequestResponse{ - Status: http.StatusInternalServerError, - Request: fmt.Sprintf(`{"name": "%s","owner":{"id":"%s"}}`, name, orgID), - ErrorResponse: `{"message": "🍎"}`, - }) - - err = client.CreateContextWithOrgID(&orgID, name) - Expect(err).ToNot(BeNil()) - }) -}) diff --git a/api/context_test.go b/api/context_test.go deleted file mode 100644 index fb6d188dd..000000000 --- a/api/context_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package api - -import ( - "encoding/json" - "errors" - "net/http" - "net/http/httptest" - "sync/atomic" - - "github.com/CircleCI-Public/circleci-cli/api/graphql" - - // we can't dot-import ginkgo because api.Context is a thing. - "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -type graphQLRequest struct { - Query string - Variables map[string]interface{} -} - -func createSingleUseGraphQLServer(result interface{}, requestAssertions func(requestCount uint64, req *graphQLRequest)) (*httptest.Server, *GraphQLContextClient) { - response := graphql.Response{ - Data: result, - } - - var requestCount uint64 = 0 - - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - atomic.AddUint64(&requestCount, 1) - defer ginkgo.GinkgoRecover() - var request graphQLRequest - Expect(json.NewDecoder(req.Body).Decode(&request)).To(Succeed()) - requestAssertions(requestCount, &request) - bytes, err := json.Marshal(response) - Expect(err).ToNot(HaveOccurred()) - _, err = rw.Write(bytes) - Expect(err).ToNot(HaveOccurred()) - })) - client := NewContextGraphqlClient(http.DefaultClient, server.URL, server.URL, "token", false) - return server, client -} - -var _ = ginkgo.Describe("API", func() { - orgID := "bb604b45-b6b0-4b81-ad80-796f15eddf87" - - ginkgo.Describe("FooBar", func() { - ginkgo.It("improveVcsTypeError", func() { - - unrelatedError := errors.New("foo") - - Expect(unrelatedError).Should(Equal(improveVcsTypeError(unrelatedError))) - - errors := []graphql.ResponseError{ - { - Message: "foo", - }, - } - - errors[0].Extensions.EnumType = "VCSType" - errors[0].Extensions.Value = "pear" - errors[0].Extensions.AllowedValues = []string{"apple", "banana"} - var vcsError graphql.ResponseErrorsCollection = errors - Expect("Invalid vcs-type 'pear' provided, expected one of apple, banana").Should(Equal(improveVcsTypeError(vcsError).Error())) - - }) - }) - - ginkgo.Describe("Create Context", func() { - - ginkgo.It("can handles failure creating contexts", func() { - - var result struct { - CreateContext struct { - Error struct { - Type string - } - } - } - - result.CreateContext.Error.Type = "force-this-error" - - server, client := createSingleUseGraphQLServer(result, func(count uint64, req *graphQLRequest) { - switch count { - case 1: - Expect(req.Variables["organizationName"]).To(Equal("test-org")) - Expect(req.Variables["organizationVcs"]).To(Equal("TEST-VCS")) - case 2: - Expect(req.Variables["input"].(map[string]interface{})["ownerType"]).To(Equal("ORGANIZATION")) - Expect(req.Variables["input"].(map[string]interface{})["contextName"]).To(Equal("foo-bar")) - } - }) - defer server.Close() - err := client.CreateContext("test-vcs", "test-org", "foo-bar") - Expect(err).To(MatchError("Error creating context: force-this-error")) - - }) - - ginkgo.It("can handles failure creating contexts", func() { - - var result struct { - CreateContext struct { - Error struct { - Type string - } - } - } - - result.CreateContext.Error.Type = "force-this-error" - - server, client := createSingleUseGraphQLServer(result, func(count uint64, req *graphQLRequest) { - switch count { - case 1: - Expect(req.Variables["input"].(map[string]interface{})["ownerId"]).To(Equal(orgID)) - } - }) - defer server.Close() - err := client.CreateContextWithOrgID(&orgID, "foo-bar") - Expect(err).To(MatchError("Error creating context: force-this-error")) - - }) - - }) - - ginkgo.It("can handles success creating contexts", func() { - - var result struct { - CreateContext struct { - Error struct { - Type string - } - } - } - - result.CreateContext.Error.Type = "" - - server, client := createSingleUseGraphQLServer(result, func(count uint64, req *graphQLRequest) { - - switch count { - case 1: - Expect(req.Variables["organizationName"]).To(Equal("test-org")) - Expect(req.Variables["organizationVcs"]).To(Equal("TEST-VCS")) - case 2: - Expect(req.Variables["input"].(map[string]interface{})["ownerType"]).To(Equal("ORGANIZATION")) - Expect(req.Variables["input"].(map[string]interface{})["contextName"]).To(Equal("foo-bar")) - } - - }) - defer server.Close() - - Expect(client.CreateContext("test-vcs", "test-org", "foo-bar")).To(Succeed()) - - }) - - ginkgo.It("can handles success creating contexts with create context with orgID", func() { - - var result struct { - CreateContext struct { - Error struct { - Type string - } - } - } - - result.CreateContext.Error.Type = "" - - server, client := createSingleUseGraphQLServer(result, func(count uint64, req *graphQLRequest) { - switch count { - case 1: - Expect(req.Variables["input"].(map[string]interface{})["ownerId"]).To(Equal(orgID)) - } - }) - defer server.Close() - Expect(client.CreateContextWithOrgID(&orgID, "foo-bar")).To(Succeed()) - - }) - - ginkgo.Describe("List Contexts", func() { - - ginkgo.It("can list contexts", func() { - - ctx := circleCIContext{ - CreatedAt: "2018-04-24T19:38:37.212Z", - Name: "Sheep", - } - - list := contextsQueryResponse{} - - list.Organization.Id = "C3D79A95-6BD5-40B4-9958-AB6BDC4CAD50" - list.Organization.Contexts.Edges = []struct{ Node circleCIContext }{ - { - Node: ctx, - }, - } - - server, client := createSingleUseGraphQLServer(list, func(count uint64, req *graphQLRequest) { - switch count { - case 1: - Expect(req.Variables["organizationName"]).To(Equal("test-org")) - Expect(req.Variables["organizationVcs"]).To(Equal("TEST-VCS")) - case 2: - Expect(req.Variables["orgId"]).To(Equal("C3D79A95-6BD5-40B4-9958-AB6BDC4CAD50")) - } - }) - defer server.Close() - - result, err := client.Contexts("test-vcs", "test-org") - Expect(err).NotTo(HaveOccurred()) - context := (*result)[0] - Expect(context.Name).To(Equal("Sheep")) - }) - }) -}) diff --git a/api/rest/client.go b/api/rest/client.go index d35363d91..b6500bec2 100644 --- a/api/rest/client.go +++ b/api/rest/client.go @@ -3,7 +3,6 @@ package rest import ( "bytes" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -101,24 +100,25 @@ func (c *Client) DoRequest(req *http.Request, resp interface{}) (int, error) { } defer httpResp.Body.Close() - if httpResp.StatusCode >= 300 { - httpError := struct { + if httpResp.StatusCode >= 400 { + var msgErr struct { Message string `json:"message"` - }{} + } body, err := io.ReadAll(httpResp.Body) if err != nil { - return 0, err + return httpResp.StatusCode, err } - err = json.Unmarshal(body, &httpError) + err = json.Unmarshal(body, &msgErr) if err != nil { return httpResp.StatusCode, &HTTPError{Code: httpResp.StatusCode, Message: string(body)} } - return httpResp.StatusCode, &HTTPError{Code: httpResp.StatusCode, Message: httpError.Message} + return httpResp.StatusCode, &HTTPError{Code: httpResp.StatusCode, Message: msgErr.Message} } if resp != nil { if !strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") { - return httpResp.StatusCode, errors.New("wrong content type received") + body, _ := io.ReadAll(httpResp.Body) + return httpResp.StatusCode, fmt.Errorf("wrong content type received. method: %s. path: %s. content-type: %s. body: %s", req.Method, req.URL.Path, httpResp.Header.Get("Content-Type"), string(body)) } err = json.NewDecoder(httpResp.Body).Decode(resp) diff --git a/api/schedule_rest.go b/api/schedule_rest.go index efa8fac5c..210bc271c 100644 --- a/api/schedule_rest.go +++ b/api/schedule_rest.go @@ -55,11 +55,11 @@ func (c *ScheduleRestClient) CreateSchedule(vcs, org, project, name, description } if resp.StatusCode != 201 { - var dest errorResponse + var dest ErrorWithMessage if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - return nil, errors.New(*dest.Message) + return nil, dest } var schedule Schedule @@ -92,11 +92,11 @@ func (c *ScheduleRestClient) UpdateSchedule(scheduleID, name, description string } if resp.StatusCode != 200 { - var dest errorResponse + var dest ErrorWithMessage if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - return nil, errors.New(*dest.Message) + return nil, dest } var schedule Schedule @@ -126,11 +126,11 @@ func (c *ScheduleRestClient) DeleteSchedule(scheduleID string) error { return err } if resp.StatusCode != 200 { - var dest errorResponse + var dest ErrorWithMessage if err := json.Unmarshal(bodyBytes, &dest); err != nil { return err } - return errors.New(*dest.Message) + return dest } return nil } @@ -162,11 +162,11 @@ func (c *ScheduleRestClient) ScheduleByID(scheduleID string) (*Schedule, error) return nil, err } if resp.StatusCode != 200 { - var dest errorResponse + var dest ErrorWithMessage if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - return nil, errors.New(*dest.Message) + return nil, dest } schedule := Schedule{} @@ -236,12 +236,11 @@ func (c *ScheduleRestClient) listSchedules(vcs, org, project string, params *lis return nil, err } if resp.StatusCode != 200 { - var dest errorResponse + var dest ErrorWithMessage if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err - } - return nil, errors.New(*dest.Message) + return nil, dest } diff --git a/clitest/clitest.go b/clitest/clitest.go index 24b0b25bc..b47b8c17e 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -102,30 +102,6 @@ type MockRequestResponse struct { ErrorResponse string } -func (tempSettings *TempSettings) AppendRESTPostHandler(combineHandlers ...MockRequestResponse) { - for _, handler := range combineHandlers { - responseBody := handler.Response - if handler.ErrorResponse != "" { - responseBody = handler.ErrorResponse - } - - tempSettings.TestServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.VerifyRequest("POST", "/api/v2/context"), - ghttp.VerifyContentType("application/json"), - func(w http.ResponseWriter, req *http.Request) { - body, err := io.ReadAll(req.Body) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - err = req.Body.Close() - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(handler.Request).Should(gomega.MatchJSON(body), "JSON Mismatch") - }, - ghttp.RespondWith(handler.Status, responseBody), - ), - ) - } -} - // AppendPostHandler stubs out the provided MockRequestResponse. // When authToken is an empty string no token validation is performed. func (tempSettings *TempSettings) AppendPostHandler(authToken string, combineHandlers ...MockRequestResponse) { diff --git a/cmd/.circleci/update_check.yml b/cmd/.circleci/update_check.yml deleted file mode 100644 index b4e5bdc0f..000000000 --- a/cmd/.circleci/update_check.yml +++ /dev/null @@ -1 +0,0 @@ -last_update_check: 2023-12-11T13:24:04.24843+01:00 diff --git a/cmd/context.go b/cmd/context.go index bc9ae2d8f..703ffb8d1 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -1,15 +1,15 @@ package cmd import ( - "bufio" "fmt" "io" "os" + "strconv" "strings" "time" - "github.com/CircleCI-Public/circleci-cli/api" - "github.com/google/uuid" + "github.com/CircleCI-Public/circleci-cli/api/context" + "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/olekukonko/tablewriter" "github.com/pkg/errors" @@ -18,115 +18,154 @@ import ( ) var ( - orgID *string + orgIDUsage = "Your organization id. You can obtain it on the \"Overview\" section of your organization settings page." + + orgID string + vcsType string + orgName string + initiatedArgs []string integrationTesting bool ) +func MultiExactArgs(numbers ...int) cobra.PositionalArgs { + numList := make([]string, len(numbers)) + for i, n := range numbers { + numList[i] = strconv.Itoa(n) + } + + return func(cmd *cobra.Command, args []string) error { + for _, n := range numbers { + if len(args) == n { + return nil + } + } + return fmt.Errorf("accepts %s arg(s), received %d", strings.Join(numList, ", "), len(args)) + } +} + func newContextCommand(config *settings.Config) *cobra.Command { - var contextClient api.ContextInterface + var contextClient context.ContextInterface initClient := func(cmd *cobra.Command, args []string) (e error) { - contextClient, e = api.NewContextRestClient(*config) - if e != nil { - return e + initiatedArgs = args + if orgID == "" && len(args) < 2 { + _ = cmd.Usage() + return errors.New("need to define either --org-id or and arguments") } - - // Ensure does not fallback to graph for testing. - if integrationTesting { - return validateToken(config) + if orgID != "" { + contextClient = context.NewContextClient(config, orgID, "", "") } - - // If we're on cloud, we're good. - if config.Host == defaultHost || contextClient.(*api.ContextRestClient).EnsureExists() == nil { - return validateToken(config) + if orgID == "" && len(args) >= 2 { + vcsType = args[0] + orgName = args[1] + initiatedArgs = args[2:] + contextClient = context.NewContextClient(config, "", vcsType, orgName) } - contextClient = api.NewContextGraphqlClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) - return validateToken(config) } command := &cobra.Command{ Use: "context", - Long: ` -Contexts provide a mechanism for securing and sharing environment variables across -projects. The environment variables are defined as name/value pairs and + Long: `Contexts provide a mechanism for securing and sharing environment variables across +projects. The environment variables are defined as name/value pairs and are injected at runtime.`, - Short: "For securing and sharing environment variables across projects"} + Short: "For securing and sharing environment variables across projects", + } listCommand := &cobra.Command{ Short: "List all contexts", - Use: "list ", + Use: "list --org-id ", PreRunE: initClient, RunE: func(cmd *cobra.Command, args []string) error { - return listContexts(contextClient, args[0], args[1]) + return listContexts(contextClient) }, - Args: cobra.ExactArgs(2), + Args: MultiExactArgs(0, 2), + Example: `circleci context list --org-id 00000000-0000-0000-0000-000000000000 +(deprecated usage) circleci context list `, } + listCommand.Flags().StringVar(&orgID, "org-id", "", orgIDUsage) showContextCommand := &cobra.Command{ Short: "Show a context", - Use: "show ", + Use: "show --org-id ", PreRunE: initClient, RunE: func(cmd *cobra.Command, args []string) error { - return showContext(contextClient, args[0], args[1], args[2]) + return showContext(contextClient, initiatedArgs[0]) }, - Args: cobra.ExactArgs(3), + Args: MultiExactArgs(1, 3), + Example: `circleci context show --org-id --org-id 00000000-0000-0000-0000-000000000000 contextName +(deprecated usage) circleci context show github orgName contextName`, } + showContextCommand.Flags().StringVar(&orgID, "org-id", "", orgIDUsage) storeCommand := &cobra.Command{ Short: "Store a new environment variable in the named context. The value is read from stdin.", - Use: "store-secret ", + Use: "store-secret --org-id ", PreRunE: initClient, RunE: func(cmd *cobra.Command, args []string) error { - return storeEnvVar(contextClient, args[0], args[1], args[2], args[3]) + var prompt storeEnvVarPrompt + if integrationTesting { + prompt = testPrompt{"value"} + } else { + prompt = secretPrompt{} + } + return storeEnvVar(contextClient, prompt, initiatedArgs[0], initiatedArgs[1]) }, - Args: cobra.ExactArgs(4), + Args: MultiExactArgs(2, 4), + Example: `circleci context store-secret --org-id 00000000-0000-0000-0000-000000000000 contextName secretName +(deprecated usage) circleci context store-secret github orgName contextName secretName`, + } + storeCommand.Flags().StringVar(&orgID, "org-id", "", orgIDUsage) + storeCommand.Flags().BoolVar(&integrationTesting, "integration-testing", false, "Enable test mode to setup rest API") + if err := storeCommand.Flags().MarkHidden("integration-testing"); err != nil { + panic(err) } removeCommand := &cobra.Command{ Short: "Remove an environment variable from the named context", - Use: "remove-secret ", + Use: "remove-secret --org-id ", PreRunE: initClient, RunE: func(cmd *cobra.Command, args []string) error { - return removeEnvVar(contextClient, args[0], args[1], args[2], args[3]) + return removeEnvVar(contextClient, initiatedArgs[0], initiatedArgs[1]) }, - Args: cobra.ExactArgs(4), + Args: MultiExactArgs(2, 4), + Example: `circleci context remove-secret --org-id 00000000-0000-0000-0000-000000000000 contextName secretName +(deprecated usage) circleci context remove-secret github orgName contextName secretName`, } + removeCommand.Flags().StringVar(&orgID, "org-id", "", orgIDUsage) createContextCommand := &cobra.Command{ Short: "Create a new context", - Use: "create [] [] ", + Use: "create --org-id ", PreRunE: initClient, RunE: func(cmd *cobra.Command, args []string) error { - return createContext(cmd, contextClient, args) + return createContext(contextClient, initiatedArgs[0]) }, - Args: cobra.RangeArgs(1, 3), - Annotations: make(map[string]string), - Example: ` circleci context create github OrgName contextName -circleci context create contextName --org-id "your-org-id-here"`, + Args: MultiExactArgs(1, 3), + Example: `circleci context create --org-id 00000000-0000-0000-0000-000000000000 contextName +(deprecated usage) circleci context create github OrgName contextName`, + } + createContextCommand.Flags().StringVar(&orgID, "org-id", "", orgIDUsage) + createContextCommand.Flags().BoolVar(&integrationTesting, "integration-testing", false, "Enable test mode to setup rest API") + if err := createContextCommand.Flags().MarkHidden("integration-testing"); err != nil { + panic(err) } - createContextCommand.Annotations["[]"] = `Your VCS provider, can be either "github" or "bitbucket". Optional when passing org-id flag.` - createContextCommand.Annotations["[]"] = `The name used for your organization. Optional when passing org-id flag.` force := false deleteContextCommand := &cobra.Command{ Short: "Delete the named context", - Use: "delete ", + Use: "delete --org-id ", PreRunE: initClient, RunE: func(cmd *cobra.Command, args []string) error { - return deleteContext(contextClient, force, args[0], args[1], args[2]) + return deleteContext(contextClient, force, initiatedArgs[0]) }, - Args: cobra.ExactArgs(3), + Args: MultiExactArgs(1, 3), + Example: `circleci context delete --org-id 00000000-0000-0000-0000-000000000000 contextName +(deprecated usage) circleci context create github OrgName contextName`, } - deleteContextCommand.Flags().BoolVarP(&force, "force", "f", false, "Delete the context without asking for confirmation.") - - orgID = createContextCommand.Flags().String("org-id", "", "The id of your organization.") - createContextCommand.Flags().BoolVar(&integrationTesting, "integration-testing", false, "Enable test mode to setup rest API") - if err := createContextCommand.Flags().MarkHidden("integration-testing"); err != nil { - panic(err) - } + deleteContextCommand.Flags().StringVar(&orgID, "org-id", "", orgIDUsage) command.AddCommand(listCommand) command.AddCommand(showContextCommand) @@ -138,32 +177,27 @@ circleci context create contextName --org-id "your-org-id-here"`, return command } -func listContexts(contextClient api.ContextInterface, vcs, org string) error { - contexts, err := contextClient.Contexts(vcs, org) - +func listContexts(contextClient context.ContextInterface) error { + contexts, err := contextClient.Contexts() if err != nil { return err } table := tablewriter.NewWriter(os.Stdout) - - table.SetHeader([]string{"Provider", "Organization", "Name", "Created At"}) - - for _, context := range *contexts { + table.SetHeader([]string{"Id", "Name", "Created At"}) + for _, context := range contexts { table.Append([]string{ - vcs, - org, + context.ID, context.Name, context.CreatedAt.Format(time.RFC3339), }) } table.Render() - return nil } -func showContext(client api.ContextInterface, vcsType, orgName, contextName string) error { - context, err := client.ContextByName(vcsType, orgName, contextName) +func showContext(client context.ContextInterface, contextName string) error { + context, err := client.ContextByName(contextName) if err != nil { return err } @@ -178,7 +212,7 @@ func showContext(client api.ContextInterface, vcsType, orgName, contextName stri table.SetHeader([]string{"Environment Variable", "Value"}) - for _, envVar := range *envVars { + for _, envVar := range envVars { table.Append([]string{envVar.Variable, "••••"}) } table.Render() @@ -186,59 +220,65 @@ func showContext(client api.ContextInterface, vcsType, orgName, contextName stri return nil } -func readSecretValue() (string, error) { - stat, _ := os.Stdin.Stat() - if (stat.Mode() & os.ModeCharDevice) == 0 { - bytes, err := io.ReadAll(os.Stdin) - return string(bytes), err - } else { - fmt.Print("Enter secret value and press enter: ") - reader := bufio.NewReader(os.Stdin) - str, err := reader.ReadString('\n') - return strings.TrimRight(str, "\n"), err - } -} - // createContext determines if the context is being created via orgid or vcs and org name // and navigates to corresponding function accordingly -func createContext(cmd *cobra.Command, client api.ContextInterface, args []string) error { - //skip if no orgid provided - if orgID != nil && strings.TrimSpace(*orgID) != "" && len(args) == 1 { - _, err := uuid.Parse(*orgID) - - if err == nil { - return client.CreateContextWithOrgID(orgID, args[0]) - } - - //skip if no vcs type and org name provided - } else if len(args) == 3 { - return client.CreateContext(args[0], args[1], args[2]) +func createContext(client context.ContextInterface, name string) error { + err := client.CreateContext(name) + if err == nil { + fmt.Printf("Created context %s.\n", name) } - return cmd.Help() + return err } -func removeEnvVar(client api.ContextInterface, vcsType, orgName, contextName, varName string) error { - context, err := client.ContextByName(vcsType, orgName, contextName) +func removeEnvVar(client context.ContextInterface, contextName, varName string) error { + context, err := client.ContextByName(contextName) if err != nil { return err } - return client.DeleteEnvironmentVariable(context.ID, varName) + err = client.DeleteEnvironmentVariable(context.ID, varName) + if err == nil { + fmt.Printf("Removed secret %s from context %s.\n", varName, contextName) + } + return err } -func storeEnvVar(client api.ContextInterface, vcsType, orgName, contextName, varName string) error { +type storeEnvVarPrompt interface { + askForValue() (string, error) +} - context, err := client.ContextByName(vcsType, orgName, contextName) +type secretPrompt struct{} +func (secretPrompt) askForValue() (string, error) { + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + bytes, err := io.ReadAll(os.Stdin) + return string(bytes), err + } else { + return prompt.ReadSecretStringFromUser("Enter secret value") + } +} + +type testPrompt struct{ value string } + +func (me testPrompt) askForValue() (string, error) { + return me.value, nil +} + +func storeEnvVar(client context.ContextInterface, prompt storeEnvVarPrompt, contextName, varName string) error { + context, err := client.ContextByName(contextName) if err != nil { return err } - secretValue, err := readSecretValue() + secretValue, err := prompt.askForValue() if err != nil { return errors.Wrap(err, "Failed to read secret value from stdin") } err = client.CreateEnvironmentVariable(context.ID, varName, secretValue) + if err != nil { + fmt.Printf("Saved environment variable %s in context %s.\n", varName, contextName) + } return err } @@ -251,16 +291,13 @@ func askForConfirmation(message string) bool { return strings.HasPrefix(strings.ToLower(response), "y") } -func deleteContext(client api.ContextInterface, force bool, vcsType, orgName, contextName string) error { - - context, err := client.ContextByName(vcsType, orgName, contextName) - +func deleteContext(client context.ContextInterface, force bool, contextName string) error { + context, err := client.ContextByName(contextName) if err != nil { return err } - message := fmt.Sprintf("Are you sure that you want to delete this context: %s/%s %s (y/n)?", - vcsType, orgName, context.Name) + message := fmt.Sprintf("Are you sure that you want to delete this context: %s (y/n)?", context.Name) shouldDelete := force || askForConfirmation(message) @@ -268,5 +305,9 @@ func deleteContext(client api.ContextInterface, force bool, vcsType, orgName, co return errors.New("OK, cancelling") } - return client.DeleteContext(context.ID) + err = client.DeleteContext(context.ID) + if err == nil { + fmt.Printf("Deleted context %s.\n", contextName) + } + return err } diff --git a/cmd/context_test.go b/cmd/context_test.go index 619ed2089..5ab8207e2 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -1,17 +1,40 @@ package cmd_test import ( + "encoding/json" "fmt" "net/http" "os/exec" + "strings" + "time" + "github.com/CircleCI-Public/circleci-cli/api/context" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/google/uuid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/ghttp" ) +var ( + contentTypeHeader http.Header = map[string][]string{"Content-Type": {"application/json"}} +) + +func mockServerForREST(tempSettings *clitest.TempSettings) { + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/openapi.json"), + ghttp.RespondWith( + http.StatusOK, + `{"paths":{"/context":{}}}`, + contentTypeHeader, + ), + ), + ) +} + var _ = Describe("Context integration tests", func() { var ( tempSettings *clitest.TempSettings @@ -19,8 +42,9 @@ var _ = Describe("Context integration tests", func() { command *exec.Cmd contextName string = "foo-context" orgID string = "bb604b45-b6b0-4b81-ad80-796f15eddf87" - vcsType string = "BITBUCKET" + vcsType string = "bitbucket" orgName string = "test-org" + orgSlug string = fmt.Sprintf("%s/%s", vcsType, orgName) ) BeforeEach(func() { @@ -31,106 +55,580 @@ var _ = Describe("Context integration tests", func() { tempSettings.Close() }) - Context("create, with interactive prompts", func() { + Describe("any command", func() { + It("should inform about invalid token", func() { + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "list", "github", "foo", + "--skip-update-check", + "--token", "", + ) - Describe("when listing contexts without a token", func() { - BeforeEach(func() { - command = commandWithHome(pathCLI, tempSettings.Home, - "context", "list", "github", "foo", - "--skip-update-check", - "--token", "", - ) - }) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - It("instructs the user to run 'circleci setup' and create a new token", func() { - By("running the command") - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err).Should(gbytes.Say(`Error: please set a token with 'circleci setup' + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say(`Error: please set a token with 'circleci setup' You can create a new personal API token here: https://circleci.com/account/api`)) - Eventually(session).Should(clitest.ShouldFail()) - }) + Eventually(session).Should(clitest.ShouldFail()) + }) + + It("should handle errors", func() { + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-id=%s", orgID)), + ghttp.RespondWith( + http.StatusBadRequest, + `{"message":"no context found"}`, + contentTypeHeader, + ), + ), + ) + + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "list", "--org-id", orgID, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit()) + // Exit codes are different between Unix and Windows so we're only checking that it does not equal 0 + Expect(session.ExitCode()).ToNot(Equal(0)) + Expect(string(session.Err.Contents())).To(Equal("Error: no context found\n")) + }) + }) + + Describe("list", func() { + It("should list context with VCS / org name", func() { + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-slug=%s", orgSlug)), + ghttp.RespondWith( + http.StatusOK, + `{"items":[]}`, + contentTypeHeader, + ), + ), + ) + + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "list", vcsType, orgName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(string(session.Out.Contents())).To(Equal(`+----+------+------------+ +| ID | NAME | CREATED AT | ++----+------+------------+ ++----+------+------------+ +`)) + }) + + It("should list context with VCS / org name", func() { + contexts := []context.Context{ + {ID: uuid.NewString(), Name: "context-name", CreatedAt: time.Now()}, + {ID: uuid.NewString(), Name: "another-name", CreatedAt: time.Now()}, + } + body, err := json.Marshal(struct{ Items []context.Context }{contexts}) + Expect(err).ShouldNot(HaveOccurred()) + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-slug=%s", orgSlug)), + ghttp.RespondWith( + http.StatusOK, + body, + contentTypeHeader, + ), + ), + ) + + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "list", vcsType, orgName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + lines := strings.Split(string(session.Out.Contents()), "\n") + Expect(lines[1]).To(MatchRegexp("|\\w+ID\\w+|\\w+NAME\\w+|\\w+CREATED AT\\w+|")) + Expect(lines).To(HaveLen(7)) + }) + + It("should list context with org id", func() { + contexts := []context.Context{ + {ID: uuid.NewString(), Name: "context-name", CreatedAt: time.Now()}, + {ID: uuid.NewString(), Name: "another-name", CreatedAt: time.Now()}, + } + body, err := json.Marshal(struct{ Items []context.Context }{contexts}) + Expect(err).ShouldNot(HaveOccurred()) + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-id=%s", orgID)), + ghttp.RespondWith( + http.StatusOK, + body, + contentTypeHeader, + ), + ), + ) + + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "list", "--org-id", orgID, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + lines := strings.Split(string(session.Out.Contents()), "\n") + Expect(lines[1]).To(MatchRegexp("|\\w+ID\\w+|\\w+NAME\\w+|\\w+CREATED AT\\w+|")) + Expect(lines).To(HaveLen(7)) + }) + }) + + Describe("show", func() { + var ( + contexts = []context.Context{ + {ID: uuid.NewString(), Name: "another-name", CreatedAt: time.Now()}, + {ID: uuid.NewString(), Name: "context-name", CreatedAt: time.Now()}, + } + ctxBody, _ = json.Marshal(struct{ Items []context.Context }{contexts}) + envVars = []context.EnvironmentVariable{ + {Variable: "var-name", ContextID: contexts[1].ID, CreatedAt: time.Now(), UpdatedAt: time.Now()}, + {Variable: "any-name", ContextID: contexts[1].ID, CreatedAt: time.Now(), UpdatedAt: time.Now()}, + } + envBody, _ = json.Marshal(struct{ Items []context.EnvironmentVariable }{envVars}) + ) + + It("should show context with vcs type / org name", func() { + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-slug=%s", orgSlug)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", fmt.Sprintf("/api/v2/context/%s/environment-variable", contexts[1].ID)), + ghttp.RespondWith( + http.StatusOK, + envBody, + contentTypeHeader, + ), + ), + ) + + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "show", vcsType, orgName, "context-name", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + lines := strings.Split(string(session.Out.Contents()), "\n") + Expect(lines[0]).To(Equal("Context: context-name")) + Expect(lines[2]).To(MatchRegexp("|\\w+ENVIRONMENT VARIABLE\\w+|\\w+VALUE\\w+|")) + Expect(lines).To(HaveLen(8)) + }) + + It("should show context with org id", func() { + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-id=%s", orgID)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", fmt.Sprintf("/api/v2/context/%s/environment-variable", contexts[1].ID)), + ghttp.RespondWith( + http.StatusOK, + envBody, + contentTypeHeader, + ), + ), + ) + + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "show", "context-name", "--org-id", orgID, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + lines := strings.Split(string(session.Out.Contents()), "\n") + Expect(lines[0]).To(Equal("Context: context-name")) + Expect(lines[2]).To(MatchRegexp("|\\w+ENVIRONMENT VARIABLE\\w+|\\w+VALUE\\w+|")) + Expect(lines).To(HaveLen(8)) }) }) - Context("create, with interactive prompts", func() { - //tests context creation via orgid - Describe("using an org id to create a context", func() { - - BeforeEach(func() { - command = commandWithHome(pathCLI, tempSettings.Home, - "context", "create", - "--skip-update-check", - "--token", token, - "--host", tempSettings.TestServer.URL(), - "--integration-testing", - contextName, - "--org-id", fmt.Sprintf(`"%s"`, orgID), - ) - }) - - It("should create new context using an org id", func() { - By("setting up a mock server") - tempSettings.AppendRESTPostHandler(clitest.MockRequestResponse{ - Status: http.StatusOK, - Request: fmt.Sprintf(`{"name": "%s","owner":{"id":"\"%s\""}}`, contextName, orgID), - Response: fmt.Sprintf(`{"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "%s", "created_at": "2015-09-21T17:29:21.042Z" }`, contextName), - }) - - By("running the command") - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - //tests context creation via orgname and vcs type - Describe("using an vcs and org name to create a context", func() { - BeforeEach(func() { - command = exec.Command(pathCLI, - "context", "create", - "--skip-update-check", - "--token", token, - "--host", tempSettings.TestServer.URL(), - "--integration-testing", - vcsType, - orgName, - contextName, - ) - }) - - It("user creating new context", func() { - By("setting up a mock server") - - tempSettings.AppendRESTPostHandler(clitest.MockRequestResponse{ - Status: http.StatusOK, - Request: fmt.Sprintf(`{"name": "%s","owner":{"slug":"%s"}}`, contextName, vcsType+"/"+orgName), - Response: fmt.Sprintf(`{"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "%s", "created_at": "2015-09-21T17:29:21.042Z" }`, contextName), - }) - - By("running the command") - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session).Should(gexec.Exit(0)) - }) - - It("prints all in-band errors returned by the API", func() { - By("setting up a mock server") - tempSettings.AppendRESTPostHandler(clitest.MockRequestResponse{ - Status: http.StatusInternalServerError, - Request: fmt.Sprintf(`{"name": "%s","owner":{"slug":"%s"}}`, contextName, vcsType+"/"+orgName), - Response: fmt.Sprintf(`{"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "%s", "created_at": "2015-09-21T17:29:21.042Z" }`, contextName), - ErrorResponse: `{ "message": "ignored error" }`, - }) - By("running the command") - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err).Should(gbytes.Say(`Error: ignored error`)) - Eventually(session).ShouldNot(gexec.Exit(0)) - }) + Describe("store", func() { + var ( + contexts = []context.Context{ + {ID: uuid.NewString(), Name: "another-name", CreatedAt: time.Now()}, + {ID: uuid.NewString(), Name: "context-name", CreatedAt: time.Now()}, + } + ctxBody, _ = json.Marshal(struct{ Items []context.Context }{contexts}) + envVar = context.EnvironmentVariable{ + Variable: "env var name", + ContextID: uuid.NewString(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + varBody, _ = json.Marshal(envVar) + ) + + It("should store value when giving vcs type / org name", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-slug=%s", orgSlug)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("PUT", fmt.Sprintf("/api/v2/context/%s/environment-variable/%s", contexts[1].ID, envVar.Variable)), + ghttp.VerifyJSON(`{"value":"value"}`), + ghttp.RespondWith( + http.StatusOK, + varBody, + contentTypeHeader, + ), + ), + ) + + By("running the command") + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "store-secret", vcsType, orgName, contexts[1].Name, envVar.Variable, + "--skip-update-check", + "--integration-testing", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("should store value when giving vcs type / org name", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-id=%s", orgID)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("PUT", fmt.Sprintf("/api/v2/context/%s/environment-variable/%s", contexts[1].ID, envVar.Variable)), + ghttp.VerifyJSON(`{"value":"value"}`), + ghttp.RespondWith( + http.StatusOK, + varBody, + contentTypeHeader, + ), + ), + ) + + By("running the command") + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "store-secret", "--org-id", orgID, contexts[1].Name, envVar.Variable, + "--skip-update-check", + "--integration-testing", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Describe("remove", func() { + var ( + contexts = []context.Context{ + {ID: uuid.NewString(), Name: "another-name", CreatedAt: time.Now()}, + {ID: uuid.NewString(), Name: "context-name", CreatedAt: time.Now()}, + } + ctxBody, _ = json.Marshal(struct{ Items []context.Context }{contexts}) + varName = "env var name" + ) + + It("should remove environment variable with vcs type / org name", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-slug=%s", orgSlug)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("DELETE", fmt.Sprintf("/api/v2/context/%s/environment-variable/%s", contexts[1].ID, varName)), + ghttp.RespondWith( + http.StatusOK, + `{"message":"Deleted env var"}`, + contentTypeHeader, + ), + ), + ) + + By("running the command") + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "remove-secret", vcsType, orgName, contexts[1].Name, varName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(string(session.Out.Contents())).To(Equal("Removed secret env var name from context context-name.\n")) + }) + + It("should remove environment variable with org id", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-id=%s", orgID)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("DELETE", fmt.Sprintf("/api/v2/context/%s/environment-variable/%s", contexts[1].ID, varName)), + ghttp.RespondWith( + http.StatusOK, + `{"message":"Deleted env var"}`, + contentTypeHeader, + ), + ), + ) + + By("running the command") + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "remove-secret", "--org-id", orgID, contexts[1].Name, varName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(string(session.Out.Contents())).To(Equal("Removed secret env var name from context context-name.\n")) + }) + }) + + Describe("delete", func() { + var ( + contexts = []context.Context{ + {ID: uuid.NewString(), Name: "another-name", CreatedAt: time.Now()}, + {ID: uuid.NewString(), Name: "context-name", CreatedAt: time.Now()}, + } + ctxBody, _ = json.Marshal(struct{ Items []context.Context }{contexts}) + ) + + It("should delete context with vcs type / org name", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-slug=%s", orgSlug)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("DELETE", fmt.Sprintf("/api/v2/context/%s", contexts[1].ID)), + ghttp.RespondWith( + http.StatusOK, + `{"message":"Deleted context"}`, + contentTypeHeader, + ), + ), + ) + + By("running the command") + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "delete", "-f", vcsType, orgName, contexts[1].Name, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(string(session.Out.Contents())).To(Equal("Deleted context context-name.\n")) + }) + + It("should delete context with org id", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v2/context", fmt.Sprintf("owner-id=%s", orgID)), + ghttp.RespondWith( + http.StatusOK, + ctxBody, + contentTypeHeader, + ), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("DELETE", fmt.Sprintf("/api/v2/context/%s", contexts[1].ID)), + ghttp.RespondWith( + http.StatusOK, + `{"message":"Deleted context"}`, + contentTypeHeader, + ), + ), + ) + + By("running the command") + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "delete", "-f", "--org-id", orgID, contexts[1].Name, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(string(session.Out.Contents())).To(Equal("Deleted context context-name.\n")) + }) + }) + + Describe("create", func() { + var ( + context = context.Context{ + ID: uuid.NewString(), + CreatedAt: time.Now(), + Name: contextName, + } + ctxResp, _ = json.Marshal(&context) + ) + + It("should create new context using an org id", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/v2/context"), + ghttp.VerifyContentType("application/json"), + ghttp.VerifyJSON(fmt.Sprintf(`{"name":"%s","owner":{"type":"organization","id":"%s"}}`, contextName, orgID)), + ghttp.RespondWith(http.StatusOK, ctxResp, contentTypeHeader), + ), + ) + + By("running the command") + command = commandWithHome(pathCLI, tempSettings.Home, + "context", "create", "--org-id", orgID, contextName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--integration-testing", + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("should create new context using vcs type / org name", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/v2/context"), + ghttp.VerifyContentType("application/json"), + ghttp.VerifyJSON(fmt.Sprintf(`{"name":"%s","owner":{"type":"organization","slug":"%s/%s"}}`, contextName, vcsType, orgName)), + ghttp.RespondWith(http.StatusOK, ctxResp, contentTypeHeader), + ), + ) + + By("running the command") + command := exec.Command(pathCLI, + "context", "create", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--integration-testing", + vcsType, + orgName, + contextName, + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("handles errors", func() { + By("setting up a mock server") + mockServerForREST(tempSettings) + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/v2/context"), + ghttp.VerifyContentType("application/json"), + ghttp.VerifyJSON(fmt.Sprintf(`{"name":"%s","owner":{"type":"organization","slug":"%s/%s"}}`, contextName, vcsType, orgName)), + ghttp.RespondWith(http.StatusInternalServerError, []byte(`{"message":"ignored error"}`), contentTypeHeader), + ), + ) + By("running the command") + command := exec.Command(pathCLI, + "context", "create", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--integration-testing", + vcsType, + orgName, + contextName, + ) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say(`Error: ignored error`)) + Eventually(session).ShouldNot(gexec.Exit(0)) }) }) }) diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go index 216817c96..c49f58afc 100644 --- a/cmd/namespace_test.go +++ b/cmd/namespace_test.go @@ -145,7 +145,12 @@ Please note that any orbs you publish in this namespace are open orbs and are wo }` expectedOrganizationRequest := `{ - "query": "query($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}","variables":{"organizationName":"test-org","organizationVcs":"BITBUCKET"}}` + "query": "query($orgName: String!, $vcsType: VCSType!) {\n\torganization(name: $orgName, vcsType: $vcsType) {\n\t\tid\n\t\tname\n\t\tvcsType\n\t}\n}", + "variables": { + "orgName": "test-org", + "vcsType": "BITBUCKET" + } +}` gqlNsResponse := `{ "createNamespace": { @@ -217,12 +222,12 @@ Please note that any orbs you publish in this namespace are open orbs and are wo }` expectedOrganizationRequest := `{ - "query": "query($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", - "variables": { - "organizationName": "test-org", - "organizationVcs": "BITBUCKET" - } - }` + "query": "query($orgName: String!, $vcsType: VCSType!) {\n\torganization(name: $orgName, vcsType: $vcsType) {\n\t\tid\n\t\tname\n\t\tvcsType\n\t}\n}", + "variables": { + "orgName": "test-org", + "vcsType": "BITBUCKET" + } +}` gqlNsResponse := `{ "createNamespace": { @@ -279,12 +284,12 @@ Please note that any orbs you publish in this namespace are open orbs and are wo }` expectedOrganizationRequest := `{ - "query": "query($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", - "variables": { - "organizationName": "test-org", - "organizationVcs": "BITBUCKET" - } - }` + "query": "query($orgName: String!, $vcsType: VCSType!) {\n\torganization(name: $orgName, vcsType: $vcsType) {\n\t\tid\n\t\tname\n\t\tvcsType\n\t}\n}", + "variables": { + "orgName": "test-org", + "vcsType": "BITBUCKET" + } +}` gqlResponse := `{ "createNamespace": { diff --git a/cmd/orb.go b/cmd/orb.go index 224d0f9d7..332b451fe 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -25,6 +25,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/collaborators" + "github.com/CircleCI-Public/circleci-cli/api/context" "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/api/orb" "github.com/CircleCI-Public/circleci-cli/filetree" @@ -1401,8 +1402,8 @@ func initOrb(opts orbOptions) error { } if createContext == 0 { - contextGql := api.NewContextGraphqlClient(opts.cfg.HTTPClient, opts.cfg.Host, opts.cfg.Endpoint, opts.cfg.Token, opts.cfg.Debug) - err = contextGql.CreateContext(vcsProvider, ownerName, "orb-publishing") + contextAPI := context.NewContextClient(opts.cfg, "", vcsProvider, ownerName) + err = contextAPI.CreateContext("orb-publishing") if err != nil { if strings.Contains(err.Error(), "A context named orb-publishing already exists") { fmt.Println("`orb-publishing` context already exists, continuing on") @@ -1410,11 +1411,11 @@ func initOrb(opts orbOptions) error { return err } } - ctx, err := contextGql.ContextByName(vcsProvider, ownerName, "orb-publishing") + ctx, err := contextAPI.ContextByName("orb-publishing") if err != nil { return err } - err = contextGql.CreateEnvironmentVariable(ctx.ID, "CIRCLE_TOKEN", opts.cfg.Token) + err = contextAPI.CreateEnvironmentVariable(ctx.ID, "CIRCLE_TOKEN", opts.cfg.Token) if err != nil && !strings.Contains(err.Error(), "ALREADY_EXISTS") { return err } diff --git a/md_docs/md_docs.go b/md_docs/md_docs.go index 36cef982a..32a713954 100644 --- a/md_docs/md_docs.go +++ b/md_docs/md_docs.go @@ -25,6 +25,10 @@ var introHeader = ` [![License](https://img.shields.io/badge/license-MIT-red.svg)](./LICENSE) ` +const ( + mdExt = ".md" +) + // PositionalArgs returns a slice of the given command's positional arguments func PositionalArgs(cmd *cobra.Command) []string { args := strings.Split(cmd.Use, " ") @@ -124,7 +128,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) if cmd.HasParent() { parent := cmd.Parent() pname := parent.CommandPath() - link := pname + ".md" + link := pname + mdExt link = strings.Replace(link, " ", "_", -1) buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short)) cmd.VisitParents(func(c *cobra.Command) { @@ -142,7 +146,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) continue } cname := name + " " + child.Name() - link := cname + ".md" + link := cname + mdExt link = strings.Replace(link, " ", "_", -1) buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short)) } @@ -188,7 +192,7 @@ func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHa } } - basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md" + basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + mdExt filename := filepath.Join(dir, basename) f, err := os.Create(filename) if err != nil {