diff --git a/acceptance/apply_changes_test.go b/acceptance/apply_changes_test.go index ff9b477a..b5fa3fcb 100644 --- a/acceptance/apply_changes_test.go +++ b/acceptance/apply_changes_test.go @@ -1,10 +1,11 @@ package acceptance import ( - "github.com/onsi/gomega/ghttp" "net/http" "os/exec" + "github.com/onsi/gomega/ghttp" + "github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/gexec" @@ -49,6 +50,7 @@ var _ = Describe("apply-changes command", func() { ghttp.VerifyRequest("POST", "/api/v0/installations"), ghttp.VerifyJSON(`{ "ignore_warnings": "false", + "force_latest_variables": false, "deploy_products": "all" }`), ghttp.RespondWith(http.StatusOK, `{"install": {"id": 42}}`), @@ -130,6 +132,7 @@ var _ = Describe("apply-changes command", func() { ghttp.VerifyRequest("POST", "/api/v0/installations"), ghttp.VerifyJSON(`{ "ignore_warnings": "false", + "force_latest_variables": false, "deploy_products": "all" }`), ghttp.RespondWith(http.StatusOK, `{"install": {"id": 42}}`), diff --git a/api/installations_service.go b/api/installations_service.go index 82e8c6db..24246e96 100644 --- a/api/installations_service.go +++ b/api/installations_service.go @@ -74,7 +74,7 @@ func (a Api) ListInstallations() ([]InstallationsServiceOutput, error) { return responseStruct.Installations, nil } -func (a Api) CreateInstallation(ignoreWarnings bool, deployProducts bool, productNames []string, errands ApplyErrandChanges) (InstallationsServiceOutput, error) { +func (a Api) CreateInstallation(ignoreWarnings bool, deployProducts bool, forceLatestVariables bool, productNames []string, errands ApplyErrandChanges) (InstallationsServiceOutput, error) { productGuidMapping, err := a.fetchProductGUID() if err != nil { return InstallationsServiceOutput{}, fmt.Errorf("failed to list staged and/or deployed products: %w", err) @@ -110,13 +110,15 @@ func (a Api) CreateInstallation(ignoreWarnings bool, deployProducts bool, produc } data, err := json.Marshal(&struct { - IgnoreWarnings string `json:"ignore_warnings"` - DeployProducts interface{} `json:"deploy_products"` - Errands map[string]ProductErrand `json:"errands,omitempty"` + IgnoreWarnings string `json:"ignore_warnings"` + ForceLatestVariables bool `json:"force_latest_variables"` + DeployProducts interface{} `json:"deploy_products"` + Errands map[string]ProductErrand `json:"errands,omitempty"` }{ - IgnoreWarnings: fmt.Sprintf("%t", ignoreWarnings), - DeployProducts: deployProductsVal, - Errands: errandsPayload, + IgnoreWarnings: fmt.Sprintf("%t", ignoreWarnings), + ForceLatestVariables: forceLatestVariables, + DeployProducts: deployProductsVal, + Errands: errandsPayload, }) if err != nil { return InstallationsServiceOutput{}, err diff --git a/api/installations_service_test.go b/api/installations_service_test.go index 1994ac96..6919fbce 100644 --- a/api/installations_service_test.go +++ b/api/installations_service_test.go @@ -1,14 +1,16 @@ package api_test import ( - "github.com/onsi/gomega/gbytes" - "github.com/onsi/gomega/ghttp" "log" "net/http" "time" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/ghttp" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/pivotal-cf/om/api" ) @@ -108,12 +110,12 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings":"false", "deploy_products":"all"}`), + ghttp.VerifyJSON(`{"ignore_warnings":"false","force_latest_variables":false, "deploy_products":"all"}`), ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - output, err := service.CreateInstallation(false, true, nil, api.ApplyErrandChanges{}) + output, err := service.CreateInstallation(false, true, false, nil, api.ApplyErrandChanges{}) Expect(err).ToNot(HaveOccurred()) Expect(output.ID).To(Equal(1)) @@ -133,12 +135,12 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings":"false", "deploy_products":"none"}`), + ghttp.VerifyJSON(`{"ignore_warnings":"false","force_latest_variables":false, "deploy_products":"none"}`), ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - output, err := service.CreateInstallation(false, false, nil, api.ApplyErrandChanges{}) + output, err := service.CreateInstallation(false, false, false, nil, api.ApplyErrandChanges{}) Expect(err).ToNot(HaveOccurred()) Expect(output.ID).To(Equal(1)) @@ -158,30 +160,38 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings":"false","deploy_products":["guid2"]}`), + ghttp.VerifyJSON(`{"ignore_warnings":"false","force_latest_variables":false,"deploy_products":["guid2"]}`), ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - output, err := service.CreateInstallation(false, true, []string{"product2"}, api.ApplyErrandChanges{}) + output, err := service.CreateInstallation(false, true, false, []string{"product2"}, api.ApplyErrandChanges{}) Expect(err).ToNot(HaveOccurred()) Expect(output.ID).To(Equal(1)) }) + }) - It("errors when the product does not exist", func() { + When("forcing latest variables", func() { + It("triggers an installation on an Ops Manager, forcing latest variables", func() { client.AppendHandlers( ghttp.CombineHandlers( ghttp.VerifyRequest("GET", "/api/v0/staged/products"), - ghttp.RespondWith(http.StatusOK, `[{"guid": "guid1", "type": "product1"}]`), + ghttp.RespondWith(http.StatusOK, `[{"guid": "guid1", "type": "product1"}, {"guid": "guid2", "type": "product2"}]`), ), ghttp.CombineHandlers( ghttp.VerifyRequest("GET", "/api/v0/deployed/products"), - ghttp.RespondWith(http.StatusOK, `[{"guid": "guid1", "type": "product1"}]`), + ghttp.RespondWith(http.StatusOK, `[{"guid": "guid1", "type": "product1"}, {"guid": "guid2", "type": "product2"}]`), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/api/v0/installations"), + ghttp.VerifyJSON(`{"ignore_warnings":"false","force_latest_variables":true,"deploy_products":["guid2"]}`), + ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - _, err := service.CreateInstallation(false, true, []string{"product2"}, api.ApplyErrandChanges{}) - Expect(err).To(HaveOccurred()) + output, err := service.CreateInstallation(false, true, true, []string{"product2"}, api.ApplyErrandChanges{}) + Expect(err).ToNot(HaveOccurred()) + Expect(output.ID).To(Equal(1)) }) }) @@ -199,12 +209,12 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings": "false", "deploy_products": ["guid1"], "errands": {"guid1": {"run_post_deploy": {"errand1": "default"}}}}`), + ghttp.VerifyJSON(`{"ignore_warnings": "false", "force_latest_variables": false, "deploy_products": ["guid1"], "errands": {"guid1": {"run_post_deploy": {"errand1": "default"}}}}`), ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - output, err := service.CreateInstallation(false, true, []string{"product1"}, api.ApplyErrandChanges{ + output, err := service.CreateInstallation(false, true, false, []string{"product1"}, api.ApplyErrandChanges{ Errands: map[string]api.ProductErrand{ "product1": { RunPostDeploy: map[string]interface{}{ @@ -229,12 +239,12 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings": "false", "deploy_products": ["guid2"]}`), + ghttp.VerifyJSON(`{"ignore_warnings": "false", "force_latest_variables": false, "deploy_products": ["guid2"]}`), ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - _, err := service.CreateInstallation(false, true, []string{"product2"}, api.ApplyErrandChanges{ + _, err := service.CreateInstallation(false, true, false, []string{"product2"}, api.ApplyErrandChanges{ Errands: map[string]api.ProductErrand{ "product3": { RunPostDeploy: map[string]interface{}{ @@ -261,12 +271,12 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings": "false", "deploy_products": "all", "errands": {"guid1": {"run_post_deploy": {"errand1": "default"}}}}`), + ghttp.VerifyJSON(`{"ignore_warnings": "false", "force_latest_variables": false, "deploy_products": "all", "errands": {"guid1": {"run_post_deploy": {"errand1": "default"}}}}`), ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - output, err := service.CreateInstallation(false, true, []string{}, api.ApplyErrandChanges{ + output, err := service.CreateInstallation(false, true, false, []string{}, api.ApplyErrandChanges{ Errands: map[string]api.ProductErrand{ "product1": { RunPostDeploy: map[string]interface{}{ @@ -291,12 +301,12 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings": "false", "deploy_products": ["guid2"], "errands": {"guid1": {"run_post_deploy": {"errand1": "default"}}}}`), + ghttp.VerifyJSON(`{"ignore_warnings": "false", "force_latest_variables": false, "deploy_products": ["guid2"], "errands": {"guid1": {"run_post_deploy": {"errand1": "default"}}}}`), ghttp.RespondWith(http.StatusOK, `{"install": {"id":1}}`), ), ) - _, err := service.CreateInstallation(false, true, []string{}, api.ApplyErrandChanges{ + _, err := service.CreateInstallation(false, true, false, []string{}, api.ApplyErrandChanges{ Errands: map[string]api.ProductErrand{ "product1": { RunPostDeploy: map[string]interface{}{ @@ -330,7 +340,7 @@ var _ = Describe("InstallationsService", func() { ), ) - _, err := service.CreateInstallation(false, true, nil, api.ApplyErrandChanges{}) + _, err := service.CreateInstallation(false, true, false, nil, api.ApplyErrandChanges{}) Expect(err).To(MatchError(ContainSubstring("could not make api request to installations endpoint: could not send api request to POST /api/v0/installations"))) }) }) @@ -348,7 +358,7 @@ var _ = Describe("InstallationsService", func() { ), ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/api/v0/installations"), - ghttp.VerifyJSON(`{"ignore_warnings":"false","deploy_products":["guid2"]}`), + ghttp.VerifyJSON(`{"ignore_warnings":"false","force_latest_variables":false,"deploy_products":["guid2"]}`), ghttp.RespondWith(http.StatusUnprocessableEntity, `{ "errors": ["'Some IAAS Error', type: SomeVerifier""], "deployment_errors": { @@ -365,7 +375,7 @@ var _ = Describe("InstallationsService", func() { ), ) - _, err := service.CreateInstallation(false, true, []string{"product2"}, api.ApplyErrandChanges{}) + _, err := service.CreateInstallation(false, true, false, []string{"product2"}, api.ApplyErrandChanges{}) Expect(err).To(MatchError(ContainSubstring("request failed: unexpected response"))) Expect(err).To(MatchError(ContainSubstring("Tip: In Ops Manager 2.6 or newer, you can use `om pre-deploy-check` to get a complete list of failed verifiers and om commands to disable them."))) }) @@ -380,7 +390,7 @@ var _ = Describe("InstallationsService", func() { ), ) - _, err := service.CreateInstallation(false, true, nil, api.ApplyErrandChanges{}) + _, err := service.CreateInstallation(false, true, false, nil, api.ApplyErrandChanges{}) Expect(err).To(MatchError(ContainSubstring("request failed: unexpected response"))) }) }) @@ -402,7 +412,7 @@ var _ = Describe("InstallationsService", func() { ), ) - _, err := service.CreateInstallation(false, true, nil, api.ApplyErrandChanges{}) + _, err := service.CreateInstallation(false, true, false, nil, api.ApplyErrandChanges{}) Expect(err).To(MatchError(ContainSubstring("failed to decode response: invalid character"))) }) }) diff --git a/commands/apply_changes.go b/commands/apply_changes.go index 5344b7f3..3b8a795c 100644 --- a/commands/apply_changes.go +++ b/commands/apply_changes.go @@ -20,18 +20,19 @@ type ApplyChanges struct { logWriter logWriter waitDuration time.Duration Options struct { - Config string `short:"c" long:"config" description:"path to yml file containing errand configuration (see docs/apply-changes/README.md for format)"` - IgnoreWarnings bool `short:"i" long:"ignore-warnings" description:"For convenience. Use other commands to disable particular verifiers if they are inappropriate."` - Reattach bool `long:"reattach" description:"reattach to an already running apply changes (if available)"` - RecreateVMs bool `long:"recreate-vms" description:"recreate all vms"` - SkipDeployProducts bool `short:"s" long:"skip-deploy-products" description:"skip deploying products when applying changes - just update the director"` - ProductNames []string `short:"n" long:"product-name" description:"name of the product(s) to deploy, cannot be used in conjunction with --skip-deploy-products (OM 2.2+)"` + Config string `short:"c" long:"config" description:"path to yml file containing errand configuration (see docs/apply-changes/README.md for format)"` + IgnoreWarnings bool `short:"i" long:"ignore-warnings" description:"For convenience. Use other commands to disable particular verifiers if they are inappropriate."` + Reattach bool `long:"reattach" description:"reattach to an already running apply changes (if available)"` + RecreateVMs bool `long:"recreate-vms" description:"recreate all vms"` + SkipDeployProducts bool `short:"s" long:"skip-deploy-products" description:"skip deploying products when applying changes - just update the director"` + ForceLatestVariables bool `long:"force-latest-variables" description:"force any certificates or other BOSH variables to use their latest version even when a stemcell is not being upgraded"` + ProductNames []string `short:"n" long:"product-name" description:"name of the product(s) to deploy, cannot be used in conjunction with --skip-deploy-products (OM 2.2+)"` } } //counterfeiter:generate -o ./fakes/apply_changes_service.go --fake-name ApplyChangesService . applyChangesService type applyChangesService interface { - CreateInstallation(bool, bool, []string, api.ApplyErrandChanges) (api.InstallationsServiceOutput, error) + CreateInstallation(bool, bool, bool, []string, api.ApplyErrandChanges) (api.InstallationsServiceOutput, error) GetInstallation(id int) (api.InstallationsServiceOutput, error) GetInstallationLogs(id int) (api.InstallationsServiceOutput, error) Info() (api.Info, error) @@ -158,7 +159,7 @@ func (ac ApplyChanges) Execute(args []string) error { } ac.logger.Printf("attempting to apply changes to the targeted Ops Manager") - installation, err = ac.service.CreateInstallation(ac.Options.IgnoreWarnings, !ac.Options.SkipDeployProducts, changedProducts, errands) + installation, err = ac.service.CreateInstallation(ac.Options.IgnoreWarnings, !ac.Options.SkipDeployProducts, ac.Options.ForceLatestVariables, changedProducts, errands) if err != nil { return fmt.Errorf("installation failed to trigger: %s", err) } diff --git a/commands/apply_changes_test.go b/commands/apply_changes_test.go index 5fc95ecd..27da14da 100644 --- a/commands/apply_changes_test.go +++ b/commands/apply_changes_test.go @@ -3,14 +3,15 @@ package commands_test import ( "errors" "fmt" - "github.com/onsi/gomega/gbytes" - "gopkg.in/yaml.v2" "io/ioutil" "log" "os" "regexp" "time" + "github.com/onsi/gomega/gbytes" + "gopkg.in/yaml.v2" + "github.com/pivotal-cf/om/api" "github.com/pivotal-cf/om/commands" "github.com/pivotal-cf/om/commands/fakes" @@ -60,9 +61,10 @@ var _ = Describe("ApplyChanges", func() { Expect(service.CreateInstallationCallCount()).To(Equal(1)) - ignoreWarnings, deployProducts, _, _ := service.CreateInstallationArgsForCall(0) + ignoreWarnings, deployProducts, forceLatestVariables, _, _ := service.CreateInstallationArgsForCall(0) Expect(ignoreWarnings).To(Equal(false)) Expect(deployProducts).To(Equal(true)) + Expect(forceLatestVariables).To(Equal(false)) Expect(stderr).To(gbytes.Say("attempting to apply changes to the targeted Ops Manager")) @@ -116,11 +118,25 @@ var _ = Describe("ApplyChanges", func() { err := executeCommand(command, []string{"--ignore-warnings"}) Expect(err).ToNot(HaveOccurred()) - ignoreWarnings, _, _, _ := service.CreateInstallationArgsForCall(0) + ignoreWarnings, _, _, _, _ := service.CreateInstallationArgsForCall(0) Expect(ignoreWarnings).To(Equal(true)) }) }) + When("passed the force-latest-variables flag", func() { + It("applies changes while forcing the latest variable versions to be used", func() { + service.InfoReturns(api.Info{Version: "2.3-build43"}, nil) + + command := commands.NewApplyChanges(service, pendingService, writer, logger, 1) + + err := executeCommand(command, []string{"--force-latest-variables"}) + Expect(err).ToNot(HaveOccurred()) + + _, _, forceLatestVariables, _, _ := service.CreateInstallationArgsForCall(0) + Expect(forceLatestVariables).To(Equal(true)) + }) + }) + When("passed the skip-deploy-products flag", func() { It("applies changes while not deploying products", func() { command := commands.NewApplyChanges(service, pendingService, writer, logger, 1) @@ -128,7 +144,7 @@ var _ = Describe("ApplyChanges", func() { err := executeCommand(command, []string{"--skip-deploy-products"}) Expect(err).ToNot(HaveOccurred()) - _, deployProducts, _, _ := service.CreateInstallationArgsForCall(0) + _, _, deployProducts, _, _ := service.CreateInstallationArgsForCall(0) Expect(deployProducts).To(Equal(false)) }) @@ -149,7 +165,7 @@ var _ = Describe("ApplyChanges", func() { err := executeCommand(command, []string{"--product-name", "product1", "--product-name", "product2"}) Expect(err).To(HaveOccurred()) - _, _, productNames, _ := service.CreateInstallationArgsForCall(0) + _, _, _, productNames, _ := service.CreateInstallationArgsForCall(0) Expect(productNames).To(ConsistOf("product1", "product2")) }) }) @@ -382,9 +398,10 @@ errands: Expect(service.CreateInstallationCallCount()).To(Equal(1)) - ignoreWarnings, deployProducts, _, errands := service.CreateInstallationArgsForCall(0) + ignoreWarnings, deployProducts, forceLatestVariables, _, errands := service.CreateInstallationArgsForCall(0) Expect(ignoreWarnings).To(Equal(false)) Expect(deployProducts).To(Equal(true)) + Expect(forceLatestVariables).To(Equal(false)) Expect(errands).To(Equal(api.ApplyErrandChanges{ Errands: map[string]api.ProductErrand{ "product1_name": { diff --git a/commands/fakes/apply_changes_service.go b/commands/fakes/apply_changes_service.go index 04d49831..9e0a26b4 100644 --- a/commands/fakes/apply_changes_service.go +++ b/commands/fakes/apply_changes_service.go @@ -8,13 +8,14 @@ import ( ) type ApplyChangesService struct { - CreateInstallationStub func(bool, bool, []string, api.ApplyErrandChanges) (api.InstallationsServiceOutput, error) + CreateInstallationStub func(bool, bool, bool, []string, api.ApplyErrandChanges) (api.InstallationsServiceOutput, error) createInstallationMutex sync.RWMutex createInstallationArgsForCall []struct { arg1 bool arg2 bool - arg3 []string - arg4 api.ApplyErrandChanges + arg3 bool + arg4 []string + arg5 api.ApplyErrandChanges } createInstallationReturns struct { result1 api.InstallationsServiceOutput @@ -101,24 +102,25 @@ type ApplyChangesService struct { invocationsMutex sync.RWMutex } -func (fake *ApplyChangesService) CreateInstallation(arg1 bool, arg2 bool, arg3 []string, arg4 api.ApplyErrandChanges) (api.InstallationsServiceOutput, error) { - var arg3Copy []string - if arg3 != nil { - arg3Copy = make([]string, len(arg3)) - copy(arg3Copy, arg3) +func (fake *ApplyChangesService) CreateInstallation(arg1 bool, arg2 bool, arg3 bool, arg4 []string, arg5 api.ApplyErrandChanges) (api.InstallationsServiceOutput, error) { + var arg4Copy []string + if arg4 != nil { + arg4Copy = make([]string, len(arg4)) + copy(arg4Copy, arg4) } fake.createInstallationMutex.Lock() ret, specificReturn := fake.createInstallationReturnsOnCall[len(fake.createInstallationArgsForCall)] fake.createInstallationArgsForCall = append(fake.createInstallationArgsForCall, struct { arg1 bool arg2 bool - arg3 []string - arg4 api.ApplyErrandChanges - }{arg1, arg2, arg3Copy, arg4}) - fake.recordInvocation("CreateInstallation", []interface{}{arg1, arg2, arg3Copy, arg4}) + arg3 bool + arg4 []string + arg5 api.ApplyErrandChanges + }{arg1, arg2, arg3, arg4Copy, arg5}) + fake.recordInvocation("CreateInstallation", []interface{}{arg1, arg2, arg3, arg4Copy, arg5}) fake.createInstallationMutex.Unlock() if fake.CreateInstallationStub != nil { - return fake.CreateInstallationStub(arg1, arg2, arg3, arg4) + return fake.CreateInstallationStub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1, ret.result2 @@ -133,17 +135,17 @@ func (fake *ApplyChangesService) CreateInstallationCallCount() int { return len(fake.createInstallationArgsForCall) } -func (fake *ApplyChangesService) CreateInstallationCalls(stub func(bool, bool, []string, api.ApplyErrandChanges) (api.InstallationsServiceOutput, error)) { +func (fake *ApplyChangesService) CreateInstallationCalls(stub func(bool, bool, bool, []string, api.ApplyErrandChanges) (api.InstallationsServiceOutput, error)) { fake.createInstallationMutex.Lock() defer fake.createInstallationMutex.Unlock() fake.CreateInstallationStub = stub } -func (fake *ApplyChangesService) CreateInstallationArgsForCall(i int) (bool, bool, []string, api.ApplyErrandChanges) { +func (fake *ApplyChangesService) CreateInstallationArgsForCall(i int) (bool, bool, bool, []string, api.ApplyErrandChanges) { fake.createInstallationMutex.RLock() defer fake.createInstallationMutex.RUnlock() argsForCall := fake.createInstallationArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 } func (fake *ApplyChangesService) CreateInstallationReturns(result1 api.InstallationsServiceOutput, result2 error) {