diff --git a/go.mod b/go.mod index 0419db27..7a7fa7a0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/AlecAivazis/survey/v2 v2.3.6 github.com/MakeNowJust/heredoc/v2 v2.0.1 - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.18.0 + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.18.1 github.com/briandowns/spinner v1.19.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 diff --git a/go.sum b/go.sum index a5818fd8..b7945c29 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZ github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.18.0 h1:EfQGnIT6/89CeSTibq3AWaHN0YXPggPwoW9lW18dhrE= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.18.0/go.mod h1:yCrdCpNf9D60Q6zYeG7wt9XHimM0r9z0AY9auR6AjLc= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.18.1 h1:Bhae8Z8vzvZus2coXQs0EKN+22tonak9nVMI1ArzvEc= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.18.1/go.mod h1:yCrdCpNf9D60Q6zYeG7wt9XHimM0r9z0AY9auR6AjLc= github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E= github.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/pkg/cmd/tenant/clone/clone.go b/pkg/cmd/tenant/clone/clone.go new file mode 100644 index 00000000..2bdb70c6 --- /dev/null +++ b/pkg/cmd/tenant/clone/clone.go @@ -0,0 +1,135 @@ +package clone + +import ( + "fmt" + "github.com/MakeNowJust/heredoc/v2" + "github.com/OctopusDeploy/cli/pkg/cmd" + "github.com/OctopusDeploy/cli/pkg/cmd/tenant/shared" + "github.com/OctopusDeploy/cli/pkg/constants" + "github.com/OctopusDeploy/cli/pkg/factory" + "github.com/OctopusDeploy/cli/pkg/machinescommon" + "github.com/OctopusDeploy/cli/pkg/output" + "github.com/OctopusDeploy/cli/pkg/question" + "github.com/OctopusDeploy/cli/pkg/question/selectors" + "github.com/OctopusDeploy/cli/pkg/util/flag" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/spf13/cobra" +) + +const ( + FlagName = "name" + FlagDescription = "description" + FlagSourceTenant = "source-tenant" +) + +type CloneFlags struct { + Name *flag.Flag[string] + Description *flag.Flag[string] + SourceTenant *flag.Flag[string] + *machinescommon.WebFlags +} + +func NewCloneFlags() *CloneFlags { + return &CloneFlags{ + Name: flag.New[string](FlagName, false), + Description: flag.New[string](FlagDescription, false), + SourceTenant: flag.New[string](FlagSourceTenant, false), + } +} + +type CloneOptions struct { + *CloneFlags + *cmd.Dependencies + GetTenantCallback shared.GetTenantCallback + GetAllTenantsCallback shared.GetAllTenantsCallback +} + +func NewCloneOptions(flags *CloneFlags, dependencies *cmd.Dependencies) *CloneOptions { + return &CloneOptions{ + CloneFlags: flags, + Dependencies: dependencies, + GetTenantCallback: func(id string) (*tenants.Tenant, error) { + return shared.GetTenant(*dependencies.Client, id) + }, + GetAllTenantsCallback: func() ([]*tenants.Tenant, error) { + return shared.GetAllTenants(*dependencies.Client) + }, + } +} + +func NewCmdClone(f factory.Factory) *cobra.Command { + cloneFlags := NewCloneFlags() + cmd := &cobra.Command{ + Use: "clone", + Short: "Clone a tenant", + Long: "Clone a tenant in Octopus Deploy", + Example: heredoc.Docf(` + $ %[1]s tenant clone + $ %[1]s tenant clone --name "Garys Cakes" --source-tenant "Bobs Wood Shop" + `, constants.ExecutableName), + RunE: func(c *cobra.Command, args []string) error { + opts := NewCloneOptions(cloneFlags, cmd.NewDependencies(f, c)) + + return cloneRun(opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&cloneFlags.Name.Value, cloneFlags.Name.Name, "n", "", "Name of the new tenant") + flags.StringVarP(&cloneFlags.Description.Value, cloneFlags.Description.Name, "d", "", "Description of the new tenant") + flags.StringVar(&cloneFlags.SourceTenant.Value, cloneFlags.SourceTenant.Name, "", "Name of the source tenant") + return cmd +} + +func cloneRun(opts *CloneOptions) error { + if !opts.NoPrompt { + if err := PromptMissing(opts); err != nil { + return err + } + } + + tenant, err := opts.GetTenantCallback(opts.SourceTenant.Value) + if err != nil { + return err + } + + clonedTenant, err := opts.Client.Tenants.Clone(tenant, tenants.TenantCloneRequest{Name: opts.Name.Value, Description: opts.Description.Value}) + if err != nil { + return err + } + + fmt.Fprintf(opts.Out, "Successfully cloned tenant '%s' to '%s'.\n", tenant.Name, clonedTenant.Name) + if !opts.NoPrompt { + autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.SourceTenant, opts.Name, opts.Description) + fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd) + } + + link := output.Bluef("%s/app#/%s/tenants/%s/overview", opts.Host, opts.Space.GetID(), clonedTenant.GetID()) + fmt.Fprintf(opts.Out, "View this tenant on Octopus Deploy: %s\n", link) + + return nil +} + +func PromptMissing(opts *CloneOptions) error { + err := question.AskName(opts.Ask, "", "Tenant", &opts.Name.Value) + if err != nil { + return err + } + err = question.AskDescription(opts.Ask, "", "Tenant", &opts.Description.Value) + if err != nil { + return err + } + + if opts.SourceTenant.Value == "" { + tenant, err := selectors.Select(opts.Ask, "You have not specified a source Tenant to clone from. Please select one:", opts.GetAllTenantsCallback, func(tenant *tenants.Tenant) string { + return tenant.Name + }) + if err != nil { + return nil + } + + opts.SourceTenant.Value = tenant.Name + } + + return nil +} diff --git a/pkg/cmd/tenant/clone/clone_test.go b/pkg/cmd/tenant/clone/clone_test.go new file mode 100644 index 00000000..63ec7ae7 --- /dev/null +++ b/pkg/cmd/tenant/clone/clone_test.go @@ -0,0 +1,58 @@ +package clone_test + +import ( + "github.com/OctopusDeploy/cli/pkg/cmd" + "github.com/OctopusDeploy/cli/pkg/cmd/tenant/clone" + "github.com/OctopusDeploy/cli/test/testutil" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPromptMissing_AllFlagsSupplied(t *testing.T) { + pa := []*testutil.PA{} + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + flags := clone.NewCloneFlags() + flags.Name.Value = "Cloned tenant" + flags.Description.Value = "the description" + flags.SourceTenant.Value = "source tenant" + + opts := clone.NewCloneOptions(flags, &cmd.Dependencies{Ask: asker}) + + opts.GetTenantCallback = func(identifier string) (*tenants.Tenant, error) { + return tenants.NewTenant("source tenant"), nil + } + + err := clone.PromptMissing(opts) + checkRemainingPrompts() + assert.NoError(t, err) +} + +func TestPromptMissing_NoFlagsSupplied(t *testing.T) { + pa := []*testutil.PA{ + testutil.NewInputPrompt("Name", "A short, memorable, unique name for this Tenant.", "cloned tenant"), + testutil.NewInputPrompt("Description", "A short, memorable, description for this Tenant.", "the description"), + testutil.NewSelectPrompt("You have not specified a source Tenant to clone from. Please select one:", "", []string{"source tenant", "source tenant 2"}, "source tenant"), + } + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + flags := clone.NewCloneFlags() + opts := clone.NewCloneOptions(flags, &cmd.Dependencies{Ask: asker}) + + opts.GetTenantCallback = func(identifier string) (*tenants.Tenant, error) { + return tenants.NewTenant("source tenant"), nil + } + opts.GetAllTenantsCallback= func () ([]*tenants.Tenant, error) { + return []*tenants.Tenant{ + tenants.NewTenant("source tenant"), + tenants.NewTenant("source tenant 2"), + },nil + } + + err := clone.PromptMissing(opts) + checkRemainingPrompts() + assert.NoError(t, err) + + assert.Equal(t, "cloned tenant", flags.Name.Value) + assert.Equal(t, "the description", flags.Description.Value) + assert.Equal(t, "source tenant", flags.SourceTenant.Value) +} \ No newline at end of file diff --git a/pkg/cmd/tenant/tenant.go b/pkg/cmd/tenant/tenant.go index 7417b307..acd1b5b0 100644 --- a/pkg/cmd/tenant/tenant.go +++ b/pkg/cmd/tenant/tenant.go @@ -8,6 +8,7 @@ import ( cmdDisconnect "github.com/OctopusDeploy/cli/pkg/cmd/tenant/disconnect" cmdList "github.com/OctopusDeploy/cli/pkg/cmd/tenant/list" cmdTag "github.com/OctopusDeploy/cli/pkg/cmd/tenant/tag" + cmdClone "github.com/OctopusDeploy/cli/pkg/cmd/tenant/clone" cmdView "github.com/OctopusDeploy/cli/pkg/cmd/tenant/view" "github.com/OctopusDeploy/cli/pkg/constants" "github.com/OctopusDeploy/cli/pkg/constants/annotations" @@ -34,6 +35,7 @@ func NewCmdTenant(f factory.Factory) *cobra.Command { cmd.AddCommand(cmdList.NewCmdList(f)) cmd.AddCommand(cmdCreate.NewCmdCreate(f)) cmd.AddCommand(cmdTag.NewCmdTag(f)) + cmd.AddCommand(cmdClone.NewCmdClone(f)) cmd.AddCommand(cmdDelete.NewCmdDelete(f)) cmd.AddCommand(cmdView.NewCmdView(f))