From 435663080ec9272badca368aea324acb9ed6a28b Mon Sep 17 00:00:00 2001 From: hannahhenderson Date: Fri, 20 Jul 2018 11:13:20 -0700 Subject: [PATCH] Add command for create namespace Takes a positional 'name' argument and two flags: 'org-name' and (org)'vcs' Modify appendPostHandler to accept a struct of information --- api/api.go | 100 ++++++++++++++++++++++++++ cmd/cmd_suite_test.go | 48 +++++++------ cmd/config_test.go | 24 +++++-- cmd/orb.go | 37 ++++++++++ cmd/orb_test.go | 160 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 339 insertions(+), 30 deletions(-) diff --git a/api/api.go b/api/api.go index 1d3832563..ac1477141 100644 --- a/api/api.go +++ b/api/api.go @@ -5,6 +5,8 @@ import ( "io/ioutil" "strings" + "fmt" + "github.com/CircleCI-Public/circleci-cli/client" "github.com/CircleCI-Public/circleci-cli/logger" "github.com/pkg/errors" @@ -41,6 +43,17 @@ type PublishOrbResponse struct { GQLResponseErrors } +// CreateNamespaceResponse type matches the data shape of the GQL response for +// creating a namespace +type CreateNamespaceResponse struct { + Namespace struct { + CreatedAt string + ID string + } + + GQLResponseErrors +} + // ToError returns all GraphQL errors for a single response concatenated, or // nil. func (response GQLResponseErrors) ToError() error { @@ -164,3 +177,90 @@ func OrbPublish(ctx context.Context, logger *logger.Logger, } return &response.PublishOrb.PublishOrbResponse, err } + +func createNamespaceByID(ctx context.Context, logger *logger.Logger, name string, ownerID string) (*CreateNamespaceResponse, error) { + var response struct { + CreateNamespace struct { + CreateNamespaceResponse + } + } + + query := ` + mutation($name: String!, $organizationId: UUID!) { + createNamespace( + name: $name, + organizationId: $organizationId + ) { + namespace { + createdAt + id + } + errors { + message + type + } + } + }` + + request := client.NewAuthorizedRequest(viper.GetString("token"), query) + request.Var("name", name) + request.Var("organizationId", ownerID) + + graphQLclient := client.NewClient(viper.GetString("endpoint"), logger) + + err := graphQLclient.Run(ctx, request, &response) + + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("Unable to create namespace %s for ownerId %s", name, ownerID)) + } + + return &response.CreateNamespace.CreateNamespaceResponse, err +} + +func getOrganization(ctx context.Context, logger *logger.Logger, organizationName string, organizationVcs string) (string, error) { + var response struct { + Organization struct { + ID string + } + } + + query := ` + query($organizationName: String!, $organizationVcs: VCSType!) { + organization( + name: $organizationName + vcsType: $organizationVcs + ) { + id + } + }` + + request := client.NewAuthorizedRequest(viper.GetString("token"), query) + request.Var("organizationName", organizationName) + request.Var("organizationVcs", organizationVcs) + + graphQLclient := client.NewClient(viper.GetString("endpoint"), logger) + + err := graphQLclient.Run(ctx, request, &response) + + if err != nil || response.Organization.ID == "" { + err = errors.Wrap(err, fmt.Sprintf("Unable to find organization %s of vcs-type %s", organizationName, organizationVcs)) + } + + return response.Organization.ID, err +} + +// CreateNamespace creates (reserves) a namespace for an organization +func CreateNamespace(ctx context.Context, logger *logger.Logger, name string, organizationName string, organizationVcs string) (*CreateNamespaceResponse, error) { + organizationID, err := getOrganization(ctx, logger, organizationName, organizationVcs) + if err != nil { + return nil, err + } + + namespace, err := createNamespaceByID(ctx, logger, name, organizationID) + + if err != nil { + return nil, err + } + + return namespace, err +} diff --git a/cmd/cmd_suite_test.go b/cmd/cmd_suite_test.go index 7ad0862f2..def292cb4 100644 --- a/cmd/cmd_suite_test.go +++ b/cmd/cmd_suite_test.go @@ -30,28 +30,36 @@ func TestCmd(t *testing.T) { RunSpecs(t, "Cmd Suite") } +type MockRequestResponse struct { + Request string + Status int + Response string +} + // Test helpers -func appendPostHandler(server *ghttp.Server, authToken string, statusCode int, expectedRequestJson string, responseBody string) { - server.AppendHandlers( - ghttp.CombineHandlers( - ghttp.VerifyRequest("POST", "/"), - ghttp.VerifyHeader(http.Header{ - "Authorization": []string{authToken}, - }), - ghttp.VerifyContentType("application/json; charset=utf-8"), - // From Gomegas ghttp.VerifyJson to avoid the - // VerifyContentType("application/json") check - // that fails with "application/json; charset=utf-8" - func(w http.ResponseWriter, req *http.Request) { - body, err := ioutil.ReadAll(req.Body) - req.Body.Close() - Expect(err).ShouldNot(HaveOccurred()) - Expect(body).Should(MatchJSON(expectedRequestJson), "JSON Mismatch") - }, - ghttp.RespondWith(statusCode, `{ "data": `+responseBody+`}`), - ), - ) +func appendPostHandler(server *ghttp.Server, authToken string, combineHandlers ...MockRequestResponse) { + for _, handler := range combineHandlers { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/"), + ghttp.VerifyHeader(http.Header{ + "Authorization": []string{authToken}, + }), + ghttp.VerifyContentType("application/json; charset=utf-8"), + // From Gomegas ghttp.VerifyJson to avoid the + // VerifyContentType("application/json") check + // that fails with "application/json; charset=utf-8" + func(w http.ResponseWriter, req *http.Request) { + body, err := ioutil.ReadAll(req.Body) + req.Body.Close() + Expect(err).ShouldNot(HaveOccurred()) + Expect(body).Should(MatchJSON(handler.Request), "JSON Mismatch") + }, + ghttp.RespondWith(handler.Status, `{ "data": `+handler.Response+`}`), + ), + ) + } } type tmpFile struct { diff --git a/cmd/config_test.go b/cmd/config_test.go index 19edfde45..1f52f048e 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -67,7 +67,11 @@ var _ = Describe("Config", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -97,7 +101,11 @@ var _ = Describe("Config", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -143,7 +151,11 @@ var _ = Describe("Config", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -174,7 +186,11 @@ var _ = Describe("Config", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) diff --git a/cmd/orb.go b/cmd/orb.go index c19e72a04..5fcc6fd77 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "strings" "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/client" @@ -18,6 +19,8 @@ import ( var orbPath string var orbVersion string var orbID string +var organizationName string +var organizationVcs string func newOrbCommand() *cobra.Command { @@ -48,6 +51,17 @@ func newOrbCommand() *cobra.Command { orbPublishCommand.PersistentFlags().StringVarP(&orbVersion, "orb-version", "o", "", "version of orb to publish") orbPublishCommand.PersistentFlags().StringVarP(&orbID, "orb-id", "i", "", "id of orb to publish") + orbCreateNamespace := &cobra.Command{ + Use: "create", + Short: "create an orb namespace", + RunE: createOrbNamespace, + Args: cobra.ExactArgs(1), + } + + namespaceCommand := &cobra.Command{ + Use: "ns", + } + orbCommand := &cobra.Command{ Use: "orb", Short: "Operate on orbs", @@ -63,6 +77,11 @@ func newOrbCommand() *cobra.Command { orbCommand.AddCommand(orbPublishCommand) + orbCreateNamespace.PersistentFlags().StringVar(&organizationName, "org-name", "", "organization name") + orbCreateNamespace.PersistentFlags().StringVar(&organizationVcs, "vcs", "github", "organization vcs, e.g. 'github', 'bitbucket'") + namespaceCommand.AddCommand(orbCreateNamespace) + orbCommand.AddCommand(namespaceCommand) + return orbCommand } @@ -229,3 +248,21 @@ func publishOrb(cmd *cobra.Command, args []string) error { Logger.Info("Orb published") return nil } + +func createOrbNamespace(cmd *cobra.Command, args []string) error { + var err error + ctx := context.Background() + + response, err := api.CreateNamespace(ctx, Logger, args[0], organizationName, strings.ToUpper(organizationVcs)) + + if err != nil { + return err + } + + if len(response.Errors) > 0 { + return response.ToError() + } + + Logger.Info("Namespace created") + return nil +} diff --git a/cmd/orb_test.go b/cmd/orb_test.go index fd016553d..ce07cab59 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -64,7 +64,11 @@ var _ = Describe("Orb integration tests", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -96,7 +100,11 @@ var _ = Describe("Orb integration tests", func() { "config": "some orb" } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -137,7 +145,11 @@ var _ = Describe("Orb integration tests", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -170,7 +182,11 @@ var _ = Describe("Orb integration tests", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -223,7 +239,11 @@ var _ = Describe("Orb integration tests", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -257,7 +277,11 @@ var _ = Describe("Orb integration tests", func() { } }` - appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -268,5 +292,129 @@ var _ = Describe("Orb integration tests", func() { }) }) + + Describe("when creating / reserving a namespace", func() { + BeforeEach(func() { + command = exec.Command(pathCLI, + "orb", "ns", "create", + "-t", token, + "-e", testServer.URL(), + "foo-ns", + "--org-name", "test-org", + "--vcs", "BITBUCKET", + ) + }) + + It("works with organizationName and organizationVcs", func() { + By("setting up a mock server") + + gqlOrganizationResponse := `{ + "organization": { + "name": "test-org", + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrganizationRequest := `{ + "query": "\n\t\t\tquery($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" + } + }` + + gqlNsResponse := `{ + "createNamespace": { + "errors": [], + "namespace": { + "createdAt": "2018-07-16T18:03:18.961Z", + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + } + }` + + expectedNsRequest := `{ + "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", + "variables": { + "name": "foo-ns", + "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + appendPostHandler(testServer, token, + MockRequestResponse{Status: http.StatusOK, + Request: expectedOrganizationRequest, + Response: gqlOrganizationResponse}) + + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedNsRequest, + Response: gqlNsResponse}) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("Namespace created")) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("prints all errors returned by the GraphQL API", func() { + By("setting up a mock server") + + gqlOrganizationResponse := `{ + "organization": { + "name": "test-org", + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrganizationRequest := `{ + "query": "\n\t\t\tquery($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" + } + }` + + gqlResponse := `{ + "createNamespace": { + "errors": [ + {"message": "error1"}, + {"message": "error2"} + ], + "namespace": null + } + }` + + expectedRequestJson := `{ + "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", + "variables": { + "name": "foo-ns", + "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + appendPostHandler(testServer, token, + MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrganizationRequest, + Response: gqlOrganizationResponse, + }) + appendPostHandler(testServer, token, + MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error: error1: error2")) + Eventually(session).ShouldNot(gexec.Exit(0)) + }) + }) }) })