Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tenant connect #119

Merged
merged 10 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/project/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/project/view/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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"
"github.com/OctopusDeploy/cli/pkg/cmd/tenant"
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"
Expand Down Expand Up @@ -42,7 +42,7 @@ func NewCmdRoot(f factory.Factory, clientFactory apiclient.ClientFactory, askPro
// core
cmd.AddCommand(projectGroupCmd.NewCmdProjectGroup(f))
cmd.AddCommand(projectCmd.NewCmdProject(f))
cmd.AddCommand(tenant.NewCmdTenaant(f))
cmd.AddCommand(tenantCmd.NewCmdTenant(f))

// configuration
cmd.AddCommand(configCmd.NewCmdConfig(f))
Expand Down
281 changes: 281 additions & 0 deletions pkg/cmd/tenant/connect/connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
package connect

import (
"errors"
"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"
"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/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants"
"github.com/spf13/cobra"
)

const (
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]
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),
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(identifier string) (*projects.Project, error) {
return getProject(*dependencies.Client, identifier)
},
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 {
*cmd.Dependencies
*ConnectFlags
GetAllTenantsCallback GetAllTenantsCallback
GetAllProjectsCallback GetAllProjectsCallback
GetProjectCallback GetProjectCallback
GetProjectProgressionCallback GetProjectProgression
}

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(c *cobra.Command, args []string) error {
opts := NewConnectOptions(connectFlags, cmd.NewDependencies(f, c))

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)")
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
}

func connectRun(opts *ConnectOptions) error {
if !opts.NoPrompt {
if err := PromptMissing(opts); err != nil {
return err
}
}

tenant, err := opts.Client.Tenants.GetByIdentifier(opts.Tenant.Value)
if err != nil {
return err
}

project, err := opts.Client.Projects.GetByIdentifier(opts.Project.Value)
if err != nil {
return err
}

if !supportsTenantedDeployments(project) {
if opts.EnableTenantDeployments.Value == false {
return errors.New(getFailureMessageForUntenantedProject(project))
}
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
}

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
}

func PromptMissing(opts *ConnectOptions) error {
if opts.Tenant.Value == "" {
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 {
return nil
}

opts.Tenant.Value = tenant.Name
}

if opts.Project.Value == "" {
project, err := projectSelector("You have not specified a Project. Please select one:", opts.GetAllProjectsCallback, opts.Ask)
if err != nil {
return nil
}
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.GetProjectProgressionCallback(project)
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, "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)
}
}
}

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 {
return errors.New(getFailureMessageForUntenantedProject(project))
}
}
}

return nil
}

func getFailureMessageForUntenantedProject(project *projects.Project) string {
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) {
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 supportsTenantedDeployments(p) {
return p.GetName()

}

return output.Dim(fmt.Sprintf("%s (Tenanted deployments not currently supported)", p.Name))
}
}

func supportsTenantedDeployments(project *projects.Project) bool {
return project.TenantedDeploymentMode != core.TenantedDeploymentModeUntenanted
}
Loading