From 598a2efaa45ca3a6aebfc73ca588b77452d17fd1 Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Tue, 18 Oct 2022 17:57:59 +1000 Subject: [PATCH 1/8] initial tenant connect command --- pkg/cmd/root/root.go | 2 + pkg/cmd/tenant/connect/connect.go | 151 ++++++++++++++++++++++++++++++ pkg/cmd/tenant/tenant.go | 30 ++++++ 3 files changed, 183 insertions(+) create mode 100644 pkg/cmd/tenant/connect/connect.go create mode 100644 pkg/cmd/tenant/tenant.go diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 9204738b..0cf5cf6d 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -10,6 +10,7 @@ import ( releaseCmd "github.com/OctopusDeploy/cli/pkg/cmd/release" runbookCmd "github.com/OctopusDeploy/cli/pkg/cmd/runbook" spaceCmd "github.com/OctopusDeploy/cli/pkg/cmd/space" + tenantCmd "github.com/OctopusDeploy/cli/pkg/cmd/tenant" "github.com/OctopusDeploy/cli/pkg/cmd/version" "github.com/OctopusDeploy/cli/pkg/constants" "github.com/OctopusDeploy/cli/pkg/factory" @@ -39,6 +40,7 @@ func NewCmdRoot(f factory.Factory, clientFactory apiclient.ClientFactory, askPro // core cmd.AddCommand(projectCmd.NewCmdProject(f)) + cmd.AddCommand(tenantCmd.NewCmdTenaant(f)) // configuration cmd.AddCommand(configCmd.NewCmdConfig(f)) diff --git a/pkg/cmd/tenant/connect/connect.go b/pkg/cmd/tenant/connect/connect.go new file mode 100644 index 00000000..41ed4f02 --- /dev/null +++ b/pkg/cmd/tenant/connect/connect.go @@ -0,0 +1,151 @@ +package connect + +import ( + "fmt" + "github.com/MakeNowJust/heredoc/v2" + "github.com/OctopusDeploy/cli/pkg/constants" + "github.com/OctopusDeploy/cli/pkg/executionscommon" + "github.com/OctopusDeploy/cli/pkg/factory" + "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/client" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources" + "github.com/spf13/cobra" +) + +const ( + FlagTenant = "tenant" + FlagProject = "project" + FlagEnvironment = "environment" + FlagAliasEnvironment = "env" +) + +type ConnectFlags struct { + Tenant *flag.Flag[string] + Project *flag.Flag[string] + Environments *flag.Flag[[]string] +} + +func NewConnectFlags() *ConnectFlags { + return &ConnectFlags{ + Tenant: flag.New[string](FlagTenant, false), + Project: flag.New[string](FlagProject, false), + Environments: flag.New[[]string](FlagEnvironment, false), + } +} + +type ConnectOptions struct { + Client *client.Client + Ask question.Asker + NoPrompt bool + *ConnectFlags +} + +func NewCmdConnect(f factory.Factory) *cobra.Command { + connectFlags := NewConnectFlags() + cmd := &cobra.Command{ + Use: "connect", + Short: "Connect a tenant to a project in Octopus Deploy", + Long: "Connect a tenant to a project in Octopus Deploy", + Example: fmt.Sprintf(heredoc.Doc(` + $ %s tenant connect + $ %s tenant connect --project "Deploy web site" --environment "Production" +`), constants.ExecutableName, constants.ExecutableName), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := f.GetSpacedClient() + if err != nil { + return err + } + + opts := &ConnectOptions{ + Client: client, + Ask: f.Ask, + NoPrompt: !f.IsPromptEnabled(), + ConnectFlags: connectFlags, + } + + return connectRun(opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&connectFlags.Tenant.Value, connectFlags.Tenant.Name, "t", "", "Name or Id of the tenant") + flags.StringVarP(&connectFlags.Project.Value, connectFlags.Project.Name, "p", "", "Name, ID or Slug of the project to connect to the tenant") + flags.StringSliceVarP(&connectFlags.Environments.Value, connectFlags.Environments.Name, "e", nil, "The environments to connect to the tenant (can be specified multiple times)") + return cmd +} + +func connectRun(opts *ConnectOptions) error { + if !opts.NoPrompt { + if err := PromptMissing(opts); err != nil { + return err + } + } + + tenant, err := opts.Client.Tenants.GetByIdOrName(opts.Tenant.Value) + if err != nil { + return err + } + + project, err := opts.Client.Projects.GetByIdOrName(opts.Project.Value) + if err != nil { + return err + } + if project.TenantedDeploymentMode == core.TenantedDeploymentModeUntenanted { + project.TenantedDeploymentMode = core.TenantedDeploymentModeTenantedOrUntenanted + project, err = opts.Client.Projects.Update(project) + } + + environments, err := executionscommon.FindEnvironments(opts.Client, opts.Environments.Value) + if err != nil { + return err + } + + var environmentIds []string + for _, e := range environments { + environmentIds = append(environmentIds, e.GetID()) + } + + tenant.ProjectEnvironments[project.GetID()] = environmentIds + tenant, err = opts.Client.Tenants.Update(tenant) + if err != nil { + return err + } + + return nil +} + +func PromptMissing(opts *ConnectOptions) error { + if opts.Tenant.Value == "" { + + } + + var selectedProject *projects.Project + var err error + if opts.Project.Value == "" { + selectedProject, err = selectors.Project("Select the project to connect", opts.Client, opts.Ask) + if err != nil { + return nil + } + opts.Project.Value = selectedProject.GetName() + } + + if opts.Environments.Value == nil || len(opts.Environments.Value) == 0 { + var progression *projects.Progression + progression, err = opts.Client.Projects.GetProgression(selectedProject) + if len(progression.Environments) == 1 { + opts.Environments.Value = append(opts.Environments.Value, progression.Environments[0].Name) + } else { + var selectedEnvironments []*resources.ReferenceDataItem + selectedEnvironments, err = question.MultiSelectMap(opts.Ask, "Select the environments to connect", progression.Environments, func(item *resources.ReferenceDataItem) string { return item.Name }, true) + for _, e := range selectedEnvironments { + opts.Environments.Value = append(opts.Environments.Value, e.Name) + } + } + } + + return nil +} diff --git a/pkg/cmd/tenant/tenant.go b/pkg/cmd/tenant/tenant.go new file mode 100644 index 00000000..eddfec86 --- /dev/null +++ b/pkg/cmd/tenant/tenant.go @@ -0,0 +1,30 @@ +package tenant + +import ( + "fmt" + "github.com/MakeNowJust/heredoc/v2" + cmdConnect "github.com/OctopusDeploy/cli/pkg/cmd/tenant/connect" + "github.com/OctopusDeploy/cli/pkg/constants" + "github.com/OctopusDeploy/cli/pkg/constants/annotations" + "github.com/OctopusDeploy/cli/pkg/factory" + "github.com/spf13/cobra" +) + +func NewCmdTenaant(f factory.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "tenant ", + Short: "Manage tenants", + Long: `Work with Octopus Deploy tenants.`, + Example: fmt.Sprintf(heredoc.Doc(` + $ %s tenant list + $ %s tenant ls + `), constants.ExecutableName, constants.ExecutableName), + Annotations: map[string]string{ + annotations.IsCore: "true", + }, + } + + cmd.AddCommand(cmdConnect.NewCmdConnect(f)) + + return cmd +} From f55d753c44444cf101cd95ab0a649484216a7d17 Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Tue, 18 Oct 2022 17:58:31 +1000 Subject: [PATCH 2/8] Support lookup by id or name --- pkg/executionscommon/executionscommon.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pkg/executionscommon/executionscommon.go b/pkg/executionscommon/executionscommon.go index 86db9990..6952c845 100644 --- a/pkg/executionscommon/executionscommon.go +++ b/pkg/executionscommon/executionscommon.go @@ -395,8 +395,8 @@ func ScheduledStartTimeAnswerFormatter(datePicker *surveyext.DatePicker, t time. } // given an array of environment names, maps these all to actual objects by querying the server -func FindEnvironments(client *octopusApiClient.Client, environmentNames []string) ([]*environments.Environment, error) { - if len(environmentNames) == 0 { +func FindEnvironments(client *octopusApiClient.Client, environmentNamesOrIds []string) ([]*environments.Environment, error) { + if len(environmentNamesOrIds) == 0 { return nil, nil } // there's no "bulk lookup" API, so we either need to do a foreach loop to find each environment individually, or load the entire server's worth of environments @@ -406,18 +406,27 @@ func FindEnvironments(client *octopusApiClient.Client, environmentNames []string return nil, err } - lookup := make(map[string]*environments.Environment, len(allEnvs)) + nameLookup := make(map[string]*environments.Environment, len(allEnvs)) + idLookup := make(map[string]*environments.Environment, len(allEnvs)) + for _, env := range allEnvs { - lookup[strings.ToLower(env.Name)] = env + nameLookup[strings.ToLower(env.GetName())] = env + idLookup[strings.ToLower(env.GetID())] = env } var result []*environments.Environment - for _, name := range environmentNames { - env := lookup[strings.ToLower(name)] + for _, n := range environmentNamesOrIds { + nameOrId := strings.ToLower(n) + env := nameLookup[nameOrId] if env != nil { result = append(result, env) } else { - return nil, fmt.Errorf("cannot find environment %s", name) + env = idLookup[nameOrId] + if env != nil { + result = append(result, env) + } else { + return nil, fmt.Errorf("cannot find environment %s", nameOrId) + } } } return result, nil From 8db61ae969daec0705ac9018a3c9dd222a1ee22c Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Wed, 19 Oct 2022 10:39:30 +1000 Subject: [PATCH 3/8] fixes from previous merge --- pkg/cmd/root/root.go | 2 +- pkg/cmd/tenant/tenant.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 0cf5cf6d..04f9c4ea 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -40,7 +40,7 @@ func NewCmdRoot(f factory.Factory, clientFactory apiclient.ClientFactory, askPro // core cmd.AddCommand(projectCmd.NewCmdProject(f)) - cmd.AddCommand(tenantCmd.NewCmdTenaant(f)) + cmd.AddCommand(tenantCmd.NewCmdTenant(f)) // configuration cmd.AddCommand(configCmd.NewCmdConfig(f)) diff --git a/pkg/cmd/tenant/tenant.go b/pkg/cmd/tenant/tenant.go index eddfec86..39b70683 100644 --- a/pkg/cmd/tenant/tenant.go +++ b/pkg/cmd/tenant/tenant.go @@ -4,13 +4,14 @@ import ( "fmt" "github.com/MakeNowJust/heredoc/v2" cmdConnect "github.com/OctopusDeploy/cli/pkg/cmd/tenant/connect" + cmdList "github.com/OctopusDeploy/cli/pkg/cmd/tenant/list" "github.com/OctopusDeploy/cli/pkg/constants" "github.com/OctopusDeploy/cli/pkg/constants/annotations" "github.com/OctopusDeploy/cli/pkg/factory" "github.com/spf13/cobra" ) -func NewCmdTenaant(f factory.Factory) *cobra.Command { +func NewCmdTenant(f factory.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "tenant ", Short: "Manage tenants", @@ -25,6 +26,7 @@ func NewCmdTenaant(f factory.Factory) *cobra.Command { } cmd.AddCommand(cmdConnect.NewCmdConnect(f)) + cmd.AddCommand(cmdList.NewCmdList(f)) return cmd } From 3d5e0a0d609c3addee520b8598f04d5c9175d5de Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Wed, 19 Oct 2022 14:04:26 +1000 Subject: [PATCH 4/8] updated due to change in client library --- pkg/cmd/project/delete/delete.go | 2 +- pkg/cmd/project/view/view.go | 2 +- pkg/cmd/tenant/connect/connect.go | 189 ++++++++++++++++++++++++------ 3 files changed, 157 insertions(+), 36 deletions(-) diff --git a/pkg/cmd/project/delete/delete.go b/pkg/cmd/project/delete/delete.go index 2361a8fc..92aeb6b0 100644 --- a/pkg/cmd/project/delete/delete.go +++ b/pkg/cmd/project/delete/delete.go @@ -59,7 +59,7 @@ func deleteRun(opts *DeleteOptions) error { } } - itemToDelete, err := opts.Client.Projects.GetByIdOrName(opts.IdOrName) + itemToDelete, err := opts.Client.Projects.GetByIdentifier(opts.IdOrName) if err != nil { return err } diff --git a/pkg/cmd/project/view/view.go b/pkg/cmd/project/view/view.go index c9bd98e4..5ac5fd63 100644 --- a/pkg/cmd/project/view/view.go +++ b/pkg/cmd/project/view/view.go @@ -74,7 +74,7 @@ func NewCmdView(f factory.Factory) *cobra.Command { } func viewRun(opts *ViewOptions) error { - project, err := opts.Client.Projects.GetByIdOrName(opts.idOrName) + project, err := opts.Client.Projects.GetByIdentifier(opts.idOrName) if err != nil { return err } diff --git a/pkg/cmd/tenant/connect/connect.go b/pkg/cmd/tenant/connect/connect.go index 41ed4f02..b1a4118b 100644 --- a/pkg/cmd/tenant/connect/connect.go +++ b/pkg/cmd/tenant/connect/connect.go @@ -2,10 +2,13 @@ package connect import ( "fmt" + "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc/v2" + "github.com/OctopusDeploy/cli/pkg/cmd" "github.com/OctopusDeploy/cli/pkg/constants" "github.com/OctopusDeploy/cli/pkg/executionscommon" "github.com/OctopusDeploy/cli/pkg/factory" + "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" @@ -13,35 +16,95 @@ import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" "github.com/spf13/cobra" ) const ( - FlagTenant = "tenant" - FlagProject = "project" - FlagEnvironment = "environment" - FlagAliasEnvironment = "env" + FlagTenant = "tenant" + FlagProject = "project" + FlagEnvironment = "environment" + FlagEnableTenantDeployments = "enable-tenant-deployments" + FlagAliasEnvironment = "env" ) +type GetAllTenantsCallback func() ([]*tenants.Tenant, error) +type GetAllProjectsCallback func() ([]*projects.Project, error) +type GetProjectCallback func(idOrName string) (*projects.Project, error) +type GetProjectProgression func(project *projects.Project) (*projects.Progression, error) + type ConnectFlags struct { - Tenant *flag.Flag[string] - Project *flag.Flag[string] - Environments *flag.Flag[[]string] + Tenant *flag.Flag[string] + Project *flag.Flag[string] + Environments *flag.Flag[[]string] + EnableTenantDeployments *flag.Flag[bool] } func NewConnectFlags() *ConnectFlags { return &ConnectFlags{ - Tenant: flag.New[string](FlagTenant, false), - Project: flag.New[string](FlagProject, false), - Environments: flag.New[[]string](FlagEnvironment, false), + Tenant: flag.New[string](FlagTenant, false), + Project: flag.New[string](FlagProject, false), + Environments: flag.New[[]string](FlagEnvironment, false), + EnableTenantDeployments: flag.New[bool](FlagEnableTenantDeployments, false), + } +} + +func NewConnectOptions(connectFlags *ConnectFlags, dependencies *cmd.Dependencies) *ConnectOptions { + return &ConnectOptions{ + Dependencies: dependencies, + ConnectFlags: connectFlags, + GetAllTenantsCallback: func() ([]*tenants.Tenant, error) { return getAllTenants(*dependencies.Client) }, + GetAllProjectsCallback: func() ([]*projects.Project, error) { return getAllProjects(*dependencies.Client) }, + GetProjectCallback: func(idOrName string) (*projects.Project, error) { return getProject(*dependencies.Client, idOrName) }, + GetProjectProgressionCallback: func(project *projects.Project) (*projects.Progression, error) { + return getProjectProgression(*dependencies.Client, project) + }, } } +func getProjectProgression(client client.Client, project *projects.Project) (*projects.Progression, error) { + res, err := client.Projects.GetProgression(project) + if err != nil { + return nil, err + } + + return res, nil +} + +func getAllTenants(client client.Client) ([]*tenants.Tenant, error) { + res, err := client.Tenants.GetAll() + if err != nil { + return nil, err + } + + return res, nil +} + +func getAllProjects(client client.Client) ([]*projects.Project, error) { + res, err := client.Projects.GetAll() + if err != nil { + return nil, err + } + + return res, nil +} + +func getProject(client client.Client, identifier string) (*projects.Project, error) { + res, err := client.Projects.GetByIdentifier(identifier) + if err != nil { + return nil, err + } + + return res, nil +} + type ConnectOptions struct { - Client *client.Client - Ask question.Asker - NoPrompt bool + *cmd.Dependencies *ConnectFlags + GetAllTenantsCallback GetAllTenantsCallback + GetAllProjectsCallback GetAllProjectsCallback + GetProjectCallback GetProjectCallback + GetProjectProgressionCallback GetProjectProgression } func NewCmdConnect(f factory.Factory) *cobra.Command { @@ -53,19 +116,9 @@ func NewCmdConnect(f factory.Factory) *cobra.Command { Example: fmt.Sprintf(heredoc.Doc(` $ %s tenant connect $ %s tenant connect --project "Deploy web site" --environment "Production" -`), constants.ExecutableName, constants.ExecutableName), - RunE: func(cmd *cobra.Command, args []string) error { - client, err := f.GetSpacedClient() - if err != nil { - return err - } - - opts := &ConnectOptions{ - Client: client, - Ask: f.Ask, - NoPrompt: !f.IsPromptEnabled(), - ConnectFlags: connectFlags, - } + `), constants.ExecutableName, constants.ExecutableName), + RunE: func(c *cobra.Command, args []string) error { + opts := NewConnectOptions(connectFlags, cmd.NewDependencies(f, c)) return connectRun(opts) }, @@ -75,6 +128,8 @@ func NewCmdConnect(f factory.Factory) *cobra.Command { flags.StringVarP(&connectFlags.Tenant.Value, connectFlags.Tenant.Name, "t", "", "Name or Id of the tenant") flags.StringVarP(&connectFlags.Project.Value, connectFlags.Project.Name, "p", "", "Name, ID or Slug of the project to connect to the tenant") flags.StringSliceVarP(&connectFlags.Environments.Value, connectFlags.Environments.Name, "e", nil, "The environments to connect to the tenant (can be specified multiple times)") + flags.StringSliceVar(&connectFlags.Environments.Value, FlagAliasEnvironment, nil, "The environments to connect to the tenant (can be specified multiple times)") + flags.BoolVar(&connectFlags.EnableTenantDeployments.Value, connectFlags.EnableTenantDeployments.Name, false, "Update the project to support tenanted deployments, if required") return cmd } @@ -85,16 +140,20 @@ func connectRun(opts *ConnectOptions) error { } } - tenant, err := opts.Client.Tenants.GetByIdOrName(opts.Tenant.Value) + tenant, err := opts.Client.Tenants.GetByIdentifier(opts.Tenant.Value) if err != nil { return err } - project, err := opts.Client.Projects.GetByIdOrName(opts.Project.Value) + project, err := opts.Client.Projects.GetByIdentifier(opts.Project.Value) if err != nil { return err } - if project.TenantedDeploymentMode == core.TenantedDeploymentModeUntenanted { + + if !supportsTenantedDeployments(project) { + if opts.EnableTenantDeployments.Value == false { + fail() + } project.TenantedDeploymentMode = core.TenantedDeploymentModeTenantedOrUntenanted project, err = opts.Client.Projects.Update(project) } @@ -120,22 +179,36 @@ func connectRun(opts *ConnectOptions) error { func PromptMissing(opts *ConnectOptions) error { if opts.Tenant.Value == "" { + tenant, err := selectors.Select(opts.Ask, "Select the tenant", opts.GetAllTenantsCallback, func(tenant *tenants.Tenant) string { + return tenant.Name + }) + if err != nil { + return nil + } + opts.Tenant.Value = tenant.Name } - var selectedProject *projects.Project - var err error if opts.Project.Value == "" { - selectedProject, err = selectors.Project("Select the project to connect", opts.Client, opts.Ask) + project, err := projectSelector("Select the project to connect", opts.GetAllProjectsCallback, opts.Ask) if err != nil { return nil } - opts.Project.Value = selectedProject.GetName() + opts.Project.Value = project.GetName() + } + + err := PromptForEnablingTenantedDeployments(opts, opts.GetProjectCallback) + if err != nil { + return err } if opts.Environments.Value == nil || len(opts.Environments.Value) == 0 { + project, err := opts.GetProjectCallback(opts.Project.Value) + if err != nil { + return nil + } var progression *projects.Progression - progression, err = opts.Client.Projects.GetProgression(selectedProject) + progression, err = opts.GetProjectProgressionCallback(project) if len(progression.Environments) == 1 { opts.Environments.Value = append(opts.Environments.Value, progression.Environments[0].Name) } else { @@ -149,3 +222,51 @@ func PromptMissing(opts *ConnectOptions) error { return nil } + +func PromptForEnablingTenantedDeployments(opts *ConnectOptions, getProjectCallback GetProjectCallback) error { + if !opts.EnableTenantDeployments.Value { + project, err := getProjectCallback(opts.Project.Value) + if err != nil { + return err + } + if !supportsTenantedDeployments(project) { + opts.Ask(&survey.Confirm{ + Message: fmt.Sprintf("Do you want to enable tenanted deployments for %s?", project.GetName()), + Default: false, + }, &opts.EnableTenantDeployments.Value) + + if !opts.EnableTenantDeployments.Value { + fail() + } + } + } + + return nil +} + +func fail() { + panic("Cannot connect tenant to project that does not support tenanted deployments") +} + +func projectSelector(questionText string, getAllProjectsCallback GetAllProjectsCallback, ask question.Asker) (*projects.Project, error) { + existingProjects, err := getAllProjectsCallback() + if err != nil { + return nil, err + } + + return question.SelectMap(ask, questionText, existingProjects, getProjectDisplay()) +} + +func getProjectDisplay() func(p *projects.Project) string { + return func(p *projects.Project) string { + if p.TenantedDeploymentMode == core.TenantedDeploymentModeUntenanted { + return output.Dim(fmt.Sprintf("%s (Tenants not currently supported)", p.Name)) + } + + return p.GetName() + } +} + +func supportsTenantedDeployments(project *projects.Project) bool { + return project.TenantedDeploymentMode != core.TenantedDeploymentModeUntenanted +} From 0eed5b612f7eddb666dc6d843f66d26770c0f8d9 Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Wed, 19 Oct 2022 14:08:10 +1000 Subject: [PATCH 5/8] updated error condition for untented project --- pkg/cmd/tenant/connect/connect.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/tenant/connect/connect.go b/pkg/cmd/tenant/connect/connect.go index b1a4118b..6da61ac8 100644 --- a/pkg/cmd/tenant/connect/connect.go +++ b/pkg/cmd/tenant/connect/connect.go @@ -1,6 +1,7 @@ package connect import ( + "errors" "fmt" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc/v2" @@ -152,7 +153,7 @@ func connectRun(opts *ConnectOptions) error { if !supportsTenantedDeployments(project) { if opts.EnableTenantDeployments.Value == false { - fail() + getFailureMessageForUntenantedProject(project) } project.TenantedDeploymentMode = core.TenantedDeploymentModeTenantedOrUntenanted project, err = opts.Client.Projects.Update(project) @@ -236,7 +237,7 @@ func PromptForEnablingTenantedDeployments(opts *ConnectOptions, getProjectCallba }, &opts.EnableTenantDeployments.Value) if !opts.EnableTenantDeployments.Value { - fail() + return errors.New(getFailureMessageForUntenantedProject(project)) } } } @@ -244,8 +245,8 @@ func PromptForEnablingTenantedDeployments(opts *ConnectOptions, getProjectCallba return nil } -func fail() { - panic("Cannot connect tenant to project that does not support tenanted deployments") +func getFailureMessageForUntenantedProject(project *projects.Project) string { + return fmt.Sprintf("Cannot connect tenant to project '%s' as it does not support tenanted deployments.", project.GetName()) } func projectSelector(questionText string, getAllProjectsCallback GetAllProjectsCallback, ask question.Asker) (*projects.Project, error) { From 099f98c4f84c5076f86ede0900941285658de44c Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Wed, 19 Oct 2022 14:08:25 +1000 Subject: [PATCH 6/8] updated project list display message --- pkg/cmd/tenant/connect/connect.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/tenant/connect/connect.go b/pkg/cmd/tenant/connect/connect.go index 6da61ac8..62251212 100644 --- a/pkg/cmd/tenant/connect/connect.go +++ b/pkg/cmd/tenant/connect/connect.go @@ -260,11 +260,12 @@ func projectSelector(questionText string, getAllProjectsCallback GetAllProjectsC func getProjectDisplay() func(p *projects.Project) string { return func(p *projects.Project) string { - if p.TenantedDeploymentMode == core.TenantedDeploymentModeUntenanted { - return output.Dim(fmt.Sprintf("%s (Tenants not currently supported)", p.Name)) + if supportsTenantedDeployments(p) { + return p.GetName() + } - return p.GetName() + return output.Dim(fmt.Sprintf("%s (Tenanted deployments not currently supported)", p.Name)) } } From 5c1754072a9e394041aa5b1caebc7adf3733ded6 Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Wed, 19 Oct 2022 16:08:18 +1000 Subject: [PATCH 7/8] now with tests --- go.mod | 2 +- go.sum | 3 +- pkg/cmd/tenant/connect/connect.go | 17 +-- pkg/cmd/tenant/connect/connect_test.go | 137 +++++++++++++++++++++++++ test/testutil/fakesurvey.go | 11 ++ 5 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 pkg/cmd/tenant/connect/connect_test.go diff --git a/go.mod b/go.mod index a9da369c..dd1d0913 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/AlecAivazis/survey/v2 v2.3.5 github.com/MakeNowJust/heredoc/v2 v2.0.1 - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.10.0 + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.11.0 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 fcfaf33c..1dd3dbee 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/OctopusDeploy/go-octopusdeploy/v2 v2.9.1 h1:rnG8B0t759J3en86aTSi9nZ/d github.com/OctopusDeploy/go-octopusdeploy/v2 v2.9.1/go.mod h1:XWqxyDUVElUlTaPqyCBblukpsHSnPcAKkAHgJgbsIAs= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.10.0 h1:DdKeV+JYaF5Z5fl+P1LTByuZXBj+tKy+1xEuiADs/JM= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.10.0/go.mod h1:l8uu7lnA1mz/JwW3pXflKqzxTrIDfl/0r5jWBjzywTs= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.11.0 h1:olDO+5/VEF3SE/WfCtBQ10XWFj8CAlOb/+dgN52tdlw= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.11.0/go.mod h1:2j9rwRfb5qUs9PEJ3W331W84kRaNge5bed4D7JR1ruU= 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= @@ -416,7 +418,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= diff --git a/pkg/cmd/tenant/connect/connect.go b/pkg/cmd/tenant/connect/connect.go index 62251212..b3e76e77 100644 --- a/pkg/cmd/tenant/connect/connect.go +++ b/pkg/cmd/tenant/connect/connect.go @@ -56,7 +56,9 @@ func NewConnectOptions(connectFlags *ConnectFlags, dependencies *cmd.Dependencie ConnectFlags: connectFlags, GetAllTenantsCallback: func() ([]*tenants.Tenant, error) { return getAllTenants(*dependencies.Client) }, GetAllProjectsCallback: func() ([]*projects.Project, error) { return getAllProjects(*dependencies.Client) }, - GetProjectCallback: func(idOrName string) (*projects.Project, error) { return getProject(*dependencies.Client, idOrName) }, + GetProjectCallback: func(identifier string) (*projects.Project, error) { + return getProject(*dependencies.Client, identifier) + }, GetProjectProgressionCallback: func(project *projects.Project) (*projects.Progression, error) { return getProjectProgression(*dependencies.Client, project) }, @@ -153,7 +155,7 @@ func connectRun(opts *ConnectOptions) error { if !supportsTenantedDeployments(project) { if opts.EnableTenantDeployments.Value == false { - getFailureMessageForUntenantedProject(project) + return errors.New(getFailureMessageForUntenantedProject(project)) } project.TenantedDeploymentMode = core.TenantedDeploymentModeTenantedOrUntenanted project, err = opts.Client.Projects.Update(project) @@ -175,12 +177,13 @@ func connectRun(opts *ConnectOptions) error { return err } + fmt.Fprintf(opts.Out, "Successfully connected '%s' to '%s'.\n", tenant.Name, project.GetName()) return nil } func PromptMissing(opts *ConnectOptions) error { if opts.Tenant.Value == "" { - tenant, err := selectors.Select(opts.Ask, "Select the tenant", opts.GetAllTenantsCallback, func(tenant *tenants.Tenant) string { + tenant, err := selectors.Select(opts.Ask, "You have not specified a Tenant. Please select one:", opts.GetAllTenantsCallback, func(tenant *tenants.Tenant) string { return tenant.Name }) if err != nil { @@ -191,7 +194,7 @@ func PromptMissing(opts *ConnectOptions) error { } if opts.Project.Value == "" { - project, err := projectSelector("Select the project to connect", opts.GetAllProjectsCallback, opts.Ask) + project, err := projectSelector("You have not specified a Project. Please select one:", opts.GetAllProjectsCallback, opts.Ask) if err != nil { return nil } @@ -214,7 +217,7 @@ func PromptMissing(opts *ConnectOptions) error { opts.Environments.Value = append(opts.Environments.Value, progression.Environments[0].Name) } else { var selectedEnvironments []*resources.ReferenceDataItem - selectedEnvironments, err = question.MultiSelectMap(opts.Ask, "Select the environments to connect", progression.Environments, func(item *resources.ReferenceDataItem) string { return item.Name }, true) + selectedEnvironments, err = question.MultiSelectMap(opts.Ask, "You have not specified any environments. Please select at least one:", progression.Environments, func(item *resources.ReferenceDataItem) string { return item.Name }, true) for _, e := range selectedEnvironments { opts.Environments.Value = append(opts.Environments.Value, e.Name) } @@ -232,7 +235,7 @@ func PromptForEnablingTenantedDeployments(opts *ConnectOptions, getProjectCallba } if !supportsTenantedDeployments(project) { opts.Ask(&survey.Confirm{ - Message: fmt.Sprintf("Do you want to enable tenanted deployments for %s?", project.GetName()), + Message: fmt.Sprintf("Do you want to enable tenanted deployments for '%s'?", project.GetName()), Default: false, }, &opts.EnableTenantDeployments.Value) @@ -246,7 +249,7 @@ func PromptForEnablingTenantedDeployments(opts *ConnectOptions, getProjectCallba } func getFailureMessageForUntenantedProject(project *projects.Project) string { - return fmt.Sprintf("Cannot connect tenant to project '%s' as it does not support tenanted deployments.", project.GetName()) + return fmt.Sprintf("Cannot connect tenant to '%s' as it does not support tenanted deployments.", project.GetName()) } func projectSelector(questionText string, getAllProjectsCallback GetAllProjectsCallback, ask question.Asker) (*projects.Project, error) { diff --git a/pkg/cmd/tenant/connect/connect_test.go b/pkg/cmd/tenant/connect/connect_test.go new file mode 100644 index 00000000..074e830a --- /dev/null +++ b/pkg/cmd/tenant/connect/connect_test.go @@ -0,0 +1,137 @@ +package connect_test + +import ( + "github.com/OctopusDeploy/cli/pkg/cmd" + "github.com/OctopusDeploy/cli/pkg/cmd/tenant/connect" + "github.com/OctopusDeploy/cli/test/testutil" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/stretchr/testify/assert" + "net/url" + "testing" +) + +var serverUrl, _ = url.Parse("https://serverurl") +var spinner = &testutil.FakeSpinner{} +var rootResource = testutil.NewRootResource() + +func TestPromptMissing_AllOptionsSupplied(t *testing.T) { + pa := []*testutil.PA{} + + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + + flags := connect.NewConnectFlags() + flags.Tenant.Value = "Tennents" + flags.Project.Value = "Stella Artois" + flags.Environments.Value = []string{"Drouthy Neebors"} + + opts := connect.NewConnectOptions(flags, &cmd.Dependencies{Ask: asker}) + opts.GetAllTenantsCallback = func() ([]*tenants.Tenant, error) { + return []*tenants.Tenant{ + tenants.NewTenant(flags.Tenant.Value), + }, nil + } + opts.GetProjectCallback = func(id string) (*projects.Project, error) { + project := projects.NewProject(flags.Project.Name, "Lifecycles-1", "ProjectGroups-1") + project.TenantedDeploymentMode = core.TenantedDeploymentModeTenantedOrUntenanted + return project, nil + } + + err := connect.PromptMissing(opts) + checkRemainingPrompts() + assert.NoError(t, err) +} + +func TestPromptMissing_ProjectSupportsTenants(t *testing.T) { + pa := []*testutil.PA{ + testutil.NewSelectPrompt("You have not specified a Tenant. Please select one:", "", []string{"Tenant 1", "Tenant 2"}, "Tenant 1"), + testutil.NewSelectPrompt("You have not specified a Project. Please select one:", "", []string{"Project A", "Project B"}, "Project A"), + testutil.NewMultiSelectPrompt("You have not specified any environments. Please select at least one:", "", []string{"Env 1", "Env 2", "Env 3"}, []string{"Env 1", "Env 3"}), + } + + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + + flags := connect.NewConnectFlags() + opts := connect.NewConnectOptions(flags, &cmd.Dependencies{Ask: asker}) + opts.GetAllTenantsCallback = func() ([]*tenants.Tenant, error) { + return []*tenants.Tenant{ + tenants.NewTenant("Tenant 1"), + tenants.NewTenant("Tenant 2"), + }, nil + } + opts.GetAllProjectsCallback = func() ([]*projects.Project, error) { + return []*projects.Project{ + projects.NewProject("Project A", "Lifecycles-1", "ProjectGroups-1"), + projects.NewProject("Project B", "Lifecycles-1", "ProjectGroups-1"), + }, nil + } + + opts.GetProjectCallback = func(id string) (*projects.Project, error) { + project := projects.NewProject("Project A", "Lifecycles-1", "ProjectGroups-1") + project.TenantedDeploymentMode = core.TenantedDeploymentModeTenantedOrUntenanted + return project, nil + } + opts.GetProjectProgressionCallback = func(project *projects.Project) (*projects.Progression, error) { + return &projects.Progression{ + Environments: []*resources.ReferenceDataItem{ + {ID: "Environments-1", Name: "Env 1"}, + {ID: "Environments-2", Name: "Env 2"}, + {ID: "Environments-3", Name: "Env 3"}, + }, + }, nil + } + + err := connect.PromptMissing(opts) + checkRemainingPrompts() + assert.NoError(t, err) + assert.Equal(t, "Tenant 1", opts.Tenant.Value) + assert.Equal(t, "Project A", opts.Project.Value) + assert.Equal(t, []string{"Env 1", "Env 3"}, opts.Environments.Value) +} + +func TestPromptForEnablingTenantedDeployments_AnswerYes_ShouldError(t *testing.T) { + pa := []*testutil.PA{ + testutil.NewConfirmPrompt("Do you want to enable tenanted deployments for 'Project A'?", "", true), + } + + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + + flags := connect.NewConnectFlags() + flags.EnableTenantDeployments.Value = false + opts := connect.NewConnectOptions(flags, &cmd.Dependencies{Ask: asker}) + + opts.GetProjectCallback = func(id string) (*projects.Project, error) { + project := projects.NewProject("Project A", "Lifecycles-1", "ProjectGroups-1") + project.TenantedDeploymentMode = core.TenantedDeploymentModeUntenanted + return project, nil + } + + err := connect.PromptForEnablingTenantedDeployments(opts, opts.GetProjectCallback) + checkRemainingPrompts() + assert.NoError(t, err) + assert.True(t, opts.EnableTenantDeployments.Value) +} + +func TestPromptForEnablingTenantedDeployments_AnswerNo_ShouldError(t *testing.T) { + pa := []*testutil.PA{ + testutil.NewConfirmPrompt("Do you want to enable tenanted deployments for 'Project A'?", "", false), + } + + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + + flags := connect.NewConnectFlags() + flags.EnableTenantDeployments.Value = false + opts := connect.NewConnectOptions(flags, &cmd.Dependencies{Ask: asker}) + + opts.GetProjectCallback = func(id string) (*projects.Project, error) { + project := projects.NewProject("Project A", "Lifecycles-1", "ProjectGroups-1") + project.TenantedDeploymentMode = core.TenantedDeploymentModeUntenanted + return project, nil + } + + err := connect.PromptForEnablingTenantedDeployments(opts, opts.GetProjectCallback) + checkRemainingPrompts() + assert.Error(t, err, "Cannot connect tenant to 'Project A' as it does not support tenanted deployments.") +} diff --git a/test/testutil/fakesurvey.go b/test/testutil/fakesurvey.go index 97c95f1a..84870661 100644 --- a/test/testutil/fakesurvey.go +++ b/test/testutil/fakesurvey.go @@ -50,6 +50,17 @@ func NewSelectPrompt(prompt string, help string, options []string, response stri } } +func NewMultiSelectPrompt(prompt string, help string, options []string, responses []string) *PA { + return &PA{ + Prompt: &survey.MultiSelect{ + Message: prompt, + Options: options, + Help: help, + }, + Answer: responses, + } +} + func NewConfirmPrompt(prompt string, help string, response any) *PA { return &PA{ Prompt: &survey.Confirm{ From 696fa642e3a51d3da874351310cd0c31590fdd74 Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Wed, 19 Oct 2022 16:24:53 +1000 Subject: [PATCH 8/8] now with automation command --- pkg/cmd/tenant/connect/connect.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/tenant/connect/connect.go b/pkg/cmd/tenant/connect/connect.go index b3e76e77..850582c1 100644 --- a/pkg/cmd/tenant/connect/connect.go +++ b/pkg/cmd/tenant/connect/connect.go @@ -178,6 +178,10 @@ func connectRun(opts *ConnectOptions) error { } fmt.Fprintf(opts.Out, "Successfully connected '%s' to '%s'.\n", tenant.Name, project.GetName()) + if !opts.NoPrompt { + autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Tenant, opts.Project, opts.Environments, opts.EnableTenantDeployments) + fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd) + } return nil }