From db1abfc3a6a051a59581ce2415e17388620b6bfe Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 3 May 2019 10:17:22 +0100 Subject: [PATCH] Add experimental --upgrade flag to update-service - allows for upgrade of an individual service instance to the latest maintenance_info version on the plan [#164496184] Co-authored-by: Oleksii Fedorov Co-authored-by: George Blue Co-authored-by: Aarti Kriplani --- actor/v2action/actor.go | 3 +- actor/v2action/cloud_controller_client.go | 1 + .../fake_get_service_instance_actor.go | 123 ++++++++ .../fake_get_service_plan_actor.go | 121 ++++++++ ...service_instance_maintenance_info_actor.go | 118 ++++++++ .../composite/update_service_instance.go | 47 +++ .../composite/update_service_instance_test.go | 156 ++++++++++ actor/v2action/service_instance.go | 11 +- actor/v2action/service_instance_test.go | 46 +++ .../fake_cloud_controller_client.go | 80 ++++++ .../ccv2/internal/api_routes.go | 2 + api/cloudcontroller/ccv2/service_instance.go | 35 +++ .../ccv2/service_instance_test.go | 58 ++++ api/cloudcontroller/ccv2/service_plan.go | 15 +- api/cloudcontroller/ccv2/service_plan_test.go | 8 +- .../ccversion/minimum_version.go | 15 +- command/v6/update_service_command.go | 108 ++++++- command/v6/update_service_command_test.go | 272 ++++++++++++++++++ .../v6/v6fakes/fake_update_service_actor.go | 203 +++++++++++++ .../assets/service_broker/broker_config.json | 5 +- integration/helpers/service_broker.go | 1 + .../isolated/update_service_command_test.go | 75 +++++ 22 files changed, 1483 insertions(+), 20 deletions(-) create mode 100644 actor/v2action/composite/compositefakes/fake_get_service_instance_actor.go create mode 100644 actor/v2action/composite/compositefakes/fake_get_service_plan_actor.go create mode 100644 actor/v2action/composite/compositefakes/fake_update_service_instance_maintenance_info_actor.go create mode 100644 actor/v2action/composite/update_service_instance.go create mode 100644 actor/v2action/composite/update_service_instance_test.go create mode 100644 command/v6/update_service_command_test.go create mode 100644 command/v6/v6fakes/fake_update_service_actor.go diff --git a/actor/v2action/actor.go b/actor/v2action/actor.go index 6b167eb9cf1..f2c9f1571ce 100644 --- a/actor/v2action/actor.go +++ b/actor/v2action/actor.go @@ -1,4 +1,5 @@ -// Package v2action contains the business logic for the commands/v2 package +// Package v2action contains the business logic for the commands/v6 package +// Actors in this package should only call CC v2 API endpoints package v2action // Warnings is a list of warnings returned back from the cloud controller diff --git a/actor/v2action/cloud_controller_client.go b/actor/v2action/cloud_controller_client.go index 0df24231380..4fbc4a1eb71 100644 --- a/actor/v2action/cloud_controller_client.go +++ b/actor/v2action/cloud_controller_client.go @@ -92,6 +92,7 @@ type CloudControllerClient interface { UpdateRouteApplication(routeGUID string, appGUID string) (ccv2.Route, ccv2.Warnings, error) UpdateSecurityGroupSpace(securityGroupGUID string, spaceGUID string) (ccv2.Warnings, error) UpdateSecurityGroupStagingSpace(securityGroupGUID string, spaceGUID string) (ccv2.Warnings, error) + UpdateServiceInstanceMaintenanceInfo(serviceInstanceGUID string, maintenanceInfo ccv2.MaintenanceInfo) (ccv2.Warnings, error) UpdateServicePlan(guid string, public bool) (ccv2.Warnings, error) UpdateSpaceDeveloper(spaceGUID string, uaaID string) (ccv2.Warnings, error) UpdateSpaceDeveloperByUsername(spaceGUID string, username string) (ccv2.Warnings, error) diff --git a/actor/v2action/composite/compositefakes/fake_get_service_instance_actor.go b/actor/v2action/composite/compositefakes/fake_get_service_instance_actor.go new file mode 100644 index 00000000000..0c2085845ef --- /dev/null +++ b/actor/v2action/composite/compositefakes/fake_get_service_instance_actor.go @@ -0,0 +1,123 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package compositefakes + +import ( + "sync" + + "code.cloudfoundry.org/cli/actor/v2action" + "code.cloudfoundry.org/cli/actor/v2action/composite" +) + +type FakeGetServiceInstanceActor struct { + GetServiceInstanceByNameAndSpaceStub func(string, string) (v2action.ServiceInstance, v2action.Warnings, error) + getServiceInstanceByNameAndSpaceMutex sync.RWMutex + getServiceInstanceByNameAndSpaceArgsForCall []struct { + arg1 string + arg2 string + } + getServiceInstanceByNameAndSpaceReturns struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + } + getServiceInstanceByNameAndSpaceReturnsOnCall map[int]struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeGetServiceInstanceActor) GetServiceInstanceByNameAndSpace(arg1 string, arg2 string) (v2action.ServiceInstance, v2action.Warnings, error) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + ret, specificReturn := fake.getServiceInstanceByNameAndSpaceReturnsOnCall[len(fake.getServiceInstanceByNameAndSpaceArgsForCall)] + fake.getServiceInstanceByNameAndSpaceArgsForCall = append(fake.getServiceInstanceByNameAndSpaceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetServiceInstanceByNameAndSpace", []interface{}{arg1, arg2}) + fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + if fake.GetServiceInstanceByNameAndSpaceStub != nil { + return fake.GetServiceInstanceByNameAndSpaceStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getServiceInstanceByNameAndSpaceReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeGetServiceInstanceActor) GetServiceInstanceByNameAndSpaceCallCount() int { + fake.getServiceInstanceByNameAndSpaceMutex.RLock() + defer fake.getServiceInstanceByNameAndSpaceMutex.RUnlock() + return len(fake.getServiceInstanceByNameAndSpaceArgsForCall) +} + +func (fake *FakeGetServiceInstanceActor) GetServiceInstanceByNameAndSpaceCalls(stub func(string, string) (v2action.ServiceInstance, v2action.Warnings, error)) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + defer fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + fake.GetServiceInstanceByNameAndSpaceStub = stub +} + +func (fake *FakeGetServiceInstanceActor) GetServiceInstanceByNameAndSpaceArgsForCall(i int) (string, string) { + fake.getServiceInstanceByNameAndSpaceMutex.RLock() + defer fake.getServiceInstanceByNameAndSpaceMutex.RUnlock() + argsForCall := fake.getServiceInstanceByNameAndSpaceArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeGetServiceInstanceActor) GetServiceInstanceByNameAndSpaceReturns(result1 v2action.ServiceInstance, result2 v2action.Warnings, result3 error) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + defer fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + fake.GetServiceInstanceByNameAndSpaceStub = nil + fake.getServiceInstanceByNameAndSpaceReturns = struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeGetServiceInstanceActor) GetServiceInstanceByNameAndSpaceReturnsOnCall(i int, result1 v2action.ServiceInstance, result2 v2action.Warnings, result3 error) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + defer fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + fake.GetServiceInstanceByNameAndSpaceStub = nil + if fake.getServiceInstanceByNameAndSpaceReturnsOnCall == nil { + fake.getServiceInstanceByNameAndSpaceReturnsOnCall = make(map[int]struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + }) + } + fake.getServiceInstanceByNameAndSpaceReturnsOnCall[i] = struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeGetServiceInstanceActor) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getServiceInstanceByNameAndSpaceMutex.RLock() + defer fake.getServiceInstanceByNameAndSpaceMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeGetServiceInstanceActor) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ composite.GetServiceInstanceActor = new(FakeGetServiceInstanceActor) diff --git a/actor/v2action/composite/compositefakes/fake_get_service_plan_actor.go b/actor/v2action/composite/compositefakes/fake_get_service_plan_actor.go new file mode 100644 index 00000000000..2cb5dfb1d27 --- /dev/null +++ b/actor/v2action/composite/compositefakes/fake_get_service_plan_actor.go @@ -0,0 +1,121 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package compositefakes + +import ( + "sync" + + "code.cloudfoundry.org/cli/actor/v2action" + "code.cloudfoundry.org/cli/actor/v2action/composite" +) + +type FakeGetServicePlanActor struct { + GetServicePlanStub func(string) (v2action.ServicePlan, v2action.Warnings, error) + getServicePlanMutex sync.RWMutex + getServicePlanArgsForCall []struct { + arg1 string + } + getServicePlanReturns struct { + result1 v2action.ServicePlan + result2 v2action.Warnings + result3 error + } + getServicePlanReturnsOnCall map[int]struct { + result1 v2action.ServicePlan + result2 v2action.Warnings + result3 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeGetServicePlanActor) GetServicePlan(arg1 string) (v2action.ServicePlan, v2action.Warnings, error) { + fake.getServicePlanMutex.Lock() + ret, specificReturn := fake.getServicePlanReturnsOnCall[len(fake.getServicePlanArgsForCall)] + fake.getServicePlanArgsForCall = append(fake.getServicePlanArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetServicePlan", []interface{}{arg1}) + fake.getServicePlanMutex.Unlock() + if fake.GetServicePlanStub != nil { + return fake.GetServicePlanStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getServicePlanReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeGetServicePlanActor) GetServicePlanCallCount() int { + fake.getServicePlanMutex.RLock() + defer fake.getServicePlanMutex.RUnlock() + return len(fake.getServicePlanArgsForCall) +} + +func (fake *FakeGetServicePlanActor) GetServicePlanCalls(stub func(string) (v2action.ServicePlan, v2action.Warnings, error)) { + fake.getServicePlanMutex.Lock() + defer fake.getServicePlanMutex.Unlock() + fake.GetServicePlanStub = stub +} + +func (fake *FakeGetServicePlanActor) GetServicePlanArgsForCall(i int) string { + fake.getServicePlanMutex.RLock() + defer fake.getServicePlanMutex.RUnlock() + argsForCall := fake.getServicePlanArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeGetServicePlanActor) GetServicePlanReturns(result1 v2action.ServicePlan, result2 v2action.Warnings, result3 error) { + fake.getServicePlanMutex.Lock() + defer fake.getServicePlanMutex.Unlock() + fake.GetServicePlanStub = nil + fake.getServicePlanReturns = struct { + result1 v2action.ServicePlan + result2 v2action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeGetServicePlanActor) GetServicePlanReturnsOnCall(i int, result1 v2action.ServicePlan, result2 v2action.Warnings, result3 error) { + fake.getServicePlanMutex.Lock() + defer fake.getServicePlanMutex.Unlock() + fake.GetServicePlanStub = nil + if fake.getServicePlanReturnsOnCall == nil { + fake.getServicePlanReturnsOnCall = make(map[int]struct { + result1 v2action.ServicePlan + result2 v2action.Warnings + result3 error + }) + } + fake.getServicePlanReturnsOnCall[i] = struct { + result1 v2action.ServicePlan + result2 v2action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeGetServicePlanActor) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getServicePlanMutex.RLock() + defer fake.getServicePlanMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeGetServicePlanActor) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ composite.GetServicePlanActor = new(FakeGetServicePlanActor) diff --git a/actor/v2action/composite/compositefakes/fake_update_service_instance_maintenance_info_actor.go b/actor/v2action/composite/compositefakes/fake_update_service_instance_maintenance_info_actor.go new file mode 100644 index 00000000000..bc73a201b5c --- /dev/null +++ b/actor/v2action/composite/compositefakes/fake_update_service_instance_maintenance_info_actor.go @@ -0,0 +1,118 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package compositefakes + +import ( + "sync" + + "code.cloudfoundry.org/cli/actor/v2action" + "code.cloudfoundry.org/cli/actor/v2action/composite" +) + +type FakeUpdateServiceInstanceMaintenanceInfoActor struct { + UpdateServiceInstanceMaintenanceInfoStub func(string, v2action.MaintenanceInfo) (v2action.Warnings, error) + updateServiceInstanceMaintenanceInfoMutex sync.RWMutex + updateServiceInstanceMaintenanceInfoArgsForCall []struct { + arg1 string + arg2 v2action.MaintenanceInfo + } + updateServiceInstanceMaintenanceInfoReturns struct { + result1 v2action.Warnings + result2 error + } + updateServiceInstanceMaintenanceInfoReturnsOnCall map[int]struct { + result1 v2action.Warnings + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) UpdateServiceInstanceMaintenanceInfo(arg1 string, arg2 v2action.MaintenanceInfo) (v2action.Warnings, error) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + ret, specificReturn := fake.updateServiceInstanceMaintenanceInfoReturnsOnCall[len(fake.updateServiceInstanceMaintenanceInfoArgsForCall)] + fake.updateServiceInstanceMaintenanceInfoArgsForCall = append(fake.updateServiceInstanceMaintenanceInfoArgsForCall, struct { + arg1 string + arg2 v2action.MaintenanceInfo + }{arg1, arg2}) + fake.recordInvocation("UpdateServiceInstanceMaintenanceInfo", []interface{}{arg1, arg2}) + fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + if fake.UpdateServiceInstanceMaintenanceInfoStub != nil { + return fake.UpdateServiceInstanceMaintenanceInfoStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.updateServiceInstanceMaintenanceInfoReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) UpdateServiceInstanceMaintenanceInfoCallCount() int { + fake.updateServiceInstanceMaintenanceInfoMutex.RLock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.RUnlock() + return len(fake.updateServiceInstanceMaintenanceInfoArgsForCall) +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) UpdateServiceInstanceMaintenanceInfoCalls(stub func(string, v2action.MaintenanceInfo) (v2action.Warnings, error)) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + fake.UpdateServiceInstanceMaintenanceInfoStub = stub +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) UpdateServiceInstanceMaintenanceInfoArgsForCall(i int) (string, v2action.MaintenanceInfo) { + fake.updateServiceInstanceMaintenanceInfoMutex.RLock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.RUnlock() + argsForCall := fake.updateServiceInstanceMaintenanceInfoArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) UpdateServiceInstanceMaintenanceInfoReturns(result1 v2action.Warnings, result2 error) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + fake.UpdateServiceInstanceMaintenanceInfoStub = nil + fake.updateServiceInstanceMaintenanceInfoReturns = struct { + result1 v2action.Warnings + result2 error + }{result1, result2} +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) UpdateServiceInstanceMaintenanceInfoReturnsOnCall(i int, result1 v2action.Warnings, result2 error) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + fake.UpdateServiceInstanceMaintenanceInfoStub = nil + if fake.updateServiceInstanceMaintenanceInfoReturnsOnCall == nil { + fake.updateServiceInstanceMaintenanceInfoReturnsOnCall = make(map[int]struct { + result1 v2action.Warnings + result2 error + }) + } + fake.updateServiceInstanceMaintenanceInfoReturnsOnCall[i] = struct { + result1 v2action.Warnings + result2 error + }{result1, result2} +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.updateServiceInstanceMaintenanceInfoMutex.RLock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeUpdateServiceInstanceMaintenanceInfoActor) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ composite.UpdateServiceInstanceMaintenanceInfoActor = new(FakeUpdateServiceInstanceMaintenanceInfoActor) diff --git a/actor/v2action/composite/update_service_instance.go b/actor/v2action/composite/update_service_instance.go new file mode 100644 index 00000000000..0b541500111 --- /dev/null +++ b/actor/v2action/composite/update_service_instance.go @@ -0,0 +1,47 @@ +package composite + +import ( + "code.cloudfoundry.org/cli/actor/v2action" +) + +//go:generate counterfeiter . GetServiceInstanceActor + +type GetServiceInstanceActor interface { + GetServiceInstanceByNameAndSpace(name string, spaceGUID string) (v2action.ServiceInstance, v2action.Warnings, error) +} + +//go:generate counterfeiter . GetServicePlanActor + +type GetServicePlanActor interface { + GetServicePlan(servicePlanGUID string) (v2action.ServicePlan, v2action.Warnings, error) +} + +//go:generate counterfeiter . UpdateServiceInstanceMaintenanceInfoActor + +type UpdateServiceInstanceMaintenanceInfoActor interface { + UpdateServiceInstanceMaintenanceInfo(serviceInsrtanceGUID string, maintenanceInfo v2action.MaintenanceInfo) (v2action.Warnings, error) +} + +type UpdateServiceInstanceCompositeActor struct { + GetServiceInstanceActor GetServiceInstanceActor + GetServicePlanActor GetServicePlanActor + UpdateServiceInstanceMaintenanceInfoActor UpdateServiceInstanceMaintenanceInfoActor +} + +// UpgradeServiceInstance requests update on the service instance with the `maintenance_info` available on the plan +func (c UpdateServiceInstanceCompositeActor) UpgradeServiceInstance(serviceInstanceGUID, servicePlanGUID string) (v2action.Warnings, error) { + servicePlan, warnings, err := c.GetServicePlanActor.GetServicePlan(servicePlanGUID) + if err != nil { + return warnings, err + } + updateWarnings, err := c.UpdateServiceInstanceMaintenanceInfoActor.UpdateServiceInstanceMaintenanceInfo( + serviceInstanceGUID, + v2action.MaintenanceInfo(servicePlan.MaintenanceInfo), + ) + return append(warnings, updateWarnings...), err +} + +// GetServiceInstanceByNameAndSpace gets the service instance by name and space guid provided +func (c UpdateServiceInstanceCompositeActor) GetServiceInstanceByNameAndSpace(name string, spaceGUID string) (v2action.ServiceInstance, v2action.Warnings, error) { + return c.GetServiceInstanceActor.GetServiceInstanceByNameAndSpace(name, spaceGUID) +} diff --git a/actor/v2action/composite/update_service_instance_test.go b/actor/v2action/composite/update_service_instance_test.go new file mode 100644 index 00000000000..b8f2eba20f0 --- /dev/null +++ b/actor/v2action/composite/update_service_instance_test.go @@ -0,0 +1,156 @@ +package composite_test + +import ( + "errors" + + "code.cloudfoundry.org/cli/actor/v2action" + . "code.cloudfoundry.org/cli/actor/v2action/composite" + "code.cloudfoundry.org/cli/actor/v2action/composite/compositefakes" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("UpdateServiceInstanceCompositeActor", func() { + var ( + composite *UpdateServiceInstanceCompositeActor + fakeGetServiceInstanceActor *compositefakes.FakeGetServiceInstanceActor + fakeGetServicePlanActor *compositefakes.FakeGetServicePlanActor + fakeUpdateServiceInstanceMaintenanceInfoActor *compositefakes.FakeUpdateServiceInstanceMaintenanceInfoActor + err error + warnings v2action.Warnings + ) + + BeforeEach(func() { + fakeGetServiceInstanceActor = new(compositefakes.FakeGetServiceInstanceActor) + fakeGetServicePlanActor = new(compositefakes.FakeGetServicePlanActor) + fakeUpdateServiceInstanceMaintenanceInfoActor = new(compositefakes.FakeUpdateServiceInstanceMaintenanceInfoActor) + composite = &UpdateServiceInstanceCompositeActor{ + GetServiceInstanceActor: fakeGetServiceInstanceActor, + GetServicePlanActor: fakeGetServicePlanActor, + UpdateServiceInstanceMaintenanceInfoActor: fakeUpdateServiceInstanceMaintenanceInfoActor, + } + }) + + Describe("UpgradeServiceInstance", func() { + const ( + serviceInstanceGUID = "service-instance-guid" + servicePlanGUID = "service-plan-guid" + ) + + JustBeforeEach(func() { + warnings, err = composite.UpgradeServiceInstance(serviceInstanceGUID, servicePlanGUID) + }) + + When("the plan exists", func() { + var maintenanceInfo v2action.MaintenanceInfo + + BeforeEach(func() { + maintenanceInfo = v2action.MaintenanceInfo{ + Version: "1.2.3-abc", + } + servicePlan := v2action.ServicePlan{ + MaintenanceInfo: ccv2.MaintenanceInfo(maintenanceInfo), + } + fakeGetServicePlanActor.GetServicePlanReturns(servicePlan, v2action.Warnings{"plan-lookup-warning"}, nil) + fakeUpdateServiceInstanceMaintenanceInfoActor.UpdateServiceInstanceMaintenanceInfoReturns(v2action.Warnings{"update-service-instance-warning"}, nil) + }) + + It("updates the service instance with the latest maintenanceInfo on the plan", func() { + Expect(err).To(BeNil()) + Expect(fakeUpdateServiceInstanceMaintenanceInfoActor.UpdateServiceInstanceMaintenanceInfoCallCount()).To(Equal(1)) + guid, minfo := fakeUpdateServiceInstanceMaintenanceInfoActor.UpdateServiceInstanceMaintenanceInfoArgsForCall(0) + Expect(guid).To(Equal(serviceInstanceGUID)) + Expect(minfo).To(Equal(maintenanceInfo)) + + Expect(fakeGetServicePlanActor.GetServicePlanCallCount()).To(Equal(1)) + planGUID := fakeGetServicePlanActor.GetServicePlanArgsForCall(0) + Expect(planGUID).To(Equal(servicePlanGUID)) + }) + + It("returns all warnings", func() { + Expect(warnings).To(ConsistOf("plan-lookup-warning", "update-service-instance-warning")) + }) + + When("updating the service instance fails", func() { + BeforeEach(func() { + fakeUpdateServiceInstanceMaintenanceInfoActor.UpdateServiceInstanceMaintenanceInfoReturns( + v2action.Warnings{"update-service-instance-warning"}, + errors.New("something really bad happened"), + ) + }) + + It("returns the error and warnings", func() { + Expect(err).To(MatchError("something really bad happened")) + Expect(warnings).To(ConsistOf("plan-lookup-warning", "update-service-instance-warning")) + }) + }) + }) + + When("fetching the plan fails", func() { + BeforeEach(func() { + fakeGetServicePlanActor.GetServicePlanReturns( + v2action.ServicePlan{}, + v2action.Warnings{"plan-lookup-warning"}, + errors.New("something really bad happened"), + ) + }) + + It("returns an error and warnings", func() { + Expect(err).To(MatchError("something really bad happened")) + Expect(warnings).To(ConsistOf("plan-lookup-warning")) + }) + }) + }) + + Describe("GetServiceInstanceByNameAndSpace", func() { + var serviceInstance v2action.ServiceInstance + + JustBeforeEach(func() { + serviceInstance, warnings, err = composite.GetServiceInstanceByNameAndSpace("some-service-instance", "some-space-guid") + }) + + When("the service instance exists", func() { + BeforeEach(func() { + fakeGetServiceInstanceActor.GetServiceInstanceByNameAndSpaceReturns( + v2action.ServiceInstance{ + GUID: "some-service-instance-guid", + Name: "some-service-instance", + }, + v2action.Warnings{"foo"}, + nil, + ) + }) + + It("returns the service instance and warnings", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(serviceInstance).To(Equal(v2action.ServiceInstance{ + GUID: "some-service-instance-guid", + Name: "some-service-instance", + })) + Expect(warnings).To(ConsistOf("foo")) + + Expect(fakeGetServiceInstanceActor.GetServiceInstanceByNameAndSpaceCallCount()).To(Equal(1)) + + serviceInstanceGUID, spaceGUID := fakeGetServiceInstanceActor.GetServiceInstanceByNameAndSpaceArgsForCall(0) + Expect(serviceInstanceGUID).To(Equal("some-service-instance")) + Expect(spaceGUID).To(Equal("some-space-guid")) + }) + }) + + When("there is an error getting the service instance", func() { + BeforeEach(func() { + fakeGetServiceInstanceActor.GetServiceInstanceByNameAndSpaceReturns( + v2action.ServiceInstance{}, + v2action.Warnings{"foo"}, + errors.New("something really bad happened"), + ) + }) + + It("returns an error and warnings", func() { + Expect(err).To(MatchError("something really bad happened")) + Expect(warnings).To(ConsistOf("foo")) + }) + }) + }) +}) diff --git a/actor/v2action/service_instance.go b/actor/v2action/service_instance.go index 86ca619902e..10da7c7097a 100644 --- a/actor/v2action/service_instance.go +++ b/actor/v2action/service_instance.go @@ -9,6 +9,7 @@ import ( // ServiceInstance represents an instance of a service. type ServiceInstance ccv2.ServiceInstance +type MaintenanceInfo ccv2.MaintenanceInfo // CreateServiceInstance creates a new service instance with the provided attributes. func (actor Actor) CreateServiceInstance(spaceGUID, serviceName, servicePlanName, serviceInstanceName, brokerName string, params map[string]interface{}, tags []string) (ServiceInstance, Warnings, error) { @@ -101,12 +102,18 @@ func (actor Actor) GetServiceInstancesBySpace(spaceGUID string) ([]ServiceInstan return serviceInstances, Warnings(warnings), nil } -// IsManaged returns true if the service instance is managed, othersise false. +// UpdateServiceInstanceMaintenanceInfo requests that the service instance be updated to the specified `maintenance_info` +func (actor Actor) UpdateServiceInstanceMaintenanceInfo(guid string, maintenanceInfo MaintenanceInfo) (Warnings, error) { + warnings, err := actor.CloudControllerClient.UpdateServiceInstanceMaintenanceInfo(guid, ccv2.MaintenanceInfo(maintenanceInfo)) + return Warnings(warnings), err +} + +// IsManaged returns true if the service instance is managed, otherwise false. func (instance ServiceInstance) IsManaged() bool { return ccv2.ServiceInstance(instance).Managed() } -// IsUserProvided returns true if the service instance is user provided, othersise false. +// IsUserProvided returns true if the service instance is user provided, otherwise false. func (instance ServiceInstance) IsUserProvided() bool { return ccv2.ServiceInstance(instance).UserProvided() } diff --git a/actor/v2action/service_instance_test.go b/actor/v2action/service_instance_test.go index fa085da44bb..e3d3858da23 100644 --- a/actor/v2action/service_instance_test.go +++ b/actor/v2action/service_instance_test.go @@ -772,4 +772,50 @@ var _ = Describe("Service Instance Actions", func() { }) }) }) + + Describe("UpdateServiceInstanceMaintenanceInfo", func() { + const serviceInstanceGUID = "service-instance-guid" + var maintenanceInfo MaintenanceInfo + + BeforeEach(func() { + maintenanceInfo = MaintenanceInfo{ + Version: "1.2.3", + } + }) + + When("the update is successful", func() { + BeforeEach(func() { + fakeCloudControllerClient.UpdateServiceInstanceMaintenanceInfoReturns( + ccv2.Warnings{"warning-1", "warning-2"}, + nil, + ) + }) + + It("returns all the warnings", func() { + warnings, err := actor.UpdateServiceInstanceMaintenanceInfo(serviceInstanceGUID, maintenanceInfo) + Expect(err).NotTo(HaveOccurred()) + + Expect(warnings).To(ConsistOf("warning-1", "warning-2")) + Expect(fakeCloudControllerClient.UpdateServiceInstanceMaintenanceInfoCallCount()).To(Equal(1)) + guid, minfo := fakeCloudControllerClient.UpdateServiceInstanceMaintenanceInfoArgsForCall(0) + Expect(guid).To(Equal(serviceInstanceGUID)) + Expect(minfo).To(Equal(ccv2.MaintenanceInfo(maintenanceInfo))) + }) + }) + + When("the update fails", func() { + BeforeEach(func() { + fakeCloudControllerClient.UpdateServiceInstanceMaintenanceInfoReturns( + ccv2.Warnings{"warning-1", "warning-2"}, + errors.New("update failed horribly!!!"), + ) + }) + + It("returns the error and all the warnings", func() { + warnings, err := actor.UpdateServiceInstanceMaintenanceInfo(serviceInstanceGUID, maintenanceInfo) + Expect(err).To(MatchError("update failed horribly!!!")) + Expect(warnings).To(ConsistOf("warning-1", "warning-2")) + }) + }) + }) }) diff --git a/actor/v2action/v2actionfakes/fake_cloud_controller_client.go b/actor/v2action/v2actionfakes/fake_cloud_controller_client.go index 172df640971..8ef87392cb4 100644 --- a/actor/v2action/v2actionfakes/fake_cloud_controller_client.go +++ b/actor/v2action/v2actionfakes/fake_cloud_controller_client.go @@ -1317,6 +1317,20 @@ type FakeCloudControllerClient struct { result1 ccv2.Warnings result2 error } + UpdateServiceInstanceMaintenanceInfoStub func(string, ccv2.MaintenanceInfo) (ccv2.Warnings, error) + updateServiceInstanceMaintenanceInfoMutex sync.RWMutex + updateServiceInstanceMaintenanceInfoArgsForCall []struct { + arg1 string + arg2 ccv2.MaintenanceInfo + } + updateServiceInstanceMaintenanceInfoReturns struct { + result1 ccv2.Warnings + result2 error + } + updateServiceInstanceMaintenanceInfoReturnsOnCall map[int]struct { + result1 ccv2.Warnings + result2 error + } UpdateServicePlanStub func(string, bool) (ccv2.Warnings, error) updateServicePlanMutex sync.RWMutex updateServicePlanArgsForCall []struct { @@ -7211,6 +7225,70 @@ func (fake *FakeCloudControllerClient) UpdateSecurityGroupStagingSpaceReturnsOnC }{result1, result2} } +func (fake *FakeCloudControllerClient) UpdateServiceInstanceMaintenanceInfo(arg1 string, arg2 ccv2.MaintenanceInfo) (ccv2.Warnings, error) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + ret, specificReturn := fake.updateServiceInstanceMaintenanceInfoReturnsOnCall[len(fake.updateServiceInstanceMaintenanceInfoArgsForCall)] + fake.updateServiceInstanceMaintenanceInfoArgsForCall = append(fake.updateServiceInstanceMaintenanceInfoArgsForCall, struct { + arg1 string + arg2 ccv2.MaintenanceInfo + }{arg1, arg2}) + fake.recordInvocation("UpdateServiceInstanceMaintenanceInfo", []interface{}{arg1, arg2}) + fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + if fake.UpdateServiceInstanceMaintenanceInfoStub != nil { + return fake.UpdateServiceInstanceMaintenanceInfoStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.updateServiceInstanceMaintenanceInfoReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCloudControllerClient) UpdateServiceInstanceMaintenanceInfoCallCount() int { + fake.updateServiceInstanceMaintenanceInfoMutex.RLock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.RUnlock() + return len(fake.updateServiceInstanceMaintenanceInfoArgsForCall) +} + +func (fake *FakeCloudControllerClient) UpdateServiceInstanceMaintenanceInfoCalls(stub func(string, ccv2.MaintenanceInfo) (ccv2.Warnings, error)) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + fake.UpdateServiceInstanceMaintenanceInfoStub = stub +} + +func (fake *FakeCloudControllerClient) UpdateServiceInstanceMaintenanceInfoArgsForCall(i int) (string, ccv2.MaintenanceInfo) { + fake.updateServiceInstanceMaintenanceInfoMutex.RLock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.RUnlock() + argsForCall := fake.updateServiceInstanceMaintenanceInfoArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeCloudControllerClient) UpdateServiceInstanceMaintenanceInfoReturns(result1 ccv2.Warnings, result2 error) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + fake.UpdateServiceInstanceMaintenanceInfoStub = nil + fake.updateServiceInstanceMaintenanceInfoReturns = struct { + result1 ccv2.Warnings + result2 error + }{result1, result2} +} + +func (fake *FakeCloudControllerClient) UpdateServiceInstanceMaintenanceInfoReturnsOnCall(i int, result1 ccv2.Warnings, result2 error) { + fake.updateServiceInstanceMaintenanceInfoMutex.Lock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.Unlock() + fake.UpdateServiceInstanceMaintenanceInfoStub = nil + if fake.updateServiceInstanceMaintenanceInfoReturnsOnCall == nil { + fake.updateServiceInstanceMaintenanceInfoReturnsOnCall = make(map[int]struct { + result1 ccv2.Warnings + result2 error + }) + } + fake.updateServiceInstanceMaintenanceInfoReturnsOnCall[i] = struct { + result1 ccv2.Warnings + result2 error + }{result1, result2} +} + func (fake *FakeCloudControllerClient) UpdateServicePlan(arg1 string, arg2 bool) (ccv2.Warnings, error) { fake.updateServicePlanMutex.Lock() ret, specificReturn := fake.updateServicePlanReturnsOnCall[len(fake.updateServicePlanArgsForCall)] @@ -7920,6 +7998,8 @@ func (fake *FakeCloudControllerClient) Invocations() map[string][][]interface{} defer fake.updateSecurityGroupSpaceMutex.RUnlock() fake.updateSecurityGroupStagingSpaceMutex.RLock() defer fake.updateSecurityGroupStagingSpaceMutex.RUnlock() + fake.updateServiceInstanceMaintenanceInfoMutex.RLock() + defer fake.updateServiceInstanceMaintenanceInfoMutex.RUnlock() fake.updateServicePlanMutex.RLock() defer fake.updateServicePlanMutex.RUnlock() fake.updateSpaceDeveloperMutex.RLock() diff --git a/api/cloudcontroller/ccv2/internal/api_routes.go b/api/cloudcontroller/ccv2/internal/api_routes.go index 04d27107b72..93a0019da93 100644 --- a/api/cloudcontroller/ccv2/internal/api_routes.go +++ b/api/cloudcontroller/ccv2/internal/api_routes.go @@ -106,6 +106,7 @@ const ( PutOrganizationUserByUsernameRequest = "PutOrganizationUserByUsername" PutResourceMatchRequest = "PutResourceMatch" PutRouteAppRequest = "PutRouteApp" + PutServiceInstanceRequest = "PutServiceInstance" PutServicePlanRequest = "PutServicePlan" PutSpaceQuotaRequest = "PutSpaceQuotaRequest" PutSpaceDeveloperRequest = "PutSpaceDeveloper" @@ -179,6 +180,7 @@ var APIRoutes = rata.Routes{ {Path: "/v2/service_instances", Method: http.MethodGet, Name: GetServiceInstancesRequest}, {Path: "/v2/service_instances", Method: http.MethodPost, Name: PostServiceInstancesRequest}, {Path: "/v2/service_instances/:service_instance_guid", Method: http.MethodGet, Name: GetServiceInstanceRequest}, + {Path: "/v2/service_instances/:service_instance_guid", Method: http.MethodPut, Name: PutServiceInstanceRequest}, {Path: "/v2/service_instances/:service_instance_guid/service_bindings", Method: http.MethodGet, Name: GetServiceInstanceServiceBindingsRequest}, {Path: "/v2/service_instances/:service_instance_guid/shared_from", Method: http.MethodGet, Name: GetServiceInstanceSharedFromRequest}, {Path: "/v2/service_instances/:service_instance_guid/shared_to", Method: http.MethodGet, Name: GetServiceInstanceSharedToRequest}, diff --git a/api/cloudcontroller/ccv2/service_instance.go b/api/cloudcontroller/ccv2/service_instance.go index 032576de152..06af706540f 100644 --- a/api/cloudcontroller/ccv2/service_instance.go +++ b/api/cloudcontroller/ccv2/service_instance.go @@ -199,6 +199,7 @@ func (client *Client) GetSpaceServiceInstances(spaceGUID string, includeUserProv URIParams: map[string]string{"guid": spaceGUID}, Query: query, }) + if err != nil { return nil, nil, err } @@ -245,3 +246,37 @@ func (client *Client) GetUserProvidedServiceInstances(filters ...Filter) ([]Serv return fullInstancesList, warnings, err } + +type MaintenanceInfo struct { + Version string `json:"version"` +} + +type updateServiceInstanceRequestBody struct { + MaintenanceInfo MaintenanceInfo `json:"maintenance_info"` +} + +func (client *Client) UpdateServiceInstanceMaintenanceInfo(serviceInstanceGUID string, maintenanceInfo MaintenanceInfo) (Warnings, error) { + requestBody := updateServiceInstanceRequestBody{ + MaintenanceInfo: maintenanceInfo, + } + + bodyBytes, err := json.Marshal(requestBody) + if err != nil { + return nil, err + } + + request, err := client.newHTTPRequest(requestOptions{ + RequestName: internal.PutServiceInstanceRequest, + URIParams: Params{"service_instance_guid": serviceInstanceGUID}, + Body: bytes.NewReader(bodyBytes), + Query: url.Values{"accepts_incomplete": {"true"}}, + }) + if err != nil { + return nil, err + } + + response := cloudcontroller.Response{} + + err = client.connection.Make(request, &response) + return response.Warnings, err +} diff --git a/api/cloudcontroller/ccv2/service_instance_test.go b/api/cloudcontroller/ccv2/service_instance_test.go index 6debdd9b4df..529874af198 100644 --- a/api/cloudcontroller/ccv2/service_instance_test.go +++ b/api/cloudcontroller/ccv2/service_instance_test.go @@ -1,9 +1,11 @@ package ccv2_test import ( + "fmt" "net/http" "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2" . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2" "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/constant" . "github.com/onsi/ginkgo" @@ -700,4 +702,60 @@ var _ = Describe("Service Instance", func() { }) }) }) + + Describe("UpdateServiceInstanceMaintenanceInfo", func() { + const instanceGUID = "fake-guid" + + When("updating succeeds", func() { + BeforeEach(func() { + requestBody := map[string]interface{}{ + "maintenance_info": map[string]interface{}{ + "version": "2.0.0", + }, + } + server.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodPut, fmt.Sprintf("/v2/service_instances/%s", instanceGUID), "accepts_incomplete=true"), + VerifyJSONRepresenting(requestBody), + RespondWith(http.StatusOK, "", http.Header{"X-Cf-Warnings": {"warning-1,warning-2"}}), + ), + ) + }) + + It("sends a request and returns all warnings from the response", func() { + priorRequests := len(server.ReceivedRequests()) + warnings, err := client.UpdateServiceInstanceMaintenanceInfo(instanceGUID, ccv2.MaintenanceInfo{Version: "2.0.0"}) + Expect(server.ReceivedRequests()).To(HaveLen(priorRequests + 1)) + + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) + }) + }) + + When("the endpoint returns an error", func() { + BeforeEach(func() { + response := `{ + "code": 10003, + "description": "You are not authorized to perform the requested action" + }` + + server.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodPut, fmt.Sprintf("/v2/service_instances/%s", instanceGUID), "accepts_incomplete=true"), + RespondWith(http.StatusForbidden, response, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), + )) + }) + + It("returns all warnings and propagates the error", func() { + priorRequests := len(server.ReceivedRequests()) + warnings, err := client.UpdateServiceInstanceMaintenanceInfo(instanceGUID, ccv2.MaintenanceInfo{Version: "2.0.0"}) + Expect(server.ReceivedRequests()).To(HaveLen(priorRequests + 1)) + + Expect(err).To(MatchError(ccerror.ForbiddenError{ + Message: "You are not authorized to perform the requested action", + })) + Expect(warnings).To(ConsistOf("warning-1", "warning-2")) + }) + }) + }) }) diff --git a/api/cloudcontroller/ccv2/service_plan.go b/api/cloudcontroller/ccv2/service_plan.go index e00bfd6a947..57e1f06c862 100644 --- a/api/cloudcontroller/ccv2/service_plan.go +++ b/api/cloudcontroller/ccv2/service_plan.go @@ -30,6 +30,9 @@ type ServicePlan struct { // Free is true if plan is free Free bool + + // Information about maintenance available + MaintenanceInfo MaintenanceInfo } // UnmarshalJSON helps unmarshal a Cloud Controller Service Plan response. @@ -37,11 +40,12 @@ func (servicePlan *ServicePlan) UnmarshalJSON(data []byte) error { var ccServicePlan struct { Metadata internal.Metadata Entity struct { - Name string `json:"name"` - ServiceGUID string `json:"service_guid"` - Public bool `json:"public"` - Description string `json:"description"` - Free bool `json:"free"` + Name string `json:"name"` + ServiceGUID string `json:"service_guid"` + Public bool `json:"public"` + Description string `json:"description"` + Free bool `json:"free"` + MaintenanceInfo MaintenanceInfo `json:"maintenance_info"` } } err := cloudcontroller.DecodeJSON(data, &ccServicePlan) @@ -55,6 +59,7 @@ func (servicePlan *ServicePlan) UnmarshalJSON(data []byte) error { servicePlan.Public = ccServicePlan.Entity.Public servicePlan.Description = ccServicePlan.Entity.Description servicePlan.Free = ccServicePlan.Entity.Free + servicePlan.MaintenanceInfo = ccServicePlan.Entity.MaintenanceInfo return nil } diff --git a/api/cloudcontroller/ccv2/service_plan_test.go b/api/cloudcontroller/ccv2/service_plan_test.go index dcd6f371f1d..a06224ee2f1 100644 --- a/api/cloudcontroller/ccv2/service_plan_test.go +++ b/api/cloudcontroller/ccv2/service_plan_test.go @@ -30,7 +30,10 @@ var _ = Describe("Service Plan", func() { "public": true, "service_guid": "some-service-guid", "description": "some-description", - "free": true + "free": true, + "maintenance_info": { + "version": "1.2.3" + } } }` @@ -53,6 +56,9 @@ var _ = Describe("Service Plan", func() { ServiceGUID: "some-service-guid", Description: "some-description", Free: true, + MaintenanceInfo: MaintenanceInfo{ + Version: "1.2.3", + }, })) Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) }) diff --git a/api/cloudcontroller/ccversion/minimum_version.go b/api/cloudcontroller/ccversion/minimum_version.go index bd4a8afb196..5854ddcfb3f 100644 --- a/api/cloudcontroller/ccversion/minimum_version.go +++ b/api/cloudcontroller/ccversion/minimum_version.go @@ -4,13 +4,14 @@ const ( MinSupportedV2ClientVersion = "2.100.0" MinSupportedV3ClientVersion = "3.35.0" - MinVersionAsyncBindingsV2 = "2.120.0" - MinVersionBuildpackStackAssociationV2 = "2.112.0" - MinVersionSymlinkedFilesV2 = "2.107.0" - MinVersionUserProvidedServiceTagsV2 = "2.104.0" - MinVersionInternalDomainV2 = "2.115.0" - MinVersionMultiServiceRegistrationV2 = "2.125.0" - MinVersionUpdateServiceNameWhenPlanNotVisibleV2 = "2.131.0" + MinVersionAsyncBindingsV2 = "2.120.0" + MinVersionBuildpackStackAssociationV2 = "2.112.0" + MinVersionSymlinkedFilesV2 = "2.107.0" + MinVersionUserProvidedServiceTagsV2 = "2.104.0" + MinVersionInternalDomainV2 = "2.115.0" + MinVersionMultiServiceRegistrationV2 = "2.125.0" + MinVersionUpdateServiceNameWhenPlanNotVisibleV2 = "2.131.0" + MinVersionUpdateServiceInstanceMaintenanceInfoV2 = "2.135.0" MinVersionShareServiceV3 = "3.36.0" MinVersionZeroDowntimePushV3 = "3.57.0" diff --git a/command/v6/update_service_command.go b/command/v6/update_service_command.go index 7fb34bfe807..a706b59e867 100644 --- a/command/v6/update_service_command.go +++ b/command/v6/update_service_command.go @@ -1,11 +1,24 @@ package v6 import ( + "code.cloudfoundry.org/cli/actor/sharedaction" + "code.cloudfoundry.org/cli/actor/v2action" + "code.cloudfoundry.org/cli/actor/v2action/composite" "code.cloudfoundry.org/cli/command" "code.cloudfoundry.org/cli/command/flag" "code.cloudfoundry.org/cli/command/translatableerror" + "code.cloudfoundry.org/cli/command/v6/shared" ) +//go:generate counterfeiter . UpdateServiceActor + +type UpdateServiceActor interface { + GetServiceInstanceByNameAndSpace(name string, spaceGUID string) (v2action.ServiceInstance, v2action.Warnings, error) + UpgradeServiceInstance(serviceInstanceGUID, servicePlanGUID string) (v2action.Warnings, error) +} + +type textData map[string]interface{} + type UpdateServiceCommand struct { RequiredArgs flag.ServiceInstance `positional-args:"yes"` ParametersAsJSON flag.Path `short:"c" description:"Valid JSON object containing service-specific configuration parameters, provided either in-line or in a file. For a list of supported configuration parameters, see documentation for the particular service offering."` @@ -13,12 +26,101 @@ type UpdateServiceCommand struct { Tags string `short:"t" description:"User provided tags"` usage interface{} `usage:"CF_NAME update-service SERVICE_INSTANCE [-p NEW_PLAN] [-c PARAMETERS_AS_JSON] [-t TAGS]\n\n Optionally provide service-specific configuration parameters in a valid JSON object in-line.\n CF_NAME update-service -c '{\"name\":\"value\",\"name\":\"value\"}'\n\n Optionally provide a file containing service-specific configuration parameters in a valid JSON object. \n The path to the parameters file can be an absolute or relative path to a file.\n CF_NAME update-service -c PATH_TO_FILE\n\n Example of valid JSON object:\n {\n \"cluster_nodes\": {\n \"count\": 5,\n \"memory_mb\": 1024\n }\n }\n\n Optionally provide a list of comma-delimited tags that will be written to the VCAP_SERVICES environment variable for any bound applications.\n\nEXAMPLES:\n CF_NAME update-service mydb -p gold\n CF_NAME update-service mydb -c '{\"ram_gb\":4}'\n CF_NAME update-service mydb -c ~/workspace/tmp/instance_config.json\n CF_NAME update-service mydb -t \"list, of, tags\""` relatedCommands interface{} `related_commands:"rename-service, services, update-user-provided-service"` + Upgrade bool `long:"upgrade" hidden:"true"` + + UI command.UI + Actor UpdateServiceActor + SharedActor command.SharedActor + Config command.Config } -func (UpdateServiceCommand) Setup(config command.Config, ui command.UI) error { +func (cmd *UpdateServiceCommand) Setup(config command.Config, ui command.UI) error { + cmd.UI = ui + cmd.Config = config + + cmd.SharedActor = sharedaction.NewActor(config) + + ccClient, uaaClient, err := shared.NewClients(config, ui, true) + if err != nil { + return err + } + + baseActor := v2action.NewActor(ccClient, uaaClient, config) + cmd.Actor = &composite.UpdateServiceInstanceCompositeActor{ + GetServiceInstanceActor: baseActor, + GetServicePlanActor: baseActor, + UpdateServiceInstanceMaintenanceInfoActor: baseActor, + } + return nil } -func (UpdateServiceCommand) Execute(args []string) error { - return translatableerror.UnrefactoredCommandError{} +func (cmd *UpdateServiceCommand) Execute(args []string) error { + if !cmd.Upgrade { + return translatableerror.UnrefactoredCommandError{} + } + + if len(args) > 0 { + return translatableerror.TooManyArgumentsError{ + ExtraArgument: args[0], + } + } + + if err := cmd.validateArgumentCombination(); err != nil { + return err + } + + if err := cmd.SharedActor.CheckTarget(true, true); err != nil { + return err + } + + instance, warnings, err := cmd.Actor.GetServiceInstanceByNameAndSpace(cmd.RequiredArgs.ServiceInstance, cmd.Config.TargetedSpace().GUID) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + + proceed, err := cmd.promptForUpgrade() + + if err != nil { + return err + } + + if !proceed { + cmd.UI.DisplayText("Update cancelled") + return nil + } + + return cmd.performUpgrade(instance) +} + +func (cmd *UpdateServiceCommand) promptForUpgrade() (bool, error) { + var serviceName = textData{"ServiceName": cmd.RequiredArgs.ServiceInstance} + + cmd.UI.DisplayText("This command is in EXPERIMENTAL stage and may change without notice.") + cmd.UI.DisplayTextWithFlavor("You are about to update {{.ServiceName}}.", serviceName) + cmd.UI.DisplayText("Warning: This operation may be long running and will block further operations on the service until complete.") + + return cmd.UI.DisplayBoolPrompt(false, "Really update service {{.ServiceName}}?", serviceName) +} + +func (cmd *UpdateServiceCommand) performUpgrade(instance v2action.ServiceInstance) error { + warnings, err := cmd.Actor.UpgradeServiceInstance(instance.GUID, instance.ServicePlanGUID) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + + cmd.UI.DisplayOK() + return nil +} + +func (cmd *UpdateServiceCommand) validateArgumentCombination() error { + if cmd.Tags != "" || cmd.ParametersAsJSON != "" || cmd.Plan != "" { + return translatableerror.ArgumentCombinationError{ + Args: []string{"--upgrade", "-t", "-c", "-p"}, + } + } + + return nil } diff --git a/command/v6/update_service_command_test.go b/command/v6/update_service_command_test.go new file mode 100644 index 00000000000..ff6cbb4cc01 --- /dev/null +++ b/command/v6/update_service_command_test.go @@ -0,0 +1,272 @@ +package v6_test + +import ( + "errors" + "fmt" + + "code.cloudfoundry.org/cli/actor/v2action" + "code.cloudfoundry.org/cli/command/commandfakes" + "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/command/translatableerror" + . "code.cloudfoundry.org/cli/command/v6" + "code.cloudfoundry.org/cli/command/v6/v6fakes" + "code.cloudfoundry.org/cli/util/configv3" + "code.cloudfoundry.org/cli/util/ui" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("update-service Command", func() { + + const ( + serviceInstanceName = "my-service" + spaceGUID = "space-guid" + instanceGUID = "instance-guid" + planGUID = "plan-guid" + ) + + var ( + cmd UpdateServiceCommand + fakeActor *v6fakes.FakeUpdateServiceActor + fakeSharedActor *commandfakes.FakeSharedActor + fakeConfig *commandfakes.FakeConfig + testUI *ui.UI + input *Buffer + executeErr error + extraArgs []string + + space = configv3.Space{Name: "space-a", GUID: spaceGUID} + ) + + BeforeEach(func() { + input = NewBuffer() + testUI = ui.NewTestUI(input, NewBuffer(), NewBuffer()) + fakeActor = new(v6fakes.FakeUpdateServiceActor) + fakeSharedActor = new(commandfakes.FakeSharedActor) + fakeConfig = new(commandfakes.FakeConfig) + + fakeConfig.TargetedSpaceReturns(space) + + extraArgs = []string{} + + cmd = UpdateServiceCommand{ + UI: testUI, + Actor: fakeActor, + SharedActor: fakeSharedActor, + Config: fakeConfig, + RequiredArgs: flag.ServiceInstance{ServiceInstance: serviceInstanceName}, + } + }) + + JustBeforeEach(func() { + executeErr = cmd.Execute(extraArgs) + }) + + When("not upgrading", func() { + It("returns UnrefactoredCommandError", func() { + // delegates non-upgrades to legacy code + Expect(executeErr).To(MatchError(translatableerror.UnrefactoredCommandError{})) + }) + }) + + When("combining upgrade with other flags", func() { + BeforeEach(func() { + cmd.Upgrade = true + }) + + When("tags provided", func() { + BeforeEach(func() { + cmd.Tags = "tags" + }) + + It("returns UpgradeArgumentCombinationError", func() { + Expect(executeErr).To(MatchError(translatableerror.ArgumentCombinationError{ + Args: []string{"--upgrade", "-t", "-c", "-p"}, + })) + }) + }) + + When("parameters provided", func() { + BeforeEach(func() { + cmd.ParametersAsJSON = "{\"some\": \"stuff\"}" + }) + + It("returns UpgradeArgumentCombinationError", func() { + Expect(executeErr).To(MatchError(translatableerror.ArgumentCombinationError{ + Args: []string{"--upgrade", "-t", "-c", "-p"}, + })) + }) + }) + + When("plan provided", func() { + BeforeEach(func() { + cmd.Plan = "new-plan" + }) + + It("returns UpgradeArgumentCombinationError", func() { + Expect(executeErr).To(MatchError(translatableerror.ArgumentCombinationError{ + Args: []string{"--upgrade", "-t", "-c", "-p"}, + })) + }) + }) + }) + + When("upgrading", func() { + BeforeEach(func() { + cmd.Upgrade = true + }) + + It("checks the user is logged in, and targeting an org and space", func() { + Expect(fakeSharedActor.CheckTargetCallCount()).To(Equal(1)) + orgChecked, spaceChecked := fakeSharedActor.CheckTargetArgsForCall(0) + Expect(orgChecked).To(BeTrue()) + Expect(spaceChecked).To(BeTrue()) + }) + + When("checking the target succeeds", func() { + When("getting the service instance succeeds", func() { + BeforeEach(func() { + fakeActor.GetServiceInstanceByNameAndSpaceReturns( + v2action.ServiceInstance{GUID: instanceGUID, ServicePlanGUID: planGUID}, + v2action.Warnings{"warning"}, + nil) + }) + + It("displays any warnings", func() { + Expect(testUI.Err).To(Say("warning")) + }) + + It("mentions that the command is experimental", func() { + Expect(testUI.Out).To(Say("This command is in EXPERIMENTAL stage and may change without notice\\.")) + }) + + It("prompts the user about the upgrade", func() { + Expect(testUI.Out).To(Say("You are about to update %s\\.", serviceInstanceName)) + Expect(testUI.Out).To(Say("Warning: This operation may be long running and will block further operations on the service until complete\\.")) + Expect(testUI.Out).To(Say("Really update service %s\\? \\[yN\\]:", serviceInstanceName)) + }) + + When("user refuses to proceed with the upgrade", func() { + BeforeEach(func() { + input.Write([]byte("n\n")) + }) + + It("does not send an upgrade request", func() { + Expect(fakeActor.UpgradeServiceInstanceCallCount()).To(Equal(0)) + }) + + It("cancels the update", func() { + Expect(executeErr).NotTo(HaveOccurred()) + Expect(testUI.Out).To(Say("Update cancelled")) + }) + }) + + When("user goes ahead with the upgrade", func() { + BeforeEach(func() { + input.Write([]byte("y\n")) + }) + + It("sends an upgrade request", func() { + Expect(fakeActor.UpgradeServiceInstanceCallCount()).To(Equal(1), "upgrade should be requested") + + serviceInstanceGUID, servicePlanGUID := fakeActor.UpgradeServiceInstanceArgsForCall(0) + Expect(serviceInstanceGUID).To(Equal(instanceGUID)) + Expect(servicePlanGUID).To(Equal(planGUID)) + }) + + When("the update request succeeds", func() { + It("says that the update was successful", func() { + Expect(executeErr).NotTo(HaveOccurred()) + Expect(testUI.Out).To(Say("OK")) + }) + }) + + When("the update request fails", func() { + BeforeEach(func() { + fakeActor.UpgradeServiceInstanceReturns( + v2action.Warnings{}, + fmt.Errorf("bad things happened"), + ) + }) + + It("says that the update has failed", func() { + Expect(executeErr).To(MatchError("bad things happened")) + }) + }) + + When("there are warnings", func() { + BeforeEach(func() { + fakeActor.UpgradeServiceInstanceReturns( + v2action.Warnings{"fake upgrade warning 1", "fake upgrade warning 2"}, + nil, + ) + }) + + It("outputs the warnings", func() { + Expect(testUI.Err).To(Say("fake upgrade warning 1")) + Expect(testUI.Err).To(Say("fake upgrade warning 2")) + }) + + It("can still output OK", func() { + Expect(testUI.Out).To(Say("OK")) + }) + }) + }) + + When("user presses return", func() { + BeforeEach(func() { + input.Write([]byte("\n")) + }) + + It("cancels the update", func() { + Expect(testUI.Out).To(Say("Update cancelled")) + Expect(executeErr).NotTo(HaveOccurred()) + }) + }) + + When("user does not answer", func() { + It("fails", func() { + Expect(executeErr).To(MatchError("EOF")) + }) + }) + }) + + When("getting the service instance fails", func() { + BeforeEach(func() { + fakeActor.GetServiceInstanceByNameAndSpaceReturns(v2action.ServiceInstance{}, v2action.Warnings{"warning"}, errors.New("explode")) + }) + + It("propagates the error", func() { + Expect(executeErr).To(MatchError("explode")) + }) + + It("displays any warnings", func() { + Expect(testUI.Err).To(Say("warning")) + }) + }) + }) + + When("too many arguments are provided", func() { + BeforeEach(func() { + extraArgs = []string{"extra"} + }) + + It("returns a TooManyArgumentsError", func() { + Expect(executeErr).To(MatchError(translatableerror.TooManyArgumentsError{ + ExtraArgument: "extra", + })) + }) + }) + + When("checking the target returns an error", func() { + BeforeEach(func() { + fakeSharedActor.CheckTargetReturns(errors.New("explode")) + }) + + It("returns an error", func() { + Expect(executeErr).To(MatchError("explode")) + }) + }) + }) +}) diff --git a/command/v6/v6fakes/fake_update_service_actor.go b/command/v6/v6fakes/fake_update_service_actor.go new file mode 100644 index 00000000000..d4c47883343 --- /dev/null +++ b/command/v6/v6fakes/fake_update_service_actor.go @@ -0,0 +1,203 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package v6fakes + +import ( + sync "sync" + + v2action "code.cloudfoundry.org/cli/actor/v2action" + v6 "code.cloudfoundry.org/cli/command/v6" +) + +type FakeUpdateServiceActor struct { + GetServiceInstanceByNameAndSpaceStub func(string, string) (v2action.ServiceInstance, v2action.Warnings, error) + getServiceInstanceByNameAndSpaceMutex sync.RWMutex + getServiceInstanceByNameAndSpaceArgsForCall []struct { + arg1 string + arg2 string + } + getServiceInstanceByNameAndSpaceReturns struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + } + getServiceInstanceByNameAndSpaceReturnsOnCall map[int]struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + } + UpgradeServiceInstanceStub func(string, string) (v2action.Warnings, error) + upgradeServiceInstanceMutex sync.RWMutex + upgradeServiceInstanceArgsForCall []struct { + arg1 string + arg2 string + } + upgradeServiceInstanceReturns struct { + result1 v2action.Warnings + result2 error + } + upgradeServiceInstanceReturnsOnCall map[int]struct { + result1 v2action.Warnings + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeUpdateServiceActor) GetServiceInstanceByNameAndSpace(arg1 string, arg2 string) (v2action.ServiceInstance, v2action.Warnings, error) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + ret, specificReturn := fake.getServiceInstanceByNameAndSpaceReturnsOnCall[len(fake.getServiceInstanceByNameAndSpaceArgsForCall)] + fake.getServiceInstanceByNameAndSpaceArgsForCall = append(fake.getServiceInstanceByNameAndSpaceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetServiceInstanceByNameAndSpace", []interface{}{arg1, arg2}) + fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + if fake.GetServiceInstanceByNameAndSpaceStub != nil { + return fake.GetServiceInstanceByNameAndSpaceStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getServiceInstanceByNameAndSpaceReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeUpdateServiceActor) GetServiceInstanceByNameAndSpaceCallCount() int { + fake.getServiceInstanceByNameAndSpaceMutex.RLock() + defer fake.getServiceInstanceByNameAndSpaceMutex.RUnlock() + return len(fake.getServiceInstanceByNameAndSpaceArgsForCall) +} + +func (fake *FakeUpdateServiceActor) GetServiceInstanceByNameAndSpaceCalls(stub func(string, string) (v2action.ServiceInstance, v2action.Warnings, error)) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + defer fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + fake.GetServiceInstanceByNameAndSpaceStub = stub +} + +func (fake *FakeUpdateServiceActor) GetServiceInstanceByNameAndSpaceArgsForCall(i int) (string, string) { + fake.getServiceInstanceByNameAndSpaceMutex.RLock() + defer fake.getServiceInstanceByNameAndSpaceMutex.RUnlock() + argsForCall := fake.getServiceInstanceByNameAndSpaceArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeUpdateServiceActor) GetServiceInstanceByNameAndSpaceReturns(result1 v2action.ServiceInstance, result2 v2action.Warnings, result3 error) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + defer fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + fake.GetServiceInstanceByNameAndSpaceStub = nil + fake.getServiceInstanceByNameAndSpaceReturns = struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeUpdateServiceActor) GetServiceInstanceByNameAndSpaceReturnsOnCall(i int, result1 v2action.ServiceInstance, result2 v2action.Warnings, result3 error) { + fake.getServiceInstanceByNameAndSpaceMutex.Lock() + defer fake.getServiceInstanceByNameAndSpaceMutex.Unlock() + fake.GetServiceInstanceByNameAndSpaceStub = nil + if fake.getServiceInstanceByNameAndSpaceReturnsOnCall == nil { + fake.getServiceInstanceByNameAndSpaceReturnsOnCall = make(map[int]struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + }) + } + fake.getServiceInstanceByNameAndSpaceReturnsOnCall[i] = struct { + result1 v2action.ServiceInstance + result2 v2action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeUpdateServiceActor) UpgradeServiceInstance(arg1 string, arg2 string) (v2action.Warnings, error) { + fake.upgradeServiceInstanceMutex.Lock() + ret, specificReturn := fake.upgradeServiceInstanceReturnsOnCall[len(fake.upgradeServiceInstanceArgsForCall)] + fake.upgradeServiceInstanceArgsForCall = append(fake.upgradeServiceInstanceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("UpgradeServiceInstance", []interface{}{arg1, arg2}) + fake.upgradeServiceInstanceMutex.Unlock() + if fake.UpgradeServiceInstanceStub != nil { + return fake.UpgradeServiceInstanceStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.upgradeServiceInstanceReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeUpdateServiceActor) UpgradeServiceInstanceCallCount() int { + fake.upgradeServiceInstanceMutex.RLock() + defer fake.upgradeServiceInstanceMutex.RUnlock() + return len(fake.upgradeServiceInstanceArgsForCall) +} + +func (fake *FakeUpdateServiceActor) UpgradeServiceInstanceCalls(stub func(string, string) (v2action.Warnings, error)) { + fake.upgradeServiceInstanceMutex.Lock() + defer fake.upgradeServiceInstanceMutex.Unlock() + fake.UpgradeServiceInstanceStub = stub +} + +func (fake *FakeUpdateServiceActor) UpgradeServiceInstanceArgsForCall(i int) (string, string) { + fake.upgradeServiceInstanceMutex.RLock() + defer fake.upgradeServiceInstanceMutex.RUnlock() + argsForCall := fake.upgradeServiceInstanceArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeUpdateServiceActor) UpgradeServiceInstanceReturns(result1 v2action.Warnings, result2 error) { + fake.upgradeServiceInstanceMutex.Lock() + defer fake.upgradeServiceInstanceMutex.Unlock() + fake.UpgradeServiceInstanceStub = nil + fake.upgradeServiceInstanceReturns = struct { + result1 v2action.Warnings + result2 error + }{result1, result2} +} + +func (fake *FakeUpdateServiceActor) UpgradeServiceInstanceReturnsOnCall(i int, result1 v2action.Warnings, result2 error) { + fake.upgradeServiceInstanceMutex.Lock() + defer fake.upgradeServiceInstanceMutex.Unlock() + fake.UpgradeServiceInstanceStub = nil + if fake.upgradeServiceInstanceReturnsOnCall == nil { + fake.upgradeServiceInstanceReturnsOnCall = make(map[int]struct { + result1 v2action.Warnings + result2 error + }) + } + fake.upgradeServiceInstanceReturnsOnCall[i] = struct { + result1 v2action.Warnings + result2 error + }{result1, result2} +} + +func (fake *FakeUpdateServiceActor) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getServiceInstanceByNameAndSpaceMutex.RLock() + defer fake.getServiceInstanceByNameAndSpaceMutex.RUnlock() + fake.upgradeServiceInstanceMutex.RLock() + defer fake.upgradeServiceInstanceMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeUpdateServiceActor) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ v6.UpdateServiceActor = new(FakeUpdateServiceActor) diff --git a/integration/assets/service_broker/broker_config.json b/integration/assets/service_broker/broker_config.json index 6f66372e878..02071fce239 100644 --- a/integration/assets/service_broker/broker_config.json +++ b/integration/assets/service_broker/broker_config.json @@ -57,7 +57,10 @@ } ] }, - "schemas": "" + "schemas": "", + "maintenance_info" : { + "version": "1.2.3" + } }, { "name": "", diff --git a/integration/helpers/service_broker.go b/integration/helpers/service_broker.go index c81d421ff73..a90863c81e7 100644 --- a/integration/helpers/service_broker.go +++ b/integration/helpers/service_broker.go @@ -135,6 +135,7 @@ func (b ServiceBroker) Configure(shareable bool) { resp, err := http.DefaultClient.Do(req) Expect(err).ToNot(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) defer resp.Body.Close() } diff --git a/integration/shared/isolated/update_service_command_test.go b/integration/shared/isolated/update_service_command_test.go index 5df7793fc87..11b9026bd84 100644 --- a/integration/shared/isolated/update_service_command_test.go +++ b/integration/shared/isolated/update_service_command_test.go @@ -1,6 +1,7 @@ package isolated import ( + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" "code.cloudfoundry.org/cli/integration/helpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -9,6 +10,13 @@ import ( ) var _ = Describe("update-service command", func() { + When("the environment is not setup correctly", func() { + It("fails with the appropriate errors", func() { + // the upgrade flag is passed here to exercise a particular code path before refactoring + helpers.CheckEnvironmentTargetedCorrectly(true, true, ReadOnlyOrg, "update-service", "foo", "--upgrade") + }) + }) + When("an api is targeted, the user is logged in, and an org and space are targeted", func() { var ( orgName string @@ -24,6 +32,26 @@ var _ = Describe("update-service command", func() { helpers.QuickDeleteOrg(orgName) }) + When("there are no service instances", func() { + When("upgrading", func() { + It("displays an informative error before prompting and exits 1", func() { + session := helpers.CF("update-service", "non-existent-service", "--upgrade") + Eventually(session.Err).Should(Say("Service instance non-existent-service not found")) + Eventually(session).Should(Exit(1)) + }) + }) + }) + + When("providing other arguments while upgrading", func() { + It("displays an informative error message and exits 1", func() { + session := helpers.CF("update-service", "irrelevant", "--upgrade", "-c", "{\"hello\": \"world\"}") + Eventually(session.Err).Should(Say("Incorrect Usage: The following arguments cannot be used together: --upgrade, -t, -c, -p")) + Eventually(session).Should(Say("FAILED")) + Eventually(session).Should(Say("USAGE:")) + Eventually(session).Should(Exit(1)) + }) + }) + When("there is a service instance", func() { var ( service string @@ -39,6 +67,7 @@ var _ = Describe("update-service command", func() { servicePlan = helpers.PrefixedRandomName("SERVICE-PLAN") broker = helpers.CreateBroker(domain, service, servicePlan) + Eventually(helpers.CF("service-access")).Should(Say(service)) Eventually(helpers.CF("enable-service-access", service)).Should(Exit(0)) serviceInstanceName = helpers.PrefixedRandomName("SI") @@ -68,6 +97,52 @@ var _ = Describe("update-service command", func() { Eventually(session).Should(Exit(0)) }) }) + + When("upgrading", func() { + var buffer *Buffer + + BeforeEach(func() { + buffer = NewBuffer() + }) + + When("cancelling the update", func() { + BeforeEach(func() { + _, err := buffer.Write([]byte("n\n")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("does not proceed", func() { + session := helpers.CFWithStdin(buffer, "update-service", serviceInstanceName, "--upgrade") + Eventually(session).Should(Say("You are about to update %s", serviceInstanceName)) + Eventually(session).Should(Say("Warning: This operation may be long running and will block further operations on the service until complete.")) + Eventually(session).Should(Say("Really update service %s\\? \\[yN\\]:", serviceInstanceName)) + Eventually(session).Should(Say("Update cancelled")) + Eventually(session).Should(Exit(0)) + }) + }) + + When("proceeding with the update", func() { + BeforeEach(func() { + helpers.SkipIfVersionLessThan(ccversion.MinVersionUpdateServiceInstanceMaintenanceInfoV2) + _, err := buffer.Write([]byte("y\n")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("updates the service", func() { + session := helpers.CFWithStdin(buffer, "update-service", serviceInstanceName, "--upgrade") + + By("displaying an informative message") + Eventually(session).Should(Say("You are about to update %s", serviceInstanceName)) + Eventually(session).Should(Say("Warning: This operation may be long running and will block further operations on the service until complete.")) + Eventually(session).Should(Say("Really update service %s\\? \\[yN\\]:", serviceInstanceName)) + Eventually(session).Should(Exit(0)) + + By("requesting an upgrade from the platform") + session = helpers.CF("service", serviceInstanceName) + Eventually(session).Should(Say("status:\\s+update succeeded")) + }) + }) + }) }) }) })