-
Notifications
You must be signed in to change notification settings - Fork 234
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #505 from CircleCI-Public/admin-category
[CIRCLE-30416] Move Server-specific administrative commands into new "admin" category
- Loading branch information
Showing
7 changed files
with
296 additions
and
428 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.