diff --git a/pkg/cmd/runbook/runbook.go b/pkg/cmd/runbook/runbook.go index b4801ce5..36aa86c5 100644 --- a/pkg/cmd/runbook/runbook.go +++ b/pkg/cmd/runbook/runbook.go @@ -4,6 +4,7 @@ import ( "github.com/MakeNowJust/heredoc/v2" cmdList "github.com/OctopusDeploy/cli/pkg/cmd/runbook/list" cmdRun "github.com/OctopusDeploy/cli/pkg/cmd/runbook/run" + cmdSnapshot "github.com/OctopusDeploy/cli/pkg/cmd/runbook/snapshot" "github.com/OctopusDeploy/cli/pkg/constants" "github.com/OctopusDeploy/cli/pkg/constants/annotations" "github.com/OctopusDeploy/cli/pkg/factory" @@ -26,5 +27,6 @@ func NewCmdRunbook(f factory.Factory) *cobra.Command { cmd.AddCommand(cmdList.NewCmdList(f)) cmd.AddCommand(cmdRun.NewCmdRun(f)) + cmd.AddCommand(cmdSnapshot.NewCmdSnapshot(f)) return cmd } diff --git a/pkg/cmd/runbook/snapshot/list/list.go b/pkg/cmd/runbook/snapshot/list/list.go new file mode 100644 index 00000000..b8aced1b --- /dev/null +++ b/pkg/cmd/runbook/snapshot/list/list.go @@ -0,0 +1,156 @@ +package list + +import ( + "errors" + "github.com/MakeNowJust/heredoc/v2" + "github.com/OctopusDeploy/cli/pkg/apiclient" + "github.com/OctopusDeploy/cli/pkg/constants" + "github.com/OctopusDeploy/cli/pkg/factory" + "github.com/OctopusDeploy/cli/pkg/output" + "github.com/OctopusDeploy/cli/pkg/question/selectors" + "github.com/OctopusDeploy/cli/pkg/util/flag" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" + "github.com/spf13/cobra" + "math" + "time" +) + +const ( + FlagProject = "project" + FlagRunbook = "runbook" + FlagLimit = "limit" +) + +type ListFlags struct { + Project *flag.Flag[string] + Runbook *flag.Flag[string] + Limit *flag.Flag[int32] +} + +func NewListFlags() *ListFlags { + return &ListFlags{ + Project: flag.New[string](FlagProject, false), + Runbook: flag.New[string](FlagRunbook, false), + Limit: flag.New[int32](FlagLimit, false), + } +} + +type SnapshotsAsJson struct { + Id string `json:"Id"` + Name string `json:"Name"` + Assembled *time.Time `json:"Assembled"` +} + +func NewCmdList(f factory.Factory) *cobra.Command { + listFlags := NewListFlags() + cmd := &cobra.Command{ + Use: "list", + Short: "List runbook snapshots", + Long: "List runbook snapshots in Octopus Deploy", + Example: heredoc.Docf(` + $ %[1]s runbook snapshot list --project "Deploy Web App" --runbook "Run maintenance" + $ %[1]s runbook snapshot ls + `, constants.ExecutableName), + Aliases: []string{"ls"}, + RunE: func(cmd *cobra.Command, args []string) error { + return listRun(cmd, f, listFlags) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&listFlags.Project.Value, listFlags.Project.Name, "p", "", "Name or ID of the project to list runbook snapshots for") + flags.StringVarP(&listFlags.Runbook.Value, listFlags.Runbook.Name, "r", "", "Name or ID of the runbook to list snapshots for") + flags.Int32Var(&listFlags.Limit.Value, listFlags.Limit.Name, math.MaxInt32, "Limit the maximum number of results that will be returned") + + return cmd +} + +func listRun(cmd *cobra.Command, f factory.Factory, flags *ListFlags) error { + client, err := f.GetSpacedClient(apiclient.NewRequester(cmd)) + if err != nil { + return err + } + + outputFormat, err := cmd.Flags().GetString(constants.FlagOutputFormat) + if err != nil { // should never happen, but fallback if it does + outputFormat = constants.OutputFormatTable + } + + projectNameOrID := flags.Project.Value + runbookNameOrID := flags.Runbook.Value + + var selectedProject *projects.Project + var selectedRunbook *runbooks.Runbook + if f.IsPromptEnabled() { // this would be AskQuestions if it were bigger + if projectNameOrID == "" { + selectedProject, err = selectors.Project("Select the project to list runbook snapshots for", client, f.Ask) + if err != nil { + return err + } + } else { // project name is already provided, fetch the object because it's needed for further questions + selectedProject, err = selectors.FindProject(client, projectNameOrID) + if err != nil { + return err + } + if !constants.IsProgrammaticOutputFormat(outputFormat) { + cmd.Printf("Project %s\n", output.Cyan(selectedProject.Name)) + } + } + + if runbookNameOrID == "" { + selectedRunbook, err = selectors.Runbook("Select the runbook to list snapshots for", client, f.Ask, selectedProject.GetID()) + if err != nil { + return err + } + } else { // project name is already provided, fetch the object because it's needed for further questions + selectedRunbook, err = selectors.FindRunbook(client, runbookNameOrID, selectedProject.GetID()) + if err != nil { + return err + } + if !constants.IsProgrammaticOutputFormat(outputFormat) { + cmd.Printf("Runbook %s\n", output.Cyan(selectedRunbook.Name)) + } + } + } else { // we don't have the executions API backing us and allowing NameOrID; we need to do the lookup ourselves + if projectNameOrID == "" { + return errors.New("project must be specified") + } + selectedProject, err = selectors.FindProject(client, projectNameOrID) + if err != nil { + return err + } + + if runbookNameOrID == "" { + return errors.New("runbook must be specified") + } + selectedRunbook, err = selectors.FindRunbook(client, runbookNameOrID, selectedProject.GetID()) + if err != nil { + return err + } + } + + allSnapshots, err := runbooks.ListSnapshots(client, client.GetSpaceID(), selectedProject.GetID(), selectedRunbook.GetID(), int(flags.Limit.Value)) + if err != nil { + return err + } + + return output.PrintArray(allSnapshots.Items, cmd, output.Mappers[*runbooks.RunbookSnapshot]{ + Json: func(s *runbooks.RunbookSnapshot) any { + return SnapshotsAsJson{ + Id: s.GetID(), + Name: s.Name, + Assembled: s.Assembled, + } + }, + Table: output.TableDefinition[*runbooks.RunbookSnapshot]{ + Header: []string{"ID", "NAME", "ASSEMBLED"}, + Row: func(s *runbooks.RunbookSnapshot) []string { + return []string{s.GetID(), output.Bold(s.Name), s.Assembled.Format(time.RFC1123Z)} + }, + }, + Basic: func(s *runbooks.RunbookSnapshot) string { + return s.Name + }, + }) +} diff --git a/pkg/cmd/runbook/snapshot/snapshot.go b/pkg/cmd/runbook/snapshot/snapshot.go new file mode 100644 index 00000000..86fee745 --- /dev/null +++ b/pkg/cmd/runbook/snapshot/snapshot.go @@ -0,0 +1,24 @@ +package snapshot + +import ( + "github.com/MakeNowJust/heredoc/v2" + cmdList "github.com/OctopusDeploy/cli/pkg/cmd/runbook/snapshot/list" + "github.com/OctopusDeploy/cli/pkg/constants" + "github.com/OctopusDeploy/cli/pkg/factory" + "github.com/spf13/cobra" +) + +func NewCmdSnapshot(f factory.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "snapshot ", + Short: "Manage runbook snapshots", + Long: "Manage runbook snapshots in Octopus Deploy", + Example: heredoc.Docf(` + $ %[1]s runbook snapshot create + $ %[1]s runbook snapshot list + `, constants.ExecutableName), + } + + cmd.AddCommand(cmdList.NewCmdList(f)) + return cmd +} diff --git a/pkg/question/selectors/runbooks.go b/pkg/question/selectors/runbooks.go new file mode 100644 index 00000000..a55817df --- /dev/null +++ b/pkg/question/selectors/runbooks.go @@ -0,0 +1,40 @@ +package selectors + +import ( + "errors" + "github.com/OctopusDeploy/cli/pkg/question" + octopusApiClient "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" + "math" +) + +func Runbook(questionText string, client *octopusApiClient.Client, ask question.Asker, projectID string) (*runbooks.Runbook, error) { + existingRunbooks, err := runbooks.List(client, client.GetSpaceID(), projectID, "", math.MaxInt32) + if err != nil { + return nil, err + } + + if len(existingRunbooks.Items) == 0 { + return nil, errors.New("no runbooks found") + } + + if len(existingRunbooks.Items) == 1 { + return existingRunbooks.Items[0], nil + } + + return question.SelectMap(ask, questionText, existingRunbooks.Items, func(r *runbooks.Runbook) string { + return r.Name + }) +} + +func FindRunbook(client *octopusApiClient.Client, runbookIdentifier string, projectID string) (*runbooks.Runbook, error) { + runbook, err := runbooks.GetByID(client, client.GetSpaceID(), runbookIdentifier) + if err != nil { + runbook, err = runbooks.GetByName(client, client.GetSpaceID(), projectID, runbookIdentifier) + if err != nil { + return nil, err + } + } + + return runbook, nil +}