@@ -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+
651769func 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+
8601000func 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