diff --git a/fly/commands/archive_pipeline.go b/fly/commands/archive_pipeline.go index fb0accdc7da..b0c32cb31e2 100644 --- a/fly/commands/archive_pipeline.go +++ b/fly/commands/archive_pipeline.go @@ -2,15 +2,19 @@ package commands import ( "fmt" - "github.com/concourse/concourse/fly/commands/internal/displayhelpers" "github.com/concourse/concourse/fly/commands/internal/flaghelpers" "github.com/concourse/concourse/fly/rc" + "github.com/concourse/concourse/fly/ui" + "github.com/fatih/color" + "github.com/vito/go-interact/interact" + "os" ) type ArchivePipelineCommand struct { - Pipeline flaghelpers.PipelineFlag `short:"p" long:"pipeline" description:"Pipeline to archive"` - All bool `short:"a" long:"all" description:"Archive all pipelines"` + Pipeline flaghelpers.PipelineFlag `short:"p" long:"pipeline" description:"Pipeline to archive"` + All bool `short:"a" long:"all" description:"Archive all pipelines"` + SkipInteractive bool `short:"n" long:"non-interactive" description:"Skips interactions, uses default values"` } func (command *ArchivePipelineCommand) Validate() error { @@ -59,6 +63,17 @@ func (command *ArchivePipelineCommand) Execute(args []string) error { } } + if len(pipelineNames) == 0 { + fmt.Println("there are no unarchived pipelines") + fmt.Println("bailing out") + return nil + } + + if !command.confirmArchive(pipelineNames) { + fmt.Println("bailing out") + return nil + } + for _, pipelineName := range pipelineNames { found, err := target.Team().ArchivePipeline(pipelineName) if err != nil { @@ -74,3 +89,37 @@ func (command *ArchivePipelineCommand) Execute(args []string) error { return nil } + +func (command ArchivePipelineCommand) confirmArchive(pipelines []string) bool { + if command.SkipInteractive { + return true + } + + if len(pipelines) > 1 { + command.printPipelinesTable(pipelines) + } + + var confirm bool + err := interact.NewInteraction(command.archivePrompt(pipelines)).Resolve(&confirm) + if err != nil { + return false + } + + return confirm +} + +func (ArchivePipelineCommand) printPipelinesTable(pipelines []string) { + table := ui.Table{Headers: ui.TableRow{{Contents: "pipelines", Color: color.New(color.Bold)}}} + for _, pipeline := range pipelines { + table.Data = append(table.Data, ui.TableRow{{Contents: pipeline}}) + } + table.Render(os.Stdout, true) + fmt.Println() +} + +func (ArchivePipelineCommand) archivePrompt(pipelines []string) string { + if len(pipelines) == 1 { + return fmt.Sprintf("archive pipeline '%s'?", pipelines[0]) + } + return fmt.Sprintf("archive %d pipelines?", len(pipelines)) +} diff --git a/fly/integration/archive_pipeline_test.go b/fly/integration/archive_pipeline_test.go index dff25b4efc6..a5214041662 100644 --- a/fly/integration/archive_pipeline_test.go +++ b/fly/integration/archive_pipeline_test.go @@ -1,6 +1,10 @@ package integration_test import ( + "fmt" + "github.com/concourse/concourse/fly/ui" + "github.com/fatih/color" + "io" "net/http" "os/exec" @@ -15,12 +19,21 @@ import ( ) var _ = Describe("Fly CLI", func() { + yes := func(stdin io.Writer) { + fmt.Fprintf(stdin, "y\n") + } + + no := func(stdin io.Writer) { + fmt.Fprintf(stdin, "n\n") + } + Describe("archive-pipeline", func() { Context("when the pipeline name is specified", func() { var ( path string err error ) + BeforeEach(func() { path, err = atc.Routes.CreatePathForRoute(atc.ArchivePipeline, rata.Params{"pipeline_name": "awesome-pipeline", "team_name": "main"}) Expect(err).NotTo(HaveOccurred()) @@ -36,20 +49,68 @@ var _ = Describe("Fly CLI", func() { ) }) - It("archives the pipeline", func() { - Expect(func() { - flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-p", "awesome-pipeline") + Context("when the user confirms", func() { + It("archives the pipeline", func() { + Expect(func() { + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-p", "awesome-pipeline") + stdin, err := flyCmd.StdinPipe() + Expect(err).NotTo(HaveOccurred()) - sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) + sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) - Eventually(sess).Should(gbytes.Say(`archived 'awesome-pipeline'`)) + Eventually(sess).Should(gbytes.Say("archive pipeline 'awesome-pipeline'?")) + yes(stdin) - <-sess.Exited - Expect(sess.ExitCode()).To(Equal(0)) - }).To(Change(func() int { - return len(atcServer.ReceivedRequests()) - }).By(2)) + Eventually(sess).Should(gbytes.Say(`archived 'awesome-pipeline'`)) + + <-sess.Exited + Expect(sess.ExitCode()).To(Equal(0)) + }).To(Change(func() int { + return len(atcServer.ReceivedRequests()) + }).By(2)) + }) + }) + + Context("when the user declines", func() { + It("does not archive the pipelines", func() { + Expect(func() { + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-p", "awesome-pipeline") + stdin, err := flyCmd.StdinPipe() + Expect(err).NotTo(HaveOccurred()) + + sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + Eventually(sess).Should(gbytes.Say("archive pipeline 'awesome-pipeline'?")) + no(stdin) + + Eventually(sess).Should(gbytes.Say(`bailing out`)) + + <-sess.Exited + Expect(sess.ExitCode()).To(Equal(0)) + }).To(Change(func() int { + return len(atcServer.ReceivedRequests()) + }).By(1)) + }) + }) + + Context("when running in non-interactive mode", func() { + It("does not prompt the user", func() { + Expect(func() { + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-n", "-p", "awesome-pipeline") + + sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + Eventually(sess).Should(gbytes.Say(`archived 'awesome-pipeline'`)) + + <-sess.Exited + Expect(sess.ExitCode()).To(Equal(0)) + }).To(Change(func() int { + return len(atcServer.ReceivedRequests()) + }).By(2)) + }) }) }) @@ -65,7 +126,7 @@ var _ = Describe("Fly CLI", func() { It("prints helpful message", func() { Expect(func() { - flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-p", "awesome-pipeline") + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-n", "-p", "awesome-pipeline") sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) @@ -117,72 +178,154 @@ var _ = Describe("Fly CLI", func() { }) }) - Context("when specifying a pipeline name with a '/' character in it", func() { - It("fails and says '/' characters are not allowed", func() { - flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-p", "forbidden/pipelinename") + Context("when the --all flag is passed, and there are unarchived pipelines", func() { + var ( + somePath string + someOtherPath string + err error + ) - sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + BeforeEach(func() { + somePath, err = atc.Routes.CreatePathForRoute(atc.ArchivePipeline, rata.Params{"pipeline_name": "awesome-pipeline", "team_name": "main"}) Expect(err).NotTo(HaveOccurred()) - <-sess.Exited - Expect(sess.ExitCode()).To(Equal(1)) + someOtherPath, err = atc.Routes.CreatePathForRoute(atc.ArchivePipeline, rata.Params{"pipeline_name": "more-awesome-pipeline", "team_name": "main"}) + Expect(err).NotTo(HaveOccurred()) - Expect(sess.Err).To(gbytes.Say("error: pipeline name cannot contain '/'")) + atcServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v1/teams/main/pipelines"), + ghttp.RespondWithJSONEncoded(200, []atc.Pipeline{ + {Name: "awesome-pipeline", Archived: false, Public: false}, + {Name: "more-awesome-pipeline", Archived: false, Public: false}, + {Name: "already-archived", Archived: true, Public: false}, + }), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("PUT", somePath), + ghttp.RespondWith(http.StatusOK, nil), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("PUT", someOtherPath), + ghttp.RespondWith(http.StatusOK, nil), + ), + ) }) - }) - }) + Context("when the user confirms", func() { + It("archives every currently unarchived pipeline", func() { + Expect(func() { + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "--all") + stdin, err := flyCmd.StdinPipe() + Expect(err).NotTo(HaveOccurred()) - Context("when the --all flag is passed", func() { - var ( - somePath string - someOtherPath string - err error - ) - - BeforeEach(func() { - somePath, err = atc.Routes.CreatePathForRoute(atc.ArchivePipeline, rata.Params{"pipeline_name": "awesome-pipeline", "team_name": "main"}) - Expect(err).NotTo(HaveOccurred()) - - someOtherPath, err = atc.Routes.CreatePathForRoute(atc.ArchivePipeline, rata.Params{"pipeline_name": "more-awesome-pipeline", "team_name": "main"}) - Expect(err).NotTo(HaveOccurred()) - - atcServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", "/api/v1/teams/main/pipelines"), - ghttp.RespondWithJSONEncoded(200, []atc.Pipeline{ - {Name: "awesome-pipeline", Archived: false, Public: false}, - {Name: "more-awesome-pipeline", Archived: false, Public: false}, - {Name: "already-archived", Archived: true, Public: false}, - }), - ), - ghttp.CombineHandlers( - ghttp.VerifyRequest("PUT", somePath), - ghttp.RespondWith(http.StatusOK, nil), - ), - ghttp.CombineHandlers( - ghttp.VerifyRequest("PUT", someOtherPath), - ghttp.RespondWith(http.StatusOK, nil), - ), - ) + sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + Eventually(sess.Out).Should(PrintTable(ui.Table{ + Headers: ui.TableRow{{Contents: "pipelines", Color: color.New(color.Bold)}}, + Data: []ui.TableRow{ + {{Contents: "awesome-pipeline"}}, + {{Contents: "more-awesome-pipeline"}}, + }, + })) + + Eventually(sess).Should(gbytes.Say("archive 2 pipelines?")) + yes(stdin) + + Eventually(sess).Should(gbytes.Say(`archived 'awesome-pipeline'`)) + Eventually(sess).Should(gbytes.Say(`archived 'more-awesome-pipeline'`)) + Consistently(sess).ShouldNot(gbytes.Say(`archived 'already-archived'`)) + + <-sess.Exited + Expect(sess.ExitCode()).To(Equal(0)) + }).To(Change(func() int { + return len(atcServer.ReceivedRequests()) + }).By(4)) + }) + }) + + Context("when the user denies", func() { + It("does not archive the pipelines", func() { + Expect(func() { + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "--all") + stdin, err := flyCmd.StdinPipe() + Expect(err).NotTo(HaveOccurred()) + + sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + Eventually(sess).Should(gbytes.Say("archive 2 pipelines?")) + no(stdin) + + Eventually(sess).Should(gbytes.Say(`bailing out`)) + + <-sess.Exited + Expect(sess.ExitCode()).To(Equal(0)) + }).To(Change(func() int { + return len(atcServer.ReceivedRequests()) + }).By(2)) + }) + }) + + Context("when running in non-interactive mode", func() { + It("does not prompt the user", func() { + Expect(func() { + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-n", "--all") + + sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + Eventually(sess).Should(gbytes.Say(`archived 'awesome-pipeline'`)) + Eventually(sess).Should(gbytes.Say(`archived 'more-awesome-pipeline'`)) + + <-sess.Exited + Expect(sess.ExitCode()).To(Equal(0)) + }).To(Change(func() int { + return len(atcServer.ReceivedRequests()) + }).By(4)) + }) + }) }) - It("archives every currently unarchived pipeline", func() { - Expect(func() { + Context("when the --all flag is passed, but there are no unarchived pipelines", func() { + BeforeEach(func() { + atcServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/api/v1/teams/main/pipelines"), + ghttp.RespondWithJSONEncoded(200, []atc.Pipeline{ + {Name: "already-archived", Archived: true, Public: false}, + }), + ), + ) + }) + + It("prints a message and exits", func() { flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "--all") sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) - Eventually(sess).Should(gbytes.Say(`archived 'awesome-pipeline'`)) - Eventually(sess).Should(gbytes.Say(`archived 'more-awesome-pipeline'`)) - Consistently(sess).ShouldNot(gbytes.Say(`archived 'already-archived'`)) + Eventually(sess).Should(gbytes.Say("there are no unarchived pipelines")) + Eventually(sess).Should(gbytes.Say("bailing out")) <-sess.Exited Expect(sess.ExitCode()).To(Equal(0)) - }).To(Change(func() int { - return len(atcServer.ReceivedRequests()) - }).By(4)) + }) + }) + + Context("when specifying a pipeline name with a '/' character in it", func() { + It("fails and says '/' characters are not allowed", func() { + flyCmd := exec.Command(flyPath, "-t", targetName, "archive-pipeline", "-p", "forbidden/pipelinename") + + sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + <-sess.Exited + Expect(sess.ExitCode()).To(Equal(1)) + + Expect(sess.Err).To(gbytes.Say("error: pipeline name cannot contain '/'")) + }) }) }) })