Skip to content

Commit

Permalink
Merge pull request #505 from CircleCI-Public/admin-category
Browse files Browse the repository at this point in the history
[CIRCLE-30416] Move Server-specific administrative commands into new "admin" category
  • Loading branch information
aengelberg authored Oct 27, 2020
2 parents 7341e15 + a85bbf4 commit c2663fe
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 428 deletions.
94 changes: 94 additions & 0 deletions cmd/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package cmd

import (
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/spf13/cobra"
)

func newAdminCommand(config *settings.Config) *cobra.Command {
orbOpts := orbOptions{
cfg: config,
tty: createOrbInteractiveUI{},
}
nsOpts := namespaceOptions{
cfg: config,
tty: createNamespaceInteractiveUI{},
}

importOrbCommand := &cobra.Command{
Use: "import-orb <namespace>[/<orb>[@<version>]]",
Short: "Import an orb version from circleci.com into a CircleCI Server installation",
RunE: func(_ *cobra.Command, _ []string) error {
return importOrb(orbOpts)
},
Args: cobra.MinimumNArgs(1),
}
importOrbCommand.Flags().BoolVar(&orbOpts.integrationTesting, "integration-testing", false, "Enable test mode to bypass interactive UI.")

renameCommand := &cobra.Command{
Use: "rename-namespace <old-name> <new-name>",
Short: "Rename a namespace",
PreRunE: func(_ *cobra.Command, args []string) error {
nsOpts.args = args
nsOpts.cl = graphql.NewClient(config.Host, config.Endpoint, config.Token, config.Debug)

return validateToken(nsOpts.cfg)
},
RunE: func(_ *cobra.Command, _ []string) error {
return renameNamespace(nsOpts)
},
Args: cobra.ExactArgs(2),
Annotations: make(map[string]string),
}
renameCommand.Flags().BoolVar(&nsOpts.noPrompt, "no-prompt", false, "Disable prompt to bypass interactive UI.")

renameCommand.Annotations["<old-name>"] = "The current name of the namespace"
renameCommand.Annotations["<new-name>"] = "The new name you want to give the namespace"

deleteAliasCommand := &cobra.Command{
Use: "delete-namespace-alias <name>",
Short: "Delete a namespace alias",
Long: `Delete a namespace alias.
A namespace can have multiple aliases (names). This command deletes an alias left behind by a rename. The most recent alias cannot be deleted.
Example:
- namespace A is renamed to B
- alias B is created, coexisting with alias A
- after migrating config accordingly, we can delete the A alias.`,
PreRunE: func(_ *cobra.Command, args []string) error {
return validateToken(nsOpts.cfg)
},
RunE: func(_ *cobra.Command, _ []string) error {
return deleteNamespaceAlias(nsOpts)
},
Args: cobra.ExactArgs(1),
Annotations: make(map[string]string),
}

deleteAliasCommand.Annotations["<name>"] = "The name of the alias to delete"

adminCommand := &cobra.Command{
Use: "admin",
Short: "Administrative operations for a CircleCI Server installation.",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
orbOpts.args = args
nsOpts.args = args
orbOpts.cl = graphql.NewClient(config.Host, config.Endpoint, config.Token, config.Debug)
nsOpts.cl = orbOpts.cl

// PersistentPreRunE overwrites the inherited persistent hook from rootCmd
// So we explicitly call it here to retain that behavior.
// As of writing this comment, that is only for daily update checks.
return rootCmdPreRun(rootOptions)
},
Hidden: true,
}

adminCommand.AddCommand(importOrbCommand)
adminCommand.AddCommand(renameCommand)
adminCommand.AddCommand(deleteAliasCommand)

return adminCommand
}
200 changes: 200 additions & 0 deletions cmd/admin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package cmd_test

import (
"net/http"
"os/exec"
"time"

"github.com/CircleCI-Public/circleci-cli/clitest"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/gexec"
)

var _ = Describe("Namespace integration tests", func() {
var (
tempSettings *clitest.TempSettings
token string = "testtoken"
command *exec.Cmd
)

BeforeEach(func() {
tempSettings = clitest.WithTempSettings()
})

AfterEach(func() {
tempSettings.Close()
})

Describe("deleting namespace aliases", func() {
BeforeEach(func() {
command = exec.Command(pathCLI,
"admin",
"delete-namespace-alias",
"--skip-update-check",
"--token", token,
"--host", tempSettings.TestServer.URL(),
"foo-ns",
)
})

It("returns message for when deletion unexpectedly failed", func() {
gqlDeleteNsAliasResponse := `{
"deleteNamespaceAlias": {
"errors": [],
"deleted": false
}
}`
expectedDeleteNsAliasRequest := `{
"query": "\nmutation($name: String!) {\n deleteNamespaceAlias(name: $name) {\n deleted\n errors {\n type\n message\n }\n }\n}\n",
"variables": {
"name": "foo-ns"
}
}`

tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{
Status: http.StatusOK,
Request: expectedDeleteNsAliasRequest,
Response: gqlDeleteNsAliasResponse})

session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)

Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err).Should(gbytes.Say("Namespace alias deletion failed for unknown reasons."))
Eventually(session).ShouldNot(gexec.Exit(0))
})

It("returns all errors returned by the GraphQL API", func() {
gqlDeleteNsAliasResponse := `{
"deleteNamespaceAlias": {
"errors": [{"message": "error1"}],
"deleted": false
}
}`
expectedDeleteNsAliasRequest := `{
"query": "\nmutation($name: String!) {\n deleteNamespaceAlias(name: $name) {\n deleted\n errors {\n type\n message\n }\n }\n}\n",
"variables": {
"name": "foo-ns"
}
}`

tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{
Status: http.StatusOK,
Request: expectedDeleteNsAliasRequest,
Response: gqlDeleteNsAliasResponse})

session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)

Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err).Should(gbytes.Say("Error: error1"))
Eventually(session).ShouldNot(gexec.Exit(0))
})

It("works given an alias name", func() {
By("setting up a mock server")
gqlDeleteNsAliasResponse := `{
"deleteNamespaceAlias": {
"errors": [],
"deleted": true
}
}`
expectedDeleteNsAliasRequest := `{
"query": "\nmutation($name: String!) {\n deleteNamespaceAlias(name: $name) {\n deleted\n errors {\n type\n message\n }\n }\n}\n",
"variables": {
"name": "foo-ns"
}
}`

tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{
Status: http.StatusOK,
Request: expectedDeleteNsAliasRequest,
Response: gqlDeleteNsAliasResponse})

By("running the command")
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)

Expect(err).ShouldNot(HaveOccurred())
Eventually(session, time.Second*5).Should(gexec.Exit(0))
})
})

Describe("renaming a namespace", func() {
var (
gqlGetNsResponse string
expectedGetNsRequest string
expectedRenameRequest string
)
BeforeEach(func() {
command = exec.Command(pathCLI,
"admin", "rename-namespace",
"ns-0", "ns-1",
"--skip-update-check",
"--token", token,
"--host", tempSettings.TestServer.URL(),
"--no-prompt",
)
gqlGetNsResponse = `{
"errors": [],
"registryNamespace": {
"id": "bb604b45-b6b0-4b81-ad80-796f15eddf87"
}
}`
expectedGetNsRequest = `{
"query": "\n\t\t\t\tquery($name: String!) {\n\t\t\t\t\tregistryNamespace(\n\t\t\t\t\t\tname: $name\n\t\t\t\t\t){\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t }",
"variables": {
"name": "ns-0"
}
}`
expectedRenameRequest = `{
"query": "\n\t\tmutation($namespaceId: UUID!, $newName: String!){\n\t\t\trenameNamespace(\n\t\t\t\tnamespaceId: $namespaceId,\n\t\t\t\tnewName: $newName\n\t\t\t){\n\t\t\t\tnamespace {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t\terrors {\n\t\t\t\t\tmessage\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}",
"variables": {"newName": "ns-1", "namespaceId": "bb604b45-b6b0-4b81-ad80-796f15eddf87"}
}`
})

It("works in the basic case", func() {
By("setting up a mock server")
gqlRenameResponse := `{"data":{"renameNamespace":{"namespace":{"id":"4e377fe3-330d-4e4c-af62-821850fe9595"},"errors":[]}}}`
tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{
Status: http.StatusOK,
Request: expectedGetNsRequest,
Response: gqlGetNsResponse})
tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{
Status: http.StatusOK,
Request: expectedRenameRequest,
Response: gqlRenameResponse})

By("running the command")
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Out, "5s").Should(gbytes.Say("`ns-0` renamed to `ns-1`"))
Eventually(session).Should(gexec.Exit(0))
})

It("returns an error when renaming a namespace fails", func() {
By("setting up a mock server")
gqlRenameResponse := `{
"renameNamespace": {
"errors": [
{"message": "error1"},
{"message": "error2"}
],
"namespace": null
}
}`
tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{
Status: http.StatusOK,
Request: expectedGetNsRequest,
Response: gqlGetNsResponse})
tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{
Status: http.StatusOK,
Request: expectedRenameRequest,
Response: gqlRenameResponse})
By("running the command")
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err).Should(gbytes.Say("Error: error1\nerror2"))
Eventually(session).ShouldNot(gexec.Exit(0))
})
})
})
52 changes: 0 additions & 52 deletions cmd/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,35 +54,6 @@ func newNamespaceCommand(config *settings.Config) *cobra.Command {
Short: "Operate on namespaces",
}

deleteAliasCmd := &cobra.Command{
Use: "delete-alias <name>",
Short: "(Server only) Delete a namespace alias",
Long: `Delete a namespace alias.
A namespace can have multiple aliases (names). This command deletes an alias left behind by a rename. The most recent alias cannot be deleted.
Example:
- namespace A is renamed to B
- alias B is created, coexisting with alias A
- after migrating config accordingly, we can delete the A alias.
This requires install admin privileges, because it will break existing builds using the deleted alias.`,
PreRunE: func(_ *cobra.Command, args []string) error {
opts.args = args
opts.cl = graphql.NewClient(config.Host, config.Endpoint, config.Token, config.Debug)

return validateToken(opts.cfg)
},
RunE: func(_ *cobra.Command, _ []string) error {
return deleteNamespaceAlias(opts)
},
Args: cobra.ExactArgs(1),
Annotations: make(map[string]string),
Hidden: true,
}

deleteAliasCmd.Annotations["<name>"] = "The name of the alias to delete"

createCmd := &cobra.Command{
Use: "create <name> <vcs-type> <org-name>",
Short: "Create a namespace",
Expand Down Expand Up @@ -117,30 +88,7 @@ Please note that at this time all namespaces created in the registry are world-r
}
createCmd.Flags().BoolVar(&opts.noPrompt, "no-prompt", false, "Disable prompt to bypass interactive UI.")

renameCmd := &cobra.Command{
Use: "rename <old-name> <new-name>",
Short: "(server admin only) rename a namespace",
PreRunE: func(_ *cobra.Command, args []string) error {
opts.args = args
opts.cl = graphql.NewClient(config.Host, config.Endpoint, config.Token, config.Debug)

return validateToken(opts.cfg)
},
RunE: func(_ *cobra.Command, _ []string) error {
return renameNamespace(opts)
},
Args: cobra.ExactArgs(2),
Annotations: make(map[string]string),
Hidden: true,
}
renameCmd.Flags().BoolVar(&opts.noPrompt, "no-prompt", false, "Disable prompt to bypass interactive UI.")

renameCmd.Annotations["<old-name>"] = "The current name of the namespace"
renameCmd.Annotations["<new-name>"] = "The new name you want to give the namespace"

namespaceCmd.AddCommand(deleteAliasCmd)
namespaceCmd.AddCommand(createCmd)
namespaceCmd.AddCommand(renameCmd)

return namespaceCmd
}
Expand Down
Loading

0 comments on commit c2663fe

Please sign in to comment.