Skip to content

Commit 4d95272

Browse files
Support deploying releases to ephemeral environments (#558)
1 parent 0fc0cdd commit 4d95272

3 files changed

Lines changed: 472 additions & 71 deletions

File tree

pkg/cmd/release/deploy/deploy.go

Lines changed: 209 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7-
"github.com/OctopusDeploy/cli/pkg/util/featuretoggle"
8-
"golang.org/x/exp/maps"
97
"io"
108
"sort"
119
"strings"
1210
"time"
1311

12+
"github.com/OctopusDeploy/cli/pkg/util/featuretoggle"
13+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments/v2/ephemeralenvironments"
14+
"golang.org/x/exp/maps"
15+
1416
"github.com/OctopusDeploy/cli/pkg/apiclient"
1517

1618
"github.com/AlecAivazis/survey/v2"
@@ -414,9 +416,10 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
414416
// select release
415417

416418
var selectedRelease *releases.Release
419+
var selectedChannel *channels.Channel
417420
if options.ReleaseVersion == "" {
418421
// first we want to ask them to pick a channel just to narrow down the search space for releases (not sent to server)
419-
selectedChannel, err := selectors.Channel(octopus, asker, stdout, "Select channel", selectedProject)
422+
selectedChannel, err = selectors.Channel(octopus, asker, stdout, "Select channel", selectedProject)
420423
if err != nil {
421424
return err
422425
}
@@ -429,6 +432,10 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
429432
if err != nil {
430433
return err
431434
}
435+
selectedChannel, err = channels.GetByID(octopus, space.ID, selectedRelease.ChannelID)
436+
if err != nil {
437+
return err
438+
}
432439
_, _ = fmt.Fprintf(stdout, "Release %s\n", output.Cyan(selectedRelease.Version))
433440
}
434441
options.ReleaseVersion = selectedRelease.Version
@@ -439,87 +446,55 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
439446

440447
indicateMissingPackagesForReleaseFeatureToggleValue, err := featuretoggle.IsToggleEnabled(octopus, "indicate-missing-packages-for-release")
441448
if indicateMissingPackagesForReleaseFeatureToggleValue {
442-
proceed := promptMissingPackages(octopus, stdout, asker, selectedRelease);
449+
proceed := promptMissingPackages(octopus, stdout, asker, selectedRelease)
443450
if !proceed {
444451
return errors.New("aborting deployment creation as requested")
445452
}
446453
}
447454

448455
// machine selection later on needs to refer back to the environments.
449456
// NOTE: this is allowed to remain nil; environments will get looked up later on if needed
450-
var selectedEnvironments []*environments.Environment
451-
if isTenanted {
452-
var selectedEnvironment *environments.Environment
453-
if len(options.Environments) == 0 {
454-
deployableEnvironmentIDs, nextEnvironmentID, err := FindDeployableEnvironmentIDs(octopus, selectedRelease)
455-
if err != nil {
456-
return err
457-
}
458-
selectedEnvironment, err = selectDeploymentEnvironment(asker, octopus, deployableEnvironmentIDs, nextEnvironmentID)
459-
if err != nil {
460-
return err
461-
}
462-
options.Environments = []string{selectedEnvironment.Name} // executions api allows env names, so let's use these instead so they look nice in generated automationcmd
463-
} else {
464-
selectedEnvironment, err = selectors.FindEnvironment(octopus, options.Environments[0])
465-
if err != nil {
466-
return err
467-
}
468-
_, _ = fmt.Fprintf(stdout, "Environment %s\n", output.Cyan(selectedEnvironment.Name))
457+
var deploymentEnvironmentIDs []string
458+
if selectedChannel.Type == channels.ChannelTypeLifecycle {
459+
deploymentEnvironmentIDs, err = selectDeploymentEnvironmentsForLifecycleChannel(octopus, stdout, asker, options, selectedRelease, isTenanted)
460+
if err != nil {
461+
return err
469462
}
470-
selectedEnvironments = []*environments.Environment{selectedEnvironment}
471-
472-
// ask for tenants and/or tags unless some were specified on the command line
473-
if len(options.Tenants) == 0 && len(options.TenantTags) == 0 {
474-
options.Tenants, options.TenantTags, err = executionscommon.AskTenantsAndTags(asker, octopus, selectedRelease.ProjectID, selectedEnvironments, true)
475-
if len(options.Tenants) == 0 && len(options.TenantTags) == 0 {
476-
return errors.New("no tenants or tags available; cannot deploy")
477-
}
478-
if err != nil {
479-
return err
480-
}
481-
} else {
482-
if len(options.Tenants) > 0 {
483-
_, _ = fmt.Fprintf(stdout, "Tenants %s\n", output.Cyan(strings.Join(options.Tenants, ",")))
484-
}
485-
if len(options.TenantTags) > 0 {
486-
_, _ = fmt.Fprintf(stdout, "Tenant Tags %s\n", output.Cyan(strings.Join(options.TenantTags, ",")))
487-
}
463+
} else if selectedChannel.Type == channels.ChannelTypeEphemeral {
464+
deploymentEnvironmentIDs, err = selectDeploymentEnvironmentsForEphemeralChannel(octopus, stdout, asker, options, selectedRelease)
465+
if err != nil {
466+
return err
488467
}
489468
} else {
490-
if len(options.Environments) == 0 {
491-
deployableEnvironmentIDs, nextEnvironmentID, err := FindDeployableEnvironmentIDs(octopus, selectedRelease)
492-
if err != nil {
493-
return err
494-
}
495-
selectedEnvironments, err = selectDeploymentEnvironments(asker, octopus, deployableEnvironmentIDs, nextEnvironmentID)
496-
if err != nil {
497-
return err
498-
}
499-
options.Environments = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.Name })
500-
} else {
501-
if len(options.Environments) > 0 {
502-
_, _ = fmt.Fprintf(stdout, "Environments %s\n", output.Cyan(strings.Join(options.Environments, ",")))
503-
}
504-
}
469+
return errors.New("invalid channel type: " + string(selectedChannel.Type))
505470
}
506471

507472
variableSet, err := variables.GetVariableSet(octopus, space.ID, selectedRelease.ProjectVariableSetSnapshotID)
508473
if err != nil {
509474
return err
510475
}
511476

512-
if len(selectedEnvironments) == 0 { // if the Q&A process earlier hasn't loaded environments already, we need to load them now
513-
selectedEnvironments, err = executionscommon.FindEnvironments(octopus, options.Environments)
514-
if err != nil {
515-
return err
477+
if len(deploymentEnvironmentIDs) == 0 { // if the Q&A process earlier hasn't loaded environments already, we need to load them now
478+
if selectedChannel.Type == channels.ChannelTypeLifecycle {
479+
selectedEnvironments, err := executionscommon.FindEnvironments(octopus, options.Environments)
480+
if err != nil {
481+
return err
482+
}
483+
484+
deploymentEnvironmentIDs = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.ID })
485+
} else if selectedChannel.Type == channels.ChannelTypeEphemeral {
486+
deploymentEnvironmentIDs, err = findEphemeralEnvironmentIDs(octopus, space, options.Environments)
487+
488+
if err != nil {
489+
return err
490+
}
516491
}
517492
}
518493

519494
var deploymentPreviewRequests []deployments.DeploymentPreviewRequest
520-
for _, environment := range selectedEnvironments {
495+
for _, environmentId := range deploymentEnvironmentIDs {
521496
preview := deployments.DeploymentPreviewRequest{
522-
EnvironmentId: environment.ID,
497+
EnvironmentId: environmentId,
523498
// We ignore the TenantId here as we're just using the deployments previews for prompted variables.
524499
// Tenant variables do not support prompted variables
525500
TenantId: "",
@@ -632,13 +607,14 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
632607
}
633608

634609
if !isDeploymentTargetsSpecified {
635-
if len(selectedEnvironments) == 0 { // if the Q&A process earlier hasn't loaded environments already, we need to load them now
636-
selectedEnvironments, err = executionscommon.FindEnvironments(octopus, options.Environments)
610+
if len(deploymentEnvironmentIDs) == 0 { // if the Q&A process earlier hasn't loaded environments already, we need to load them now
611+
selectedEnvironments, err := executionscommon.FindEnvironments(octopus, options.Environments)
637612
if err != nil {
638613
return err
639614
}
615+
deploymentEnvironmentIDs = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.ID })
640616
}
641-
options.DeploymentTargets, err = askDeploymentTargets(octopus, asker, space.ID, selectedRelease.ID, selectedEnvironments)
617+
options.DeploymentTargets, err = askDeploymentTargets(octopus, asker, space.ID, selectedRelease.ID, deploymentEnvironmentIDs)
642618
if err != nil {
643619
return err
644620
}
@@ -648,6 +624,148 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
648624
return nil
649625
}
650626

627+
func findEphemeralEnvironmentIDs(octopus *octopusApiClient.Client, space *spaces.Space, environments []string) ([]string, error) {
628+
allEphemeralEnvironments, err := ephemeralenvironments.GetAll(octopus, space.ID)
629+
if err != nil {
630+
return nil, err
631+
}
632+
633+
if allEphemeralEnvironments == nil || allEphemeralEnvironments.TotalResults == 0 {
634+
return nil, errors.New("no ephemeral environments exist to deploy to")
635+
}
636+
637+
var selectedEnvironments []string
638+
if len(environments) == 0 {
639+
return nil, nil
640+
}
641+
642+
envMap := make(map[string]*ephemeralenvironments.EphemeralEnvironment, len(allEphemeralEnvironments.Items)*2)
643+
for _, ephemeralEnv := range allEphemeralEnvironments.Items {
644+
envMap[ephemeralEnv.ID] = ephemeralEnv
645+
envMap[ephemeralEnv.Name] = ephemeralEnv
646+
}
647+
648+
for _, envIdentifier := range environments {
649+
ephemeralEnv, found := envMap[envIdentifier]
650+
if !found {
651+
return nil, fmt.Errorf("environment '%s' not found in ephemeral environments", envIdentifier)
652+
}
653+
selectedEnvironments = append(selectedEnvironments, ephemeralEnv.ID)
654+
}
655+
656+
return selectedEnvironments, nil
657+
}
658+
659+
func selectDeploymentEnvironmentsForEphemeralChannel(octopus *octopusApiClient.Client, stdout io.Writer, asker question.Asker, options *executor.TaskOptionsDeployRelease, selectedRelease *releases.Release) ([]string, error) {
660+
var deploymentEnvironmentIds []string
661+
var selectedEnvironments []*ephemeralenvironments.EphemeralEnvironment
662+
663+
if len(options.Environments) == 0 {
664+
allEphemeralEnvironments, err := ephemeralenvironments.GetAll(octopus, selectedRelease.SpaceID)
665+
if err != nil {
666+
return nil, err
667+
}
668+
if allEphemeralEnvironments == nil || allEphemeralEnvironments.TotalResults == 0 {
669+
return nil, errors.New("no ephemeral environments exist to deploy to")
670+
}
671+
672+
deploymentEnvironmentTemplate, err := releases.GetReleaseDeploymentTemplate(octopus, selectedRelease.SpaceID, selectedRelease.ID)
673+
if err != nil {
674+
return nil, err
675+
}
676+
677+
allowedEnvironmentIds := map[string]bool{}
678+
for _, p := range deploymentEnvironmentTemplate.PromoteTo {
679+
allowedEnvironmentIds[p.ID] = true
680+
}
681+
682+
var availableEnvironments []*ephemeralenvironments.EphemeralEnvironment
683+
for _, env := range allEphemeralEnvironments.Items {
684+
if _, ok := allowedEnvironmentIds[env.ID]; ok {
685+
availableEnvironments = append(availableEnvironments, env)
686+
}
687+
}
688+
689+
if len(availableEnvironments) > 0 {
690+
selectedEnvironments, err = selectEphemeralDeploymentEnvironments(asker, availableEnvironments)
691+
if err != nil {
692+
return nil, err
693+
}
694+
deploymentEnvironmentIds = util.SliceTransform(selectedEnvironments, func(env *ephemeralenvironments.EphemeralEnvironment) string { return env.ID })
695+
options.Environments = util.SliceTransform(selectedEnvironments, func(env *ephemeralenvironments.EphemeralEnvironment) string { return env.Name })
696+
}
697+
} else {
698+
_, _ = fmt.Fprintf(stdout, "Environments %s\n", output.Cyan(strings.Join(options.Environments, ",")))
699+
}
700+
701+
return deploymentEnvironmentIds, nil
702+
}
703+
704+
func selectDeploymentEnvironmentsForLifecycleChannel(octopus *octopusApiClient.Client, stdout io.Writer, asker question.Asker, options *executor.TaskOptionsDeployRelease, selectedRelease *releases.Release, isTenanted bool) ([]string, error) {
705+
var deploymentEnvironmentIds []string
706+
var selectedEnvironments []*environments.Environment
707+
var err error
708+
709+
if isTenanted {
710+
var selectedEnvironment *environments.Environment
711+
if len(options.Environments) == 0 {
712+
deployableEnvironmentIDs, nextEnvironmentID, err := FindDeployableEnvironmentIDs(octopus, selectedRelease)
713+
if err != nil {
714+
return nil, err
715+
}
716+
selectedEnvironment, err = selectDeploymentEnvironment(asker, octopus, deployableEnvironmentIDs, nextEnvironmentID)
717+
if err != nil {
718+
return nil, err
719+
}
720+
options.Environments = []string{selectedEnvironment.Name} // executions api allows env names, so let's use these instead so they look nice in generated automationcmd
721+
} else {
722+
selectedEnvironment, err = selectors.FindEnvironment(octopus, options.Environments[0])
723+
if err != nil {
724+
return nil, err
725+
}
726+
_, _ = fmt.Fprintf(stdout, "Environment %s\n", output.Cyan(selectedEnvironment.Name))
727+
}
728+
selectedEnvironments = []*environments.Environment{selectedEnvironment}
729+
deploymentEnvironmentIds = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.ID })
730+
731+
// ask for tenants and/or tags unless some were specified on the command line
732+
if len(options.Tenants) == 0 && len(options.TenantTags) == 0 {
733+
options.Tenants, options.TenantTags, err = executionscommon.AskTenantsAndTags(asker, octopus, selectedRelease.ProjectID, selectedEnvironments, true)
734+
if len(options.Tenants) == 0 && len(options.TenantTags) == 0 {
735+
return nil, errors.New("no tenants or tags available; cannot deploy")
736+
}
737+
if err != nil {
738+
return nil, err
739+
}
740+
} else {
741+
if len(options.Tenants) > 0 {
742+
_, _ = fmt.Fprintf(stdout, "Tenants %s\n", output.Cyan(strings.Join(options.Tenants, ",")))
743+
}
744+
if len(options.TenantTags) > 0 {
745+
_, _ = fmt.Fprintf(stdout, "Tenant Tags %s\n", output.Cyan(strings.Join(options.TenantTags, ",")))
746+
}
747+
}
748+
} else {
749+
if len(options.Environments) == 0 {
750+
deployableEnvironmentIDs, nextEnvironmentID, err := FindDeployableEnvironmentIDs(octopus, selectedRelease)
751+
if err != nil {
752+
return nil, err
753+
}
754+
selectedEnvironments, err = selectDeploymentEnvironments(asker, octopus, deployableEnvironmentIDs, nextEnvironmentID)
755+
if err != nil {
756+
return nil, err
757+
}
758+
deploymentEnvironmentIds = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.ID })
759+
options.Environments = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.Name })
760+
} else {
761+
if len(options.Environments) > 0 {
762+
_, _ = fmt.Fprintf(stdout, "Environments %s\n", output.Cyan(strings.Join(options.Environments, ",")))
763+
}
764+
}
765+
}
766+
return deploymentEnvironmentIds, nil
767+
}
768+
651769
func validateDeployment(isTenanted bool, environments []string) error {
652770
if isTenanted && len(environments) > 1 {
653771
return fmt.Errorf("tenanted deployments can only specify one environment")
@@ -656,12 +774,12 @@ func validateDeployment(isTenanted bool, environments []string) error {
656774
return nil
657775
}
658776

659-
func askDeploymentTargets(octopus *octopusApiClient.Client, asker question.Asker, spaceID string, releaseID string, selectedEnvironments []*environments.Environment) ([]string, error) {
777+
func askDeploymentTargets(octopus *octopusApiClient.Client, asker question.Asker, spaceID string, releaseID string, deploymentEnvironmentIDs []string) ([]string, error) {
660778
var results []string
661779

662780
// this is what the portal does. Can we do it better? I don't know
663-
for _, env := range selectedEnvironments {
664-
preview, err := deployments.GetReleaseDeploymentPreview(octopus, spaceID, releaseID, env.ID, true)
781+
for _, envID := range deploymentEnvironmentIDs {
782+
preview, err := deployments.GetReleaseDeploymentPreview(octopus, spaceID, releaseID, envID, true)
665783
if err != nil {
666784
return nil, err
667785
}
@@ -762,7 +880,7 @@ func promptMissingPackages(octopus *octopusApiClient.Client, stdout io.Writer, a
762880
return true
763881
}
764882

765-
_, _ = fmt.Fprintf(stdout ,"Warning: The following packages are missing from the built-in feed for this release:\n")
883+
_, _ = fmt.Fprintf(stdout, "Warning: The following packages are missing from the built-in feed for this release:\n")
766884
for _, p := range missingPackages {
767885
_, _ = fmt.Fprintf(stdout, " - %s (Version: %s)\n", p.ID, p.Version)
768886
}
@@ -857,6 +975,28 @@ func selectDeploymentEnvironment(asker question.Asker, octopus *octopusApiClient
857975
return selectedValue, nil
858976
}
859977

978+
func selectEphemeralDeploymentEnvironments(asker question.Asker, deployableEnvironments []*ephemeralenvironments.EphemeralEnvironment) ([]*ephemeralenvironments.EphemeralEnvironment, error) {
979+
var err error
980+
optionMap, options := question.MakeItemMapAndOptions(deployableEnvironments, func(e *ephemeralenvironments.EphemeralEnvironment) string { return e.Name })
981+
var selectedKeys []string
982+
err = asker(&survey.MultiSelect{
983+
Message: "Select environment(s)",
984+
Options: options,
985+
Default: nil,
986+
}, &selectedKeys, survey.WithValidator(survey.Required))
987+
988+
if err != nil {
989+
return nil, err
990+
}
991+
var selectedValues []*ephemeralenvironments.EphemeralEnvironment
992+
for _, k := range selectedKeys {
993+
if value, ok := optionMap[k]; ok {
994+
selectedValues = append(selectedValues, value)
995+
} // if we were to somehow get invalid answers, ignore them
996+
}
997+
return selectedValues, nil
998+
}
999+
8601000
func selectDeploymentEnvironments(asker question.Asker, octopus *octopusApiClient.Client, deployableEnvironmentIDs []string, nextDeployEnvironmentID string) ([]*environments.Environment, error) {
8611001
allEnvs, nextDeployEnvironmentName, err := loadEnvironmentsForDeploy(octopus, deployableEnvironmentIDs, nextDeployEnvironmentID)
8621002
if err != nil {

0 commit comments

Comments
 (0)