diff --git a/cmd/check_test.go b/cmd/check_test.go index b7cf5daa2..b22b293a3 100644 --- a/cmd/check_test.go +++ b/cmd/check_test.go @@ -1,7 +1,6 @@ package cmd_test import ( - "fmt" "net/http" "os" "os/exec" @@ -17,48 +16,41 @@ import ( var _ = Describe("Check", func() { var ( - command *exec.Cmd - err error - checkCLI string - tempHome string - testServer *ghttp.Server - updateCheck *settings.UpdateCheck - updateFile *os.File + command *exec.Cmd + err error + checkCLI string + tempSettings *temporarySettings + testServer *ghttp.Server + updateCheck *settings.UpdateCheck ) BeforeEach(func() { checkCLI, err = gexec.Build("github.com/CircleCI-Public/circleci-cli") Expect(err).ShouldNot(HaveOccurred()) - tempHome, _, updateFile = withTempSettings() + tempSettings = withTempSettings() updateCheck = &settings.UpdateCheck{ LastUpdateCheck: time.Time{}, } - updateCheck.FileUsed = updateFile.Name() + updateCheck.FileUsed = tempSettings.updateFile.Name() err = updateCheck.WriteToDisk() Expect(err).ShouldNot(HaveOccurred()) testServer = ghttp.NewServer() - command = exec.Command(checkCLI, "help", - "--github-api", testServer.URL(), - ) - - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), + command = commandWithHome(checkCLI, tempSettings.home, + "help", "--github-api", testServer.URL(), ) }) AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) + Expect(os.RemoveAll(tempSettings.home)).To(Succeed()) }) Describe("update auto checks with a new release", func() { - var ( - response string - ) + var response string BeforeEach(func() { checkCLI, err = gexec.Build("github.com/CircleCI-Public/circleci-cli", @@ -67,14 +59,10 @@ var _ = Describe("Check", func() { ) Expect(err).ShouldNot(HaveOccurred()) - tempHome, _, _ = withTempSettings() - - command = exec.Command(checkCLI, "help", - "--github-api", testServer.URL(), - ) + tempSettings = withTempSettings() - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), + command = commandWithHome(checkCLI, tempSettings.home, + "help", "--github-api", testServer.URL(), ) response = ` diff --git a/cmd/cmd_suite_test.go b/cmd/cmd_suite_test.go index af20d3750..9f8618634 100644 --- a/cmd/cmd_suite_test.go +++ b/cmd/cmd_suite_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "net/http" "os" + "os/exec" "path/filepath" "testing" @@ -22,40 +23,68 @@ var _ = BeforeSuite(func() { Ω(err).ShouldNot(HaveOccurred()) }) -func withTempSettings() (string, *os.File, *os.File) { - var ( - tempHome string - err error - config *os.File - update *os.File - ) +type temporarySettings struct { + home string + configFile *os.File + configPath string + updateFile *os.File + updatePath string +} - tempHome, err = ioutil.TempDir("", "circleci-cli-test-") +func (tempSettings temporarySettings) writeToConfigAndClose(contents []byte) { + _, err := tempSettings.configFile.Write(contents) Expect(err).ToNot(HaveOccurred()) + Expect(tempSettings.configFile.Close()).To(Succeed()) +} + +func (tempSettings temporarySettings) assertConfigRereadMatches(contents string) { + file, err := os.Open(tempSettings.configPath) + Expect(err).ShouldNot(HaveOccurred()) - const ( - settingsPath = ".circleci" - configFilename = "cli.yml" - updateCheckFilename = "update_check.yml" + reread, err := ioutil.ReadAll(file) + Expect(err).ShouldNot(HaveOccurred()) + Expect(string(reread)).To(Equal(contents)) +} + +func commandWithHome(bin, home string, args ...string) *exec.Cmd { + command := exec.Command(bin, args...) + + command.Env = append(os.Environ(), + fmt.Sprintf("HOME=%s", home), + fmt.Sprintf("USERPROFILE=%s", home), // windows ) - Expect(os.Mkdir(filepath.Join(tempHome, settingsPath), 0700)).To(Succeed()) + return command +} + +func withTempSettings() *temporarySettings { + var err error + + tempSettings := &temporarySettings{} + + tempSettings.home, err = ioutil.TempDir("", "circleci-cli-test-") + Expect(err).ToNot(HaveOccurred()) + + settingsPath := filepath.Join(tempSettings.home, ".circleci") + + Expect(os.Mkdir(settingsPath, 0700)).To(Succeed()) + + tempSettings.configPath = filepath.Join(settingsPath, "cli.yml") - config, err = os.OpenFile( - filepath.Join(tempHome, settingsPath, configFilename), + tempSettings.configFile, err = os.OpenFile(tempSettings.configPath, os.O_RDWR|os.O_CREATE, 0600, ) Expect(err).ToNot(HaveOccurred()) - update, err = os.OpenFile( - filepath.Join(tempHome, settingsPath, updateCheckFilename), + tempSettings.updatePath = filepath.Join(settingsPath, "update_check.yml") + tempSettings.updateFile, err = os.OpenFile(tempSettings.updatePath, os.O_RDWR|os.O_CREATE, 0600, ) Expect(err).ToNot(HaveOccurred()) - return tempHome, config, update + return tempSettings } var _ = AfterSuite(func() { diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 0a78b9220..af94186f4 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -3,7 +3,6 @@ package cmd_test import ( "bytes" "encoding/json" - "fmt" "io" "net/http" "os" @@ -1760,23 +1759,20 @@ query namespaceOrbs ($namespace: String, $after: String!) { }) Describe("when creating an orb without a token", func() { - var tempHome string + var tempSettings *temporarySettings BeforeEach(func() { - tempHome, _, _ = withTempSettings() + tempSettings = withTempSettings() - command = exec.Command(pathCLI, + command = commandWithHome(pathCLI, tempSettings.home, "orb", "create", "bar-ns/foo-orb", "--skip-update-check", "--token", "", ) - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), - ) }) AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) + Expect(os.RemoveAll(tempSettings.home)).To(Succeed()) }) It("instructs the user to run 'circleci setup' and create a new token", func() { diff --git a/cmd/root.go b/cmd/root.go index fd118d869..c781c792c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -122,9 +122,9 @@ func MakeCommands() *cobra.Command { rootCmd.PersistentFlags().BoolVar(&rootOptions.Debug, "debug", rootOptions.Debug, "Enable debug logging.") rootCmd.PersistentFlags().StringVar(&rootTokenFromFlag, - "token", "", "your token for using CircleCI") + "token", "", "your token for using CircleCI, also CIRCLECI_CLI_TOKEN") rootCmd.PersistentFlags().StringVar(&rootOptions.Host, - "host", rootOptions.Host, "URL to your CircleCI host") + "host", rootOptions.Host, "URL to your CircleCI host, also CIRCLECI_CLI_HOST") rootCmd.PersistentFlags().StringVar(&rootOptions.Endpoint, "endpoint", rootOptions.Endpoint, "URI to your CircleCI GraphQL API endpoint") diff --git a/cmd/root_test.go b/cmd/root_test.go index bf661e438..4c63088ae 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,11 +1,8 @@ package cmd_test import ( - "fmt" - "io/ioutil" "os" "os/exec" - "path/filepath" "github.com/CircleCI-Public/circleci-cli/cmd" . "github.com/onsi/ginkgo" @@ -24,14 +21,14 @@ var _ = Describe("Root", func() { Describe("build without auto update", func() { var ( - command *exec.Cmd - err error - noUpdateCLI string - tempHome string + command *exec.Cmd + err error + noUpdateCLI string + tempSettings *temporarySettings ) BeforeEach(func() { - tempHome, _, _ = withTempSettings() + tempSettings = withTempSettings() noUpdateCLI, err = gexec.Build("github.com/CircleCI-Public/circleci-cli", "-ldflags", @@ -41,15 +38,12 @@ var _ = Describe("Root", func() { }) AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) + Expect(os.RemoveAll(tempSettings.home)).To(Succeed()) }) It("reports update command as unavailable", func() { - command = exec.Command(noUpdateCLI, "help", - "--skip-update-check", - ) - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), + command = commandWithHome(noUpdateCLI, tempSettings.home, + "help", "--skip-update-check", ) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -105,50 +99,25 @@ var _ = Describe("Root", func() { Describe("token in help text", func() { var ( - command *exec.Cmd - tempHome string - ) - - const ( - configDir = ".circleci" - configFile = "cli.yml" + command *exec.Cmd + tempSettings *temporarySettings ) BeforeEach(func() { - var err error - tempHome, err = ioutil.TempDir("", "circleci-cli-test-") - Expect(err).ToNot(HaveOccurred()) + tempSettings = withTempSettings() - command = exec.Command(pathCLI, "help", - "--skip-update-check", - ) - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), - fmt.Sprintf("USERPROFILE=%s", tempHome), // windows + command = commandWithHome(pathCLI, tempSettings.home, + "help", "--skip-update-check", ) }) AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) + Expect(os.RemoveAll(tempSettings.home)).To(Succeed()) }) Describe("existing config file", func() { - var config *os.File - BeforeEach(func() { - Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) - - var err error - config, err = os.OpenFile( - filepath.Join(tempHome, configDir, configFile), - os.O_RDWR|os.O_CREATE, - 0600, - ) - Expect(err).ToNot(HaveOccurred()) - - _, err = config.Write([]byte(`token: secret`)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) + tempSettings.writeToConfigAndClose([]byte(`token: secret`)) }) It("does not include the users token in help text", func() { diff --git a/cmd/setup.go b/cmd/setup.go index c43b43e2f..ff5f8d25a 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -13,9 +13,13 @@ import ( var testing = false type setupOptions struct { - cfg *settings.Config - cl *client.Client - args []string + cfg *settings.Config + cl *client.Client + noPrompt bool + // Add host and token for use with --no-prompt + host string + token string + args []string } func newSetupCommand(config *settings.Config) *cobra.Command { @@ -40,10 +44,26 @@ func newSetupCommand(config *settings.Config) *cobra.Command { panic(err) } + setupCommand.Flags().BoolVar(&opts.noPrompt, "no-prompt", false, "Disable prompt to bypass interactive UI. (MUST supply --host and --token)") + + setupCommand.Flags().StringVar(&opts.host, "host", "", "URL to your CircleCI host") + if err := setupCommand.Flags().MarkHidden("host"); err != nil { + panic(err) + } + + setupCommand.Flags().StringVar(&opts.token, "token", "", "your token for using CircleCI") + if err := setupCommand.Flags().MarkHidden("token"); err != nil { + panic(err) + } + return setupCommand } func setup(opts setupOptions) error { + if opts.noPrompt { + return setupNoPrompt(opts) + } + var tty ui.UserInterface = ui.InteractiveUI{} if testing { @@ -74,6 +94,61 @@ func setup(opts setupOptions) error { return errors.Wrap(err, "Failed to save config file") } - fmt.Println("Setup complete. Your configuration has been saved.") + fmt.Printf("Setup complete.\nYour configuration has been saved to %s.\n", opts.cfg.FileUsed) + return nil +} + +func shouldKeepExistingConfig(opts setupOptions) bool { + // Host will always be set, since it has a default value of circleci.com + // We assume by an empty token there is no existing config. + if opts.cfg.Token == "" { + return false + } + + // If they pass either host or token with a value this will be false, overwriting their existing config + return opts.host == "" && opts.token == "" +} + +func setupNoPrompt(opts setupOptions) error { + if shouldKeepExistingConfig(opts) { + fmt.Printf("Setup has kept your existing configuration at %s.\n", opts.cfg.FileUsed) + return nil + } + + // Throw an error if both flags are blank are blank! + if opts.host == "" && opts.token == "" { + return errors.New("No existing host or token saved.\nThe proper format is `circleci setup --host HOST --token TOKEN --no-prompt") + } + + config := settings.Config{} + + // First calling load will ensure the new config can be saved to disk + if err := config.LoadFromDisk(); err != nil { + return errors.Wrap(err, "Failed to create config file on disk") + } + + // Use the default endpoint since we don't expose that to users + config.Endpoint = defaultEndpoint + config.Host = opts.host // Set new host to flag + config.Token = opts.token // Set new token to flag + + // Reset their host if the flag was blank + if opts.host == "" { + fmt.Println("Host unchanged from existing config. Use --host with --no-prompt to overwrite it.") + config.Host = opts.cfg.Host + } + + // Reset their token if the flag was blank + if opts.token == "" { + fmt.Println("Token unchanged from existing config. Use --token with --no-prompt to overwrite it.") + config.Token = opts.cfg.Token + } + + // Then save the new config to disk + if err := config.WriteToDisk(); err != nil { + return errors.Wrap(err, "Failed to save config file") + } + + fmt.Printf("Setup complete.\nYour configuration has been saved to %s.\n", config.FileUsed) return nil } diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 875e45053..16dc86a71 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "os" "os/exec" - "path/filepath" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -13,39 +12,27 @@ import ( "github.com/onsi/gomega/gexec" ) -var _ = Describe("Setup", func() { +var _ = Describe("Setup with prompts", func() { var ( - tempHome string - command *exec.Cmd - ) - - const ( - configDir = ".circleci" - configFile = "cli.yml" + command *exec.Cmd + tempSettings *temporarySettings ) BeforeEach(func() { - var err error - tempHome, err = ioutil.TempDir("", "circleci-cli-test-") - Expect(err).ToNot(HaveOccurred()) + tempSettings = withTempSettings() - command = exec.Command(pathCLI, + command = commandWithHome(pathCLI, tempSettings.home, "setup", "--testing", "--skip-update-check", ) - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), - fmt.Sprintf("USERPROFILE=%s", tempHome), // windows - ) }) AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) + Expect(os.RemoveAll(tempSettings.home)).To(Succeed()) }) Describe("new config file", func() { - It("should set file permissions to 0600", func() { session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) @@ -54,45 +41,30 @@ var _ = Describe("Setup", func() { Eventually(session.Out).Should(gbytes.Say("API token has been set.")) Eventually(session.Out).Should(gbytes.Say("CircleCI Host")) Eventually(session.Out).Should(gbytes.Say("CircleCI host has been set.")) - Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) + Eventually(session.Out).Should(gbytes.Say(fmt.Sprintf("Setup complete.\nYour configuration has been saved to %s.\n", tempSettings.configPath))) Eventually(session.Err.Contents()).Should(BeEmpty()) Eventually(session).Should(gexec.Exit(0)) - fileInfo, err := os.Stat(filepath.Join(tempHome, configDir, configFile)) + fileInfo, err := os.Stat(tempSettings.configPath) Expect(err).ToNot(HaveOccurred()) Expect(fileInfo.Mode().Perm().String()).To(Equal("-rw-------")) }) }) Describe("existing config file", func() { - var config *os.File - - BeforeEach(func() { - Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) - - var err error - config, err = os.OpenFile( - filepath.Join(tempHome, configDir, configFile), - os.O_RDWR|os.O_CREATE, - 0600, - ) - Expect(err).ToNot(HaveOccurred()) - }) - It("should set file permissions to 0600", func() { session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - fileInfo, err := os.Stat(filepath.Join(tempHome, configDir, configFile)) + fileInfo, err := os.Stat(tempSettings.configPath) Expect(err).ToNot(HaveOccurred()) Expect(fileInfo.Mode().Perm().String()).To(Equal("-rw-------")) }) - Describe("token and endpoint set in config file", func() { - + Describe("token and host set in config file", func() { It("print success", func() { session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) @@ -102,19 +74,17 @@ var _ = Describe("Setup", func() { Eventually(session.Out).Should(gbytes.Say("API token has been set.")) Eventually(session.Out).Should(gbytes.Say("CircleCI Host")) Eventually(session.Out).Should(gbytes.Say("CircleCI host has been set.")) - Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) + Eventually(session.Out).Should(gbytes.Say(fmt.Sprintf("Setup complete.\nYour configuration has been saved to %s.\n", tempSettings.configPath))) Eventually(session).Should(gexec.Exit(0)) }) }) Context("token set to some string in config file", func() { BeforeEach(func() { - _, err := config.Write([]byte(` -endpoint: https://example.com/graphql + tempSettings.writeToConfigAndClose([]byte(` +host: https://example.com/graphql token: fooBarBaz `)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) }) It("print error", func() { @@ -125,9 +95,160 @@ token: fooBarBaz Eventually(session.Out).Should(gbytes.Say("API token has been set.")) Eventually(session.Out).Should(gbytes.Say("CircleCI Host")) Eventually(session.Out).Should(gbytes.Say("CircleCI host has been set.")) - Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) + Eventually(session.Out).Should(gbytes.Say(fmt.Sprintf("Setup complete.\nYour configuration has been saved to %s.\n", tempSettings.configPath))) Eventually(session).Should(gexec.Exit(0)) }) }) }) }) + +var _ = Describe("Setup without prompts", func() { + var ( + tempSettings *temporarySettings + command *exec.Cmd + ) + + BeforeEach(func() { + tempSettings = withTempSettings() + command = commandWithHome(pathCLI, tempSettings.home, + "setup", + "--no-prompt", + "--skip-update-check", + ) + }) + + AfterEach(func() { + Expect(os.RemoveAll(tempSettings.home)).To(Succeed()) + }) + + Context("with an existing config", func() { + Describe("of valid settings", func() { + BeforeEach(func() { + tempSettings.writeToConfigAndClose([]byte(` +host: https://example.com +token: fooBarBaz +`)) + }) + + It("should keep the existing configuration", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say(fmt.Sprintf("Setup has kept your existing configuration at %s.\n", tempSettings.configPath))) + + Context("re-open the config to check the contents", func() { + tempSettings.assertConfigRereadMatches(` +host: https://example.com +token: fooBarBaz +`) + }) + }) + + It("should change if provided one of flags", func() { + command = commandWithHome(pathCLI, tempSettings.home, + "setup", + "--host", "asdf", + "--no-prompt", + "--skip-update-check", + ) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + stdout := session.Wait().Out.Contents() + Expect(string(stdout)).To(Equal(fmt.Sprintf(`Token unchanged from existing config. Use --token with --no-prompt to overwrite it. +Setup complete. +Your configuration has been saved to %s. +`, tempSettings.configPath))) + Eventually(session).Should(gexec.Exit(0)) + + Context("re-open the config to check the contents", func() { + tempSettings.assertConfigRereadMatches(`host: asdf +endpoint: graphql-unstable +token: fooBarBaz +`) + }) + }) + + It("should change only the provided token", func() { + command = commandWithHome(pathCLI, tempSettings.home, + "setup", + "--token", "asdf", + "--no-prompt", + "--skip-update-check", + ) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + stdout := session.Wait().Out.Contents() + Expect(string(stdout)).To(Equal(fmt.Sprintf(`Host unchanged from existing config. Use --host with --no-prompt to overwrite it. +Setup complete. +Your configuration has been saved to %s. +`, tempSettings.configPath))) + Eventually(session).Should(gexec.Exit(0)) + + Context("re-open the config to check the contents", func() { + tempSettings.assertConfigRereadMatches(`host: https://example.com +endpoint: graphql-unstable +token: asdf +`) + }) + }) + }) + + }) + + Context("with no existing config", func() { + Context("with no host or token flags", func() { + BeforeEach(func() { + command = commandWithHome(pathCLI, tempSettings.home, + "setup", + "--no-prompt", + "--skip-update-check", + ) + }) + + It("Should raise an error about missing host and token flags", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(255)) + + stderr := session.Wait().Err.Contents() + Expect(string(stderr)).To(Equal("Error: No existing host or token saved.\nThe proper format is `circleci setup --host HOST --token TOKEN --no-prompt\n")) + }) + }) + + Context("with both host and token flags", func() { + BeforeEach(func() { + command = commandWithHome(pathCLI, tempSettings.home, + "setup", + "--host", "https://zomg.com", + "--token", "mytoken", + "--no-prompt", + "--skip-update-check", + ) + }) + + It("write the configuration to a file", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + stdout := session.Wait().Out.Contents() + Expect(string(stdout)).To(Equal(fmt.Sprintf(`Setup complete. +Your configuration has been saved to %s. +`, tempSettings.configPath))) + + Context("re-open the config to check the contents", func() { + file, err := os.Open(tempSettings.configPath) + Expect(err).ShouldNot(HaveOccurred()) + + reread, err := ioutil.ReadAll(file) + Expect(err).ShouldNot(HaveOccurred()) + Expect(string(reread)).To(Equal(`host: https://zomg.com +endpoint: graphql-unstable +token: mytoken +`)) + }) + }) + }) + }) +})