Skip to content

Commit c349e2d

Browse files
authored
Merge pull request #523 from OctopusDeploy/ck/missing-packages-warning
feat: warn about missing packages
2 parents 7e4d973 + b5b93ff commit c349e2d

4 files changed

Lines changed: 231 additions & 11 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
77
github.com/MakeNowJust/heredoc/v2 v2.0.1
88
github.com/OctopusDeploy/go-octodiff v1.0.0
9-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0
9+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.73.0
1010
github.com/bmatcuk/doublestar/v4 v4.4.0
1111
github.com/briandowns/spinner v1.19.0
1212
github.com/google/uuid v1.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
4646
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
4747
github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0=
4848
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
49-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0 h1:q7bAzC/gdTvgeVxypHyTSlBYoH0ejbjE3VIyDfJ2lzw=
50-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0/go.mod h1:ZCOnCz9ae/uuOk7AIQ9NzjnzFbuN8Q7H3oj2Eq4QSgQ=
49+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.73.0 h1:zLDnx3vpFAoNnGLWlPy01Oxr2DjxEwdD5mRu+aoPArA=
50+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.73.0/go.mod h1:ZCOnCz9ae/uuOk7AIQ9NzjnzFbuN8Q7H3oj2Eq4QSgQ=
5151
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
5252
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
5353
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=

pkg/cmd/release/deploy/deploy.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"github.com/OctopusDeploy/cli/pkg/util/featuretoggle"
78
"golang.org/x/exp/maps"
89
"io"
910
"sort"
@@ -436,6 +437,14 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
436437
return err
437438
}
438439

440+
indicateMissingPackagesForReleaseFeatureToggleValue, err := featuretoggle.IsToggleEnabled(octopus, "indicate-missing-packages-for-release")
441+
if indicateMissingPackagesForReleaseFeatureToggleValue {
442+
proceed := promptMissingPackages(octopus, stdout, asker, selectedRelease);
443+
if !proceed {
444+
return errors.New("aborting deployment creation as requested")
445+
}
446+
}
447+
439448
// machine selection later on needs to refer back to the environments.
440449
// NOTE: this is allowed to remain nil; environments will get looked up later on if needed
441450
var selectedEnvironments []*environments.Environment
@@ -741,6 +750,37 @@ func askDeploymentPreviewVariables(octopus *octopusApiClient.Client, variablesFr
741750
return result, nil
742751
}
743752

753+
func promptMissingPackages(octopus *octopusApiClient.Client, stdout io.Writer, asker question.Asker, release *releases.Release) bool {
754+
missingPackages, err := releases.GetMissingPackages(octopus, release)
755+
if err != nil {
756+
// We don't want to prevent deployments from going through because of this check
757+
_, _ = fmt.Fprintf(stdout, "Unable to determine if there are missing packages for this release - %v\n", err)
758+
return true
759+
}
760+
761+
if len(missingPackages) == 0 {
762+
return true
763+
}
764+
765+
_, _ = fmt.Fprintf(stdout ,"Warning: The following packages are missing from the built-in feed for this release:\n")
766+
for _, p := range missingPackages {
767+
_, _ = fmt.Fprintf(stdout, " - %s (Version: %s)\n", p.ID, p.Version)
768+
}
769+
_, _ = fmt.Fprintln(stdout, "\nThis might cause the deployment to fail.")
770+
771+
prompt := &survey.Confirm{
772+
Message: "Do you want to continue?",
773+
Default: false,
774+
}
775+
776+
var answer bool
777+
if err := asker(prompt, &answer); err != nil {
778+
return answer
779+
}
780+
781+
return answer
782+
}
783+
744784
// FindDeployableEnvironmentIDs returns an array of environment IDs that we can deploy to,
745785
// the preferred 'next' environment, and an error
746786
func FindDeployableEnvironmentIDs(octopus *octopusApiClient.Client, release *releases.Release) ([]string, string, error) {

pkg/cmd/release/deploy/deploy_test.go

Lines changed: 188 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/configuration"
89
"net/url"
910
"testing"
1011
"time"
@@ -134,6 +135,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
134135
})
135136

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

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

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

209+
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
210+
FeatureToggles: []configuration.ConfiguredFeatureToggle{
211+
{
212+
Name: "indicate-missing-packages-for-release",
213+
IsEnabled: false,
214+
},
215+
},
216+
})
217+
200218
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
201219
Phases: []*releases.LifecycleProgressionPhase{
202220
{Name: "Dev", Progress: releases.PhaseProgressCurrent, AutomaticDeploymentTargets: []string{scratchEnvironment.ID}, OptionalDeploymentTargets: []string{devEnvironment.ID}},
@@ -269,6 +287,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
269287
})
270288

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

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

@@ -336,6 +362,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
336362
})
337363

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

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

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

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

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

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

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

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

685743
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
744+
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
745+
FeatureToggles: []configuration.ConfiguredFeatureToggle{
746+
{
747+
Name: "indicate-missing-packages-for-release",
748+
IsEnabled: false,
749+
},
750+
},
751+
})
752+
753+
// find environments via progression
754+
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
755+
Phases: []*releases.LifecycleProgressionPhase{
756+
{Name: "Dev", Progress: releases.PhaseProgressCurrent, AutomaticDeploymentTargets: []string{scratchEnvironment.ID}, OptionalDeploymentTargets: []string{devEnvironment.ID}},
757+
{Name: "Prod", Progress: releases.PhaseProgressPending, OptionalDeploymentTargets: []string{prodEnvironment.ID}}, // should scope this out due to pending
758+
},
759+
NextDeployments: []string{devEnvironment.ID},
760+
})
761+
api.ExpectRequest(t, "GET", fmt.Sprintf("/api/Spaces-1/environments?ids=%s%%2C%s", scratchEnvironment.ID, devEnvironment.ID)).RespondWith(resources.Resources[*environments.Environment]{
762+
Items: []*environments.Environment{scratchEnvironment, devEnvironment},
763+
})
764+
765+
// Note: scratch comes first but default should be dev, due to NextDeployments
766+
_ = qa.ExpectQuestion(t, &survey.MultiSelect{
767+
Message: "Select environment(s)",
768+
Options: []string{scratchEnvironment.Name, devEnvironment.Name},
769+
Default: []string{devEnvironment.Name},
770+
}).AnswerWith([]surveyCore.OptionAnswer{
771+
{Value: devEnvironment.Name, Index: 0},
772+
})
773+
774+
// now it's going to go looking for prompted variables; we don't have any prompted variables here so it skips
775+
api.ExpectRequest(t, "GET", "/api/Spaces-1/variables/"+variableSnapshotNoVars.ID).RespondWith(&variableSnapshotNoVars)
776+
emptyDeploymentPreviews := fixtures.EmptyDeploymentPreviews()
777+
api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/"+release19.ID+"/deployments/previews").RespondWith(&emptyDeploymentPreviews)
778+
779+
assert.Equal(t, heredoc.Doc(`
780+
Project Fire Project
781+
Release 1.9
782+
`), stdout.String())
783+
stdout.Reset()
784+
785+
q := qa.ExpectQuestion(t, &survey.Select{
786+
Message: "Change additional options?",
787+
Options: []string{"Proceed to deploy", "Change"},
788+
})
789+
assert.Regexp(t, "Additional Options", stdout.String()) // actual options tested in PrintAdvancedSummary
790+
_ = q.AnswerWith("Proceed to deploy")
791+
792+
err := <-errReceiver
793+
assert.Nil(t, err)
794+
795+
// check that the question-asking process has filled out the things we told it to
796+
assert.Equal(t, &executor.TaskOptionsDeployRelease{
797+
ProjectName: "Fire Project",
798+
ReleaseVersion: "1.9",
799+
Environments: []string{"dev"},
800+
GuidedFailureMode: "",
801+
Variables: make(map[string]string, 0),
802+
ReleaseID: release19.ID,
803+
}, options)
804+
}},
805+
806+
{"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) {
807+
options := &executor.TaskOptionsDeployRelease{
808+
ProjectName: "fire project",
809+
ReleaseVersion: "1.9",
810+
}
811+
812+
errReceiver := testutil.GoBegin(func() error {
813+
defer testutil.Close(api, qa)
814+
// NewClient makes network calls so we have to run it in the goroutine
815+
octopus, _ := octopusApiClient.NewClient(testutil.NewMockHttpClientWithTransport(api), serverUrl, placeholderApiKey, "")
816+
return deploy.AskQuestions(octopus, stdout, qa.AsAsker(), space1, options, now)
817+
})
818+
819+
api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
820+
api.ExpectRequest(t, "GET", "/api/spaces").RespondWith(rootResource)
821+
822+
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/fire project").RespondWithStatus(404, "NotFound", nil)
823+
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects?partialName=fire+project").
824+
RespondWith(resources.Resources[*projects.Project]{
825+
Items: []*projects.Project{fireProjectMaybeTenanted},
826+
})
827+
828+
_ = qa.ExpectQuestion(t, &survey.Select{
829+
Message: "Select Tenanted or Untenanted deployment",
830+
Options: []string{"Tenanted", "Untenanted"},
831+
}).AnswerWith("Untenanted")
832+
833+
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProjectID+"/releases/"+release19.Version).RespondWith(release19)
834+
api.ExpectRequest(t, "GET", "/api/configuration/feature-toggles?Name=indicate-missing-packages-for-release").RespondWith(&configuration.FeatureToggleConfigurationResponse{
835+
FeatureToggles: []configuration.ConfiguredFeatureToggle{
836+
{
837+
Name: "indicate-missing-packages-for-release",
838+
IsEnabled: true,
839+
},
840+
},
841+
})
842+
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/missingpackages").RespondWith(&releases.MissingPackages{
843+
Packages: []releases.MissingPackageInfo{
844+
{ID: "apples", Version: "1.0.0"},
845+
{ID: "bananas", Version: "2.0.0"},
846+
},
847+
})
848+
849+
_ = qa.ExpectQuestion(t, &survey.Confirm{
850+
Message: "Do you want to continue?",
851+
Default: false,
852+
}).AnswerWith("true")
686853

687854
// find environments via progression
688855
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/"+release19.ID+"/progression").RespondWith(&releases.LifecycleProgression{
@@ -713,6 +880,11 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
713880
assert.Equal(t, heredoc.Doc(`
714881
Project Fire Project
715882
Release 1.9
883+
Warning: The following packages are missing from the built-in feed for this release:
884+
- apples (Version: 1.0.0)
885+
- bananas (Version: 2.0.0)
886+
887+
This might cause the deployment to fail.
716888
`), stdout.String())
717889
stdout.Reset()
718890

@@ -743,14 +915,6 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
743915
errReceiver := testutil.GoBegin(func() error {
744916
defer testutil.Close(api, qa)
745917
octopus, _ := octopusApiClient.NewClient(testutil.NewMockHttpClientWithTransport(api), serverUrl, placeholderApiKey, "")
746-
//
747-
//api.ExpectRequest(t, "GET", "/api/Spaces-1/environments/all").RespondWith([]*environments.Environment{
748-
// devEnvironment, scratchEnvironment,
749-
//})
750-
//
751-
//emptyDeploymentPreviews := fixtures.EmptyDeploymentPreviews()
752-
//api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/"+release19.ID+"/deployments/previews").RespondWith(&emptyDeploymentPreviews)
753-
754918
return deploy.AskQuestions(octopus, stdout, qa.AsAsker(), space1, options, now)
755919
})
756920

@@ -871,6 +1035,14 @@ func TestDeployCreate_AskQuestions(t *testing.T) {
8711035
})
8721036

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

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

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

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

0 commit comments

Comments
 (0)