Skip to content
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 @@ -6,7 +6,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/OctopusDeploy/go-octodiff v1.0.0
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.73.0
github.com/bmatcuk/doublestar/v4 v4.4.0
github.com/briandowns/spinner v1.19.0
github.com/google/uuid v1.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0=
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0 h1:q7bAzC/gdTvgeVxypHyTSlBYoH0ejbjE3VIyDfJ2lzw=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0/go.mod h1:ZCOnCz9ae/uuOk7AIQ9NzjnzFbuN8Q7H3oj2Eq4QSgQ=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.73.0 h1:zLDnx3vpFAoNnGLWlPy01Oxr2DjxEwdD5mRu+aoPArA=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.73.0/go.mod h1:ZCOnCz9ae/uuOk7AIQ9NzjnzFbuN8Q7H3oj2Eq4QSgQ=
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=
Expand Down
40 changes: 40 additions & 0 deletions pkg/cmd/release/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/OctopusDeploy/cli/pkg/util/featuretoggle"
"golang.org/x/exp/maps"
"io"
"sort"
Expand Down Expand Up @@ -436,6 +437,14 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
return err
}

indicateMissingPackagesForReleaseFeatureToggleValue, err := featuretoggle.IsToggleEnabled(octopus, "indicate-missing-packages-for-release")
if indicateMissingPackagesForReleaseFeatureToggleValue {
proceed := promptMissingPackages(octopus, stdout, asker, selectedRelease);
if !proceed {
return errors.New("aborting deployment creation as requested")
}
}

// machine selection later on needs to refer back to the environments.
// NOTE: this is allowed to remain nil; environments will get looked up later on if needed
var selectedEnvironments []*environments.Environment
Expand Down Expand Up @@ -741,6 +750,37 @@ func askDeploymentPreviewVariables(octopus *octopusApiClient.Client, variablesFr
return result, nil
}

func promptMissingPackages(octopus *octopusApiClient.Client, stdout io.Writer, asker question.Asker, release *releases.Release) bool {
missingPackages, err := releases.GetMissingPackages(octopus, release)
if err != nil {
// We don't want to prevent deployments from going through because of this check
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

_, _ = fmt.Fprintf(stdout, "Unable to determine if there are missing packages for this release - %v\n", err)
return true
}

if len(missingPackages) == 0 {
return true
}

_, _ = fmt.Fprintf(stdout ,"Warning: The following packages are missing from the built-in feed for this release:\n")
for _, p := range missingPackages {
_, _ = fmt.Fprintf(stdout, " - %s (Version: %s)\n", p.ID, p.Version)
}
_, _ = fmt.Fprintln(stdout, "\nThis might cause the deployment to fail.")

prompt := &survey.Confirm{
Message: "Do you want to continue?",
Default: false,
}

var answer bool
if err := asker(prompt, &answer); err != nil {
return answer
}

return answer
}

// FindDeployableEnvironmentIDs returns an array of environment IDs that we can deploy to,
// the preferred 'next' environment, and an error
func FindDeployableEnvironmentIDs(octopus *octopusApiClient.Client, release *releases.Release) ([]string, string, error) {
Expand Down
196 changes: 188 additions & 8 deletions pkg/cmd/release/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/configuration"
"net/url"
"testing"
"time"
Expand Down Expand Up @@ -134,6 +135,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release.Version).RespondWith(release)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/variables/"+vars.ID).RespondWith(&vars)

Expand Down Expand Up @@ -197,6 +206,15 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
Options: []string{release20.Version, release19.Version},
}).AnswerWith(release19.Version)

api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
Phases: []*releases.LifecycleProgressionPhase{
{Name: "Dev", Progress: releases.PhaseProgressCurrent, AutomaticDeploymentTargets: []string{scratchEnvironment.ID}, OptionalDeploymentTargets: []string{devEnvironment.ID}},
Expand Down Expand Up @@ -269,6 +287,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

// doesn't lookup the progression or env names because it already has them

Expand Down Expand Up @@ -336,6 +362,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release20.Version).RespondWith(release20)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

// now it's going to go looking for prompted variables; we don't have any prompted variables here so it skips
api.ExpectRequest(t, "GET", "/api/Spaces-1/variables/"+variableSnapshotWithPromptedVariables.ID).RespondWith(&variableSnapshotWithPromptedVariables)
Expand Down Expand Up @@ -409,6 +443,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release20.Version).RespondWith(release20)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

// now it's going to go looking for prompted variables; we don't have any prompted variables here so it skips
api.ExpectRequest(t, "GET", "/api/Spaces-1/variables/"+variableSnapshotWithPromptedVariables.ID).RespondWith(&variableSnapshotWithPromptedVariables)
Expand Down Expand Up @@ -479,6 +521,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
Phases: []*releases.LifecycleProgressionPhase{
Expand Down Expand Up @@ -582,6 +632,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
}).AnswerWith("Tenanted")

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

// find environments via progression
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
Expand Down Expand Up @@ -683,6 +741,115 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
}).AnswerWith("Untenanted")

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

// find environments via progression
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
Phases: []*releases.LifecycleProgressionPhase{
{Name: "Dev", Progress: releases.PhaseProgressCurrent, AutomaticDeploymentTargets: []string{scratchEnvironment.ID}, OptionalDeploymentTargets: []string{devEnvironment.ID}},
{Name: "Prod", Progress: releases.PhaseProgressPending, OptionalDeploymentTargets: []string{prodEnvironment.ID}}, // should scope this out due to pending
},
NextDeployments: []string{devEnvironment.ID},
})
api.ExpectRequest(t, "GET", fmt.Sprintf("/api/Spaces-1/environments?ids=%s%%2C%s", scratchEnvironment.ID, devEnvironment.ID)).RespondWith(resources.Resources[*environments.Environment]{
Items: []*environments.Environment{scratchEnvironment, devEnvironment},
})

// Note: scratch comes first but default should be dev, due to NextDeployments
_ = qa.ExpectQuestion(t, &survey.MultiSelect{
Message: "Select environment(s)",
Options: []string{scratchEnvironment.Name, devEnvironment.Name},
Default: []string{devEnvironment.Name},
}).AnswerWith([]surveyCore.OptionAnswer{
{Value: devEnvironment.Name, Index: 0},
})

// now it's going to go looking for prompted variables; we don't have any prompted variables here so it skips
api.ExpectRequest(t, "GET", "/api/Spaces-1/variables/"+variableSnapshotNoVars.ID).RespondWith(&variableSnapshotNoVars)
emptyDeploymentPreviews := fixtures.EmptyDeploymentPreviews()
api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/"+release19.ID+"/deployments/previews").RespondWith(&emptyDeploymentPreviews)

assert.Equal(t, heredoc.Doc(`
Project Fire Project
Release 1.9
`), stdout.String())
stdout.Reset()

q := qa.ExpectQuestion(t, &survey.Select{
Message: "Change additional options?",
Options: []string{"Proceed to deploy", "Change"},
})
assert.Regexp(t, "Additional Options", stdout.String()) // actual options tested in PrintAdvancedSummary
_ = q.AnswerWith("Proceed to deploy")

err := <-errReceiver
assert.Nil(t, err)

// check that the question-asking process has filled out the things we told it to
assert.Equal(t, &executor.TaskOptionsDeployRelease{
ProjectName: "Fire Project",
ReleaseVersion: "1.9",
Environments: []string{"dev"},
GuidedFailureMode: "",
Variables: make(map[string]string, 0),
ReleaseID: release19.ID,
}, options)
}},

{"prompt if feature toggle is on and a release has missing packages", func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, stdout *bytes.Buffer) {
options := &executor.TaskOptionsDeployRelease{
ProjectName: "fire project",
ReleaseVersion: "1.9",
}

errReceiver := testutil.GoBegin(func() error {
defer testutil.Close(api, qa)
// NewClient makes network calls so we have to run it in the goroutine
octopus, _ := octopusApiClient.NewClient(testutil.NewMockHttpClientWithTransport(api), serverUrl, placeholderApiKey, "")
return deploy.AskQuestions(octopus, stdout, qa.AsAsker(), space1, options, now)
})

api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/spaces").RespondWith(rootResource)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/fire project").RespondWithStatus(404, "NotFound", nil)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects?partialName=fire+project").
RespondWith(resources.Resources[*projects.Project]{
Items: []*projects.Project{fireProjectMaybeTenanted},
})

_ = qa.ExpectQuestion(t, &survey.Select{
Message: "Select Tenanted or Untenanted deployment",
Options: []string{"Tenanted", "Untenanted"},
}).AnswerWith("Untenanted")

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: true,
},
},
})
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/missingpackages").RespondWith(&releases.MissingPackages{
Packages: []releases.MissingPackageInfo{
{ID: "apples", Version: "1.0.0"},
{ID: "bananas", Version: "2.0.0"},
},
})

_ = qa.ExpectQuestion(t, &survey.Confirm{
Message: "Do you want to continue?",
Default: false,
}).AnswerWith("true")

// find environments via progression
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
Expand Down Expand Up @@ -713,6 +880,11 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
assert.Equal(t, heredoc.Doc(`
Project Fire Project
Release 1.9
Warning: The following packages are missing from the built-in feed for this release:
- apples (Version: 1.0.0)
- bananas (Version: 2.0.0)

This might cause the deployment to fail.
`), stdout.String())
stdout.Reset()

Expand Down Expand Up @@ -743,14 +915,6 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
errReceiver := testutil.GoBegin(func() error {
defer testutil.Close(api, qa)
octopus, _ := octopusApiClient.NewClient(testutil.NewMockHttpClientWithTransport(api), serverUrl, placeholderApiKey, "")
//
//api.ExpectRequest(t, "GET", "/api/Spaces-1/environments/all").RespondWith([]*environments.Environment{
// devEnvironment, scratchEnvironment,
//})
//
//emptyDeploymentPreviews := fixtures.EmptyDeploymentPreviews()
//api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/"+release19.ID+"/deployments/previews").RespondWith(&emptyDeploymentPreviews)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaning up some commented out code

return deploy.AskQuestions(octopus, stdout, qa.AsAsker(), space1, options, now)
})

Expand Down Expand Up @@ -871,6 +1035,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
Phases: []*releases.LifecycleProgressionPhase{
Expand Down Expand Up @@ -1689,6 +1861,14 @@ func TestDeployCreate_GenerationOfAutomationCommand_MasksSensitiveVariables(t *t
})

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release20.Version).RespondWith(release20)
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
FeatureToggles: []configuration.ConfiguredFeatureToggle{
{
Name: "indicate-missing-packages-for-release",
IsEnabled: false,
},
},
})

// now it's going to go looking for prompted variables; we don't have any prompted variables here so it skips
api.ExpectRequest(t, "GET", "/api/Spaces-1/variables/"+variableSnapshotWithPromptedVariables.ID).RespondWith(&variableSnapshotWithPromptedVariables)
Expand Down