diff --git a/actor/v7action/cloud_controller_client.go b/actor/v7action/cloud_controller_client.go index fbdceb791c..b70e63b552 100644 --- a/actor/v7action/cloud_controller_client.go +++ b/actor/v7action/cloud_controller_client.go @@ -138,7 +138,7 @@ type CloudControllerClient interface { GetAppFeature(appGUID string, featureName string) (resources.ApplicationFeature, ccv3.Warnings, error) GetStacks(query ...ccv3.Query) ([]resources.Stack, ccv3.Warnings, error) GetStagingSecurityGroups(spaceGUID string, queries ...ccv3.Query) ([]resources.SecurityGroup, ccv3.Warnings, error) - UpdateStack(stackGUID string, state string) (resources.Stack, ccv3.Warnings, error) + UpdateStack(stackGUID string, state string, reason string) (resources.Stack, ccv3.Warnings, error) GetTask(guid string) (resources.Task, ccv3.Warnings, error) GetUser(userGUID string) (resources.User, ccv3.Warnings, error) GetUsers(query ...ccv3.Query) ([]resources.User, ccv3.Warnings, error) diff --git a/actor/v7action/stack.go b/actor/v7action/stack.go index 3c674c2da8..41715cd995 100644 --- a/actor/v7action/stack.go +++ b/actor/v7action/stack.go @@ -47,8 +47,8 @@ func (actor Actor) GetStacks(labelSelector string) ([]resources.Stack, Warnings, return stacks, Warnings(warnings), nil } -func (actor Actor) UpdateStack(stackGUID string, state string) (resources.Stack, Warnings, error) { - stack, warnings, err := actor.CloudControllerClient.UpdateStack(stackGUID, state) +func (actor Actor) UpdateStack(stackGUID string, state string, reason string) (resources.Stack, Warnings, error) { + stack, warnings, err := actor.CloudControllerClient.UpdateStack(stackGUID, state, reason) if err != nil { return resources.Stack{}, Warnings(warnings), err } diff --git a/actor/v7action/stack_test.go b/actor/v7action/stack_test.go index f401f421ee..73f425979b 100644 --- a/actor/v7action/stack_test.go +++ b/actor/v7action/stack_test.go @@ -238,6 +238,7 @@ var _ = Describe("Stack", func() { var ( stackGUID string state string + reason string stack resources.Stack warnings Warnings executeErr error @@ -246,10 +247,11 @@ var _ = Describe("Stack", func() { BeforeEach(func() { stackGUID = "some-stack-guid" state = "DEPRECATED" + reason = "" }) JustBeforeEach(func() { - stack, warnings, executeErr = actor.UpdateStack(stackGUID, state) + stack, warnings, executeErr = actor.UpdateStack(stackGUID, state, reason) }) When("the cloud controller request is successful", func() { @@ -277,9 +279,10 @@ var _ = Describe("Stack", func() { })) Expect(fakeCloudControllerClient.UpdateStackCallCount()).To(Equal(1)) - actualGUID, actualState := fakeCloudControllerClient.UpdateStackArgsForCall(0) + actualGUID, actualState, actualReason := fakeCloudControllerClient.UpdateStackArgsForCall(0) Expect(actualGUID).To(Equal(stackGUID)) Expect(actualState).To(Equal(state)) + Expect(actualReason).To(Equal(reason)) }) }) diff --git a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go index b8abbda3f6..19a65585c5 100644 --- a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go +++ b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go @@ -2709,11 +2709,12 @@ type FakeCloudControllerClient struct { result2 ccv3.Warnings result3 error } - UpdateStackStub func(string, string) (resources.Stack, ccv3.Warnings, error) + UpdateStackStub func(string, string, string) (resources.Stack, ccv3.Warnings, error) updateStackMutex sync.RWMutex updateStackArgsForCall []struct { arg1 string arg2 string + arg3 string } updateStackReturns struct { result1 resources.Stack @@ -14801,19 +14802,20 @@ func (fake *FakeCloudControllerClient) UpdateSpaceQuotaReturnsOnCall(i int, resu }{result1, result2, result3} } -func (fake *FakeCloudControllerClient) UpdateStack(arg1 string, arg2 string) (resources.Stack, ccv3.Warnings, error) { +func (fake *FakeCloudControllerClient) UpdateStack(arg1 string, arg2 string, arg3 string) (resources.Stack, ccv3.Warnings, error) { fake.updateStackMutex.Lock() ret, specificReturn := fake.updateStackReturnsOnCall[len(fake.updateStackArgsForCall)] fake.updateStackArgsForCall = append(fake.updateStackArgsForCall, struct { arg1 string arg2 string - }{arg1, arg2}) + arg3 string + }{arg1, arg2, arg3}) stub := fake.UpdateStackStub fakeReturns := fake.updateStackReturns - fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2}) + fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2, arg3}) fake.updateStackMutex.Unlock() if stub != nil { - return stub(arg1, arg2) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2, ret.result3 @@ -14827,17 +14829,17 @@ func (fake *FakeCloudControllerClient) UpdateStackCallCount() int { return len(fake.updateStackArgsForCall) } -func (fake *FakeCloudControllerClient) UpdateStackCalls(stub func(string, string) (resources.Stack, ccv3.Warnings, error)) { +func (fake *FakeCloudControllerClient) UpdateStackCalls(stub func(string, string, string) (resources.Stack, ccv3.Warnings, error)) { fake.updateStackMutex.Lock() defer fake.updateStackMutex.Unlock() fake.UpdateStackStub = stub } -func (fake *FakeCloudControllerClient) UpdateStackArgsForCall(i int) (string, string) { +func (fake *FakeCloudControllerClient) UpdateStackArgsForCall(i int) (string, string, string) { fake.updateStackMutex.RLock() defer fake.updateStackMutex.RUnlock() argsForCall := fake.updateStackArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeCloudControllerClient) UpdateStackReturns(result1 resources.Stack, result2 ccv3.Warnings, result3 error) { diff --git a/api/cloudcontroller/ccv3/stack.go b/api/cloudcontroller/ccv3/stack.go index feb2748663..ceb8068470 100644 --- a/api/cloudcontroller/ccv3/stack.go +++ b/api/cloudcontroller/ccv3/stack.go @@ -22,18 +22,19 @@ func (client *Client) GetStacks(query ...Query) ([]resources.Stack, Warnings, er return stacks, warnings, err } -// UpdateStack updates a stack's state. -func (client *Client) UpdateStack(stackGUID string, state string) (resources.Stack, Warnings, error) { +// UpdateStack updates a stack's state and optionally its state reason. +func (client *Client) UpdateStack(stackGUID string, state string, reason string) (resources.Stack, Warnings, error) { var responseStack resources.Stack type StackUpdate struct { - State string `json:"state"` + State string `json:"state"` + StateReason string `json:"state_reason,omitempty"` } _, warnings, err := client.MakeRequest(RequestParams{ RequestName: internal.PatchStackRequest, URIParams: internal.Params{"stack_guid": stackGUID}, - RequestBody: StackUpdate{State: state}, + RequestBody: StackUpdate{State: state, StateReason: reason}, ResponseBody: &responseStack, }) diff --git a/api/cloudcontroller/ccv3/stack_test.go b/api/cloudcontroller/ccv3/stack_test.go index 447eed4302..2c464ac783 100644 --- a/api/cloudcontroller/ccv3/stack_test.go +++ b/api/cloudcontroller/ccv3/stack_test.go @@ -148,6 +148,7 @@ var _ = Describe("Stacks", func() { var ( stackGUID string state string + reason string stack resources.Stack warnings Warnings err error @@ -156,10 +157,11 @@ var _ = Describe("Stacks", func() { BeforeEach(func() { stackGUID = "some-stack-guid" state = "DEPRECATED" + reason = "" }) JustBeforeEach(func() { - stack, warnings, err = client.UpdateStack(stackGUID, state) + stack, warnings, err = client.UpdateStack(stackGUID, state, reason) }) When("the request succeeds", func() { @@ -192,6 +194,40 @@ var _ = Describe("Stacks", func() { }) }) + When("a reason is provided", func() { + BeforeEach(func() { + reason = "Use cflinuxfs4 instead" + server.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodPatch, "/v3/stacks/some-stack-guid"), + VerifyJSONRepresenting(map[string]string{ + "state": "DEPRECATED", + "state_reason": "Use cflinuxfs4 instead", + }), + RespondWith(http.StatusOK, `{ + "guid": "some-stack-guid", + "name": "some-stack", + "description": "some description", + "state": "DEPRECATED", + "state_reason": "Use cflinuxfs4 instead" + }`, http.Header{"X-Cf-Warnings": {"this is a warning"}}), + ), + ) + }) + + It("returns the updated stack with reason and warnings", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(warnings).To(ConsistOf("this is a warning")) + Expect(stack).To(Equal(resources.Stack{ + GUID: "some-stack-guid", + Name: "some-stack", + Description: "some description", + State: "DEPRECATED", + StateReason: "Use cflinuxfs4 instead", + })) + }) + }) + When("the cloud controller returns an error", func() { BeforeEach(func() { server.AppendHandlers( diff --git a/api/cloudcontroller/ccversion/minimum_version.go b/api/cloudcontroller/ccversion/minimum_version.go index a092d504d0..7959916617 100644 --- a/api/cloudcontroller/ccversion/minimum_version.go +++ b/api/cloudcontroller/ccversion/minimum_version.go @@ -25,5 +25,5 @@ const ( MinVersionServiceBindingStrategy = "3.205.0" - MinVersionUpdateStack = "3.210.0" + MinVersionUpdateStack = "3.211.0" ) diff --git a/command/v7/actor.go b/command/v7/actor.go index dc7f60fd61..bb2a59c930 100644 --- a/command/v7/actor.go +++ b/command/v7/actor.go @@ -179,7 +179,7 @@ type Actor interface { GetStackByName(stackName string) (resources.Stack, v7action.Warnings, error) GetStackLabels(stackName string) (map[string]types.NullString, v7action.Warnings, error) GetStacks(string) ([]resources.Stack, v7action.Warnings, error) - UpdateStack(stackGUID string, state string) (resources.Stack, v7action.Warnings, error) + UpdateStack(stackGUID string, state string, reason string) (resources.Stack, v7action.Warnings, error) GetStreamingLogsForApplicationByNameAndSpace(appName string, spaceGUID string, client sharedaction.LogCacheClient) (<-chan sharedaction.LogMessage, <-chan error, context.CancelFunc, v7action.Warnings, error) GetTaskBySequenceIDAndApplication(sequenceID int, appGUID string) (resources.Task, v7action.Warnings, error) GetUAAAPIVersion() (string, error) diff --git a/command/v7/stack_command.go b/command/v7/stack_command.go index d451fa9d49..bc15d7300c 100644 --- a/command/v7/stack_command.go +++ b/command/v7/stack_command.go @@ -69,6 +69,11 @@ func (cmd *StackCommand) displayStackInfo() error { // Add state only if it's present if stack.State != "" { displayTable = append(displayTable, []string{cmd.UI.TranslateText("state:"), stack.State}) + + // Add reason whenever state is not ACTIVE + if stack.State != resources.StackStateActive { + displayTable = append(displayTable, []string{cmd.UI.TranslateText("reason:"), stack.StateReason}) + } } cmd.UI.DisplayKeyValueTable("", displayTable, 3) diff --git a/command/v7/stack_command_test.go b/command/v7/stack_command_test.go index 643cd162c3..9b5192c4f2 100644 --- a/command/v7/stack_command_test.go +++ b/command/v7/stack_command_test.go @@ -148,7 +148,8 @@ var _ = Describe("Stack Command", func() { }) }) - Context("When the stack has a state", func() { + Context("When the stack has a state", func() { + Context("When the state is ACTIVE", func() { BeforeEach(func() { stack := resources.Stack{ Name: "some-stack-name", @@ -159,7 +160,7 @@ var _ = Describe("Stack Command", func() { fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil) }) - It("Displays the stack information with state", func() { + It("Displays the stack information with state but no reason", func() { Expect(executeErr).ToNot(HaveOccurred()) Expect(fakeActor.GetStackByNameArgsForCall(0)).To(Equal("some-stack-name")) Expect(fakeActor.GetStackByNameCallCount()).To(Equal(1)) @@ -167,9 +168,58 @@ var _ = Describe("Stack Command", func() { Expect(testUI.Out).To(Say("name:\\s+some-stack-name")) Expect(testUI.Out).To(Say("description:\\s+some-stack-desc")) Expect(testUI.Out).To(Say("state:\\s+ACTIVE")) + Expect(testUI.Out).NotTo(Say("reason:")) }) }) + Context("When the state is not ACTIVE and has a reason", func() { + BeforeEach(func() { + stack := resources.Stack{ + Name: "some-stack-name", + GUID: "some-stack-guid", + Description: "some-stack-desc", + State: "DEPRECATED", + StateReason: "This stack is being phased out", + } + fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil) + }) + + It("Displays the stack information with state and reason", func() { + Expect(executeErr).ToNot(HaveOccurred()) + Expect(fakeActor.GetStackByNameArgsForCall(0)).To(Equal("some-stack-name")) + Expect(fakeActor.GetStackByNameCallCount()).To(Equal(1)) + + Expect(testUI.Out).To(Say("name:\\s+some-stack-name")) + Expect(testUI.Out).To(Say("description:\\s+some-stack-desc")) + Expect(testUI.Out).To(Say("state:\\s+DEPRECATED")) + Expect(testUI.Out).To(Say("reason:\\s+This stack is being phased out")) + }) + }) + + Context("When the state is not ACTIVE but has no reason", func() { + BeforeEach(func() { + stack := resources.Stack{ + Name: "some-stack-name", + GUID: "some-stack-guid", + Description: "some-stack-desc", + State: "RESTRICTED", + } + fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil) + }) + + It("Displays the stack information with state and empty reason", func() { + Expect(executeErr).ToNot(HaveOccurred()) + Expect(fakeActor.GetStackByNameArgsForCall(0)).To(Equal("some-stack-name")) + Expect(fakeActor.GetStackByNameCallCount()).To(Equal(1)) + + Expect(testUI.Out).To(Say("name:\\s+some-stack-name")) + Expect(testUI.Out).To(Say("description:\\s+some-stack-desc")) + Expect(testUI.Out).To(Say("state:\\s+RESTRICTED")) + Expect(testUI.Out).To(Say("reason:")) + }) + }) + }) + When("The Stack does not Exist", func() { expectedError := actionerror.StackNotFoundError{Name: "some-stack-name"} BeforeEach(func() { diff --git a/command/v7/update_stack_command.go b/command/v7/update_stack_command.go index 35e2f86c39..afe3688acd 100644 --- a/command/v7/update_stack_command.go +++ b/command/v7/update_stack_command.go @@ -15,7 +15,8 @@ type UpdateStackCommand struct { RequiredArgs flag.StackName `positional-args:"yes"` State string `long:"state" description:"State to transition the stack to (active, restricted, deprecated, disabled)" required:"true"` - usage interface{} `usage:"CF_NAME update-stack STACK_NAME [--state (active | restricted | deprecated | disabled)]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled"` + Reason string `long:"reason" description:"Optional plain text describing the stack state change"` + usage interface{} `usage:"CF_NAME update-stack STACK_NAME [--state (active | restricted | deprecated | disabled)] [--reason text]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled\n CF_NAME update-stack cflinuxfs3 --state deprecated --reason \"This stack is based on Ubuntu 18.04, which is no longer supported. Please migrate your applications to 'cflinuxfs4'. For more information, see: .\""` relatedCommands interface{} `related_commands:"stack, stacks"` } @@ -56,7 +57,7 @@ func (cmd UpdateStackCommand) Execute(args []string) error { } // Update the stack - updatedStack, warnings, err := cmd.Actor.UpdateStack(stack.GUID, stateValue) + updatedStack, warnings, err := cmd.Actor.UpdateStack(stack.GUID, stateValue, cmd.Reason) cmd.UI.DisplayWarnings(warnings) if err != nil { return err @@ -66,11 +67,18 @@ func (cmd UpdateStackCommand) Execute(args []string) error { cmd.UI.DisplayNewline() // Display the updated stack info - cmd.UI.DisplayKeyValueTable("", [][]string{ + displayTable := [][]string{ {cmd.UI.TranslateText("name:"), updatedStack.Name}, {cmd.UI.TranslateText("description:"), updatedStack.Description}, {cmd.UI.TranslateText("state:"), updatedStack.State}, - }, 3) + } + + // Add reason whenever state is not ACTIVE + if updatedStack.State != resources.StackStateActive { + displayTable = append(displayTable, []string{cmd.UI.TranslateText("reason:"), updatedStack.StateReason}) + } + + cmd.UI.DisplayKeyValueTable("", displayTable, 3) return nil } diff --git a/command/v7/update_stack_command_test.go b/command/v7/update_stack_command_test.go index e3d4f63e60..71a2f16926 100644 --- a/command/v7/update_stack_command_test.go +++ b/command/v7/update_stack_command_test.go @@ -51,7 +51,7 @@ var _ = Describe("update-stack Command", func() { binaryName = "faceman" fakeConfig.BinaryNameReturns(binaryName) - fakeConfig.APIVersionReturns("3.210.0") + fakeConfig.APIVersionReturns("3.211.0") }) Context("When the environment is not setup correctly", func() { @@ -149,51 +149,76 @@ var _ = Describe("update-stack Command", func() { Expect(fakeActor.GetStackByNameArgsForCall(0)).To(Equal("some-stack")) Expect(fakeActor.UpdateStackCallCount()).To(Equal(1)) - guid, state := fakeActor.UpdateStackArgsForCall(0) + guid, state, reason := fakeActor.UpdateStackArgsForCall(0) Expect(guid).To(Equal("stack-guid")) Expect(state).To(Equal(resources.StackStateDeprecated)) + Expect(reason).To(Equal("")) }) }) }) }) - Context("when state values are provided in different cases", func() { - It("accepts 'active' and capitalizes it", func() { - cmd.State = "active" - fakeActor.GetStackByNameReturns(resources.Stack{GUID: "guid"}, v7action.Warnings{}, nil) - fakeActor.UpdateStackReturns(resources.Stack{Name: "some-stack", State: resources.StackStateActive}, v7action.Warnings{}, nil) + Context("when state values are provided in different cases", func() { + It("accepts 'active' and capitalizes it", func() { + cmd.State = "active" + fakeActor.GetStackByNameReturns(resources.Stack{GUID: "guid"}, v7action.Warnings{}, nil) + fakeActor.UpdateStackReturns(resources.Stack{Name: "some-stack", State: resources.StackStateActive}, v7action.Warnings{}, nil) - executeErr = cmd.Execute(args) + executeErr = cmd.Execute(args) - Expect(executeErr).ToNot(HaveOccurred()) - _, state := fakeActor.UpdateStackArgsForCall(0) - Expect(state).To(Equal(resources.StackStateActive)) - }) + Expect(executeErr).ToNot(HaveOccurred()) + _, state, _ := fakeActor.UpdateStackArgsForCall(0) + Expect(state).To(Equal(resources.StackStateActive)) + }) - It("accepts 'RESTRICTED' and keeps it capitalized", func() { - cmd.State = "RESTRICTED" - fakeActor.GetStackByNameReturns(resources.Stack{GUID: "guid"}, v7action.Warnings{}, nil) - fakeActor.UpdateStackReturns(resources.Stack{Name: "some-stack", State: resources.StackStateRestricted}, v7action.Warnings{}, nil) + It("accepts 'RESTRICTED' and keeps it capitalized", func() { + cmd.State = "RESTRICTED" + fakeActor.GetStackByNameReturns(resources.Stack{GUID: "guid"}, v7action.Warnings{}, nil) + fakeActor.UpdateStackReturns(resources.Stack{Name: "some-stack", State: resources.StackStateRestricted}, v7action.Warnings{}, nil) - executeErr = cmd.Execute(args) + executeErr = cmd.Execute(args) - Expect(executeErr).ToNot(HaveOccurred()) - _, state := fakeActor.UpdateStackArgsForCall(0) - Expect(state).To(Equal(resources.StackStateRestricted)) - }) + Expect(executeErr).ToNot(HaveOccurred()) + _, state, _ := fakeActor.UpdateStackArgsForCall(0) + Expect(state).To(Equal(resources.StackStateRestricted)) + }) - It("accepts 'Disabled' and capitalizes it", func() { - cmd.State = "Disabled" - fakeActor.GetStackByNameReturns(resources.Stack{GUID: "guid"}, v7action.Warnings{}, nil) - fakeActor.UpdateStackReturns(resources.Stack{Name: "some-stack", State: resources.StackStateDisabled}, v7action.Warnings{}, nil) + It("accepts 'Disabled' and capitalizes it", func() { + cmd.State = "Disabled" + fakeActor.GetStackByNameReturns(resources.Stack{GUID: "guid"}, v7action.Warnings{}, nil) + fakeActor.UpdateStackReturns(resources.Stack{Name: "some-stack", State: resources.StackStateDisabled}, v7action.Warnings{}, nil) - executeErr = cmd.Execute(args) + executeErr = cmd.Execute(args) - Expect(executeErr).ToNot(HaveOccurred()) - _, state := fakeActor.UpdateStackArgsForCall(0) - Expect(state).To(Equal(resources.StackStateDisabled)) - }) + Expect(executeErr).ToNot(HaveOccurred()) + _, state, _ := fakeActor.UpdateStackArgsForCall(0) + Expect(state).To(Equal(resources.StackStateDisabled)) + }) + }) + + Context("when the reason flag is provided", func() { + BeforeEach(func() { + cmd.State = "deprecated" + cmd.Reason = "Use cflinuxfs4 instead" + fakeActor.GetStackByNameReturns(resources.Stack{GUID: "guid"}, v7action.Warnings{}, nil) + fakeActor.UpdateStackReturns(resources.Stack{ + Name: "some-stack", + Description: "some description", + State: resources.StackStateDeprecated, + StateReason: "Use cflinuxfs4 instead", + }, v7action.Warnings{}, nil) }) + + It("passes the reason to the actor and displays it", func() { + Expect(executeErr).ToNot(HaveOccurred()) + + Expect(fakeActor.UpdateStackCallCount()).To(Equal(1)) + _, _, reason := fakeActor.UpdateStackArgsForCall(0) + Expect(reason).To(Equal("Use cflinuxfs4 instead")) + + Expect(testUI.Out).To(Say(`reason:\s+Use cflinuxfs4 instead`)) + }) + }) }) }) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 46e5bcbc23..fdd4d54254 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -3619,11 +3619,12 @@ type FakeActor struct { result1 v7action.Warnings result2 error } - UpdateStackStub func(string, string) (resources.Stack, v7action.Warnings, error) + UpdateStackStub func(string, string, string) (resources.Stack, v7action.Warnings, error) updateStackMutex sync.RWMutex updateStackArgsForCall []struct { arg1 string arg2 string + arg3 string } updateStackReturns struct { result1 resources.Stack @@ -19525,19 +19526,20 @@ func (fake *FakeActor) UpdateSpaceQuotaReturnsOnCall(i int, result1 v7action.War }{result1, result2} } -func (fake *FakeActor) UpdateStack(arg1 string, arg2 string) (resources.Stack, v7action.Warnings, error) { +func (fake *FakeActor) UpdateStack(arg1 string, arg2 string, arg3 string) (resources.Stack, v7action.Warnings, error) { fake.updateStackMutex.Lock() ret, specificReturn := fake.updateStackReturnsOnCall[len(fake.updateStackArgsForCall)] fake.updateStackArgsForCall = append(fake.updateStackArgsForCall, struct { arg1 string arg2 string - }{arg1, arg2}) + arg3 string + }{arg1, arg2, arg3}) stub := fake.UpdateStackStub fakeReturns := fake.updateStackReturns - fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2}) + fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2, arg3}) fake.updateStackMutex.Unlock() if stub != nil { - return stub(arg1, arg2) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2, ret.result3 @@ -19551,17 +19553,17 @@ func (fake *FakeActor) UpdateStackCallCount() int { return len(fake.updateStackArgsForCall) } -func (fake *FakeActor) UpdateStackCalls(stub func(string, string) (resources.Stack, v7action.Warnings, error)) { +func (fake *FakeActor) UpdateStackCalls(stub func(string, string, string) (resources.Stack, v7action.Warnings, error)) { fake.updateStackMutex.Lock() defer fake.updateStackMutex.Unlock() fake.UpdateStackStub = stub } -func (fake *FakeActor) UpdateStackArgsForCall(i int) (string, string) { +func (fake *FakeActor) UpdateStackArgsForCall(i int) (string, string, string) { fake.updateStackMutex.RLock() defer fake.updateStackMutex.RUnlock() argsForCall := fake.updateStackArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeActor) UpdateStackReturns(result1 resources.Stack, result2 v7action.Warnings, result3 error) { diff --git a/integration/v7/isolated/stack_command_test.go b/integration/v7/isolated/stack_command_test.go index 6f819fd00c..4ef62c356b 100644 --- a/integration/v7/isolated/stack_command_test.go +++ b/integration/v7/isolated/stack_command_test.go @@ -124,6 +124,15 @@ var _ = Describe("stack command", func() { Eventually(session).Should(Say(`name:\s+%s`, stackName)) Eventually(session).Should(Say(`description:\s+%s`, stackDescription)) Eventually(session).Should(Say(`state:\s+ACTIVE`)) + Consistently(session).ShouldNot(Say(`reason:`)) + Eventually(session).Should(Exit(0)) + }) + + It("does not show reason for an active stack", func() { + session := helpers.CF("stack", stackName) + + Eventually(session).Should(Say(`state:\s+ACTIVE`)) + Consistently(session).ShouldNot(Say(`reason:`)) Eventually(session).Should(Exit(0)) }) @@ -137,6 +146,42 @@ var _ = Describe("stack command", func() { Eventually(session).Should(Say(`^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}`)) Eventually(session).Should(Exit(0)) }) + + When("the stack is in a non-active state without a reason", func() { + BeforeEach(func() { + session := helpers.CF("update-stack", stackName, "--state", "deprecated") + Eventually(session).Should(Exit(0)) + }) + + It("shows an empty reason field", func() { + session := helpers.CF("stack", stackName) + + Eventually(session).Should(Say(`Getting info for stack %s as %s\.\.\.`, stackName, username)) + Eventually(session).Should(Say(`name:\s+%s`, stackName)) + Eventually(session).Should(Say(`description:\s+%s`, stackDescription)) + Eventually(session).Should(Say(`state:\s+DEPRECATED`)) + Eventually(session).Should(Say(`reason:\s*$`)) + Eventually(session).Should(Exit(0)) + }) + }) + + When("the stack is in a non-active state with a reason", func() { + BeforeEach(func() { + session := helpers.CF("update-stack", stackName, "--state", "disabled", "--reason", "This stack is no longer supported.") + Eventually(session).Should(Exit(0)) + }) + + It("shows the reason in the output", func() { + session := helpers.CF("stack", stackName) + + Eventually(session).Should(Say(`Getting info for stack %s as %s\.\.\.`, stackName, username)) + Eventually(session).Should(Say(`name:\s+%s`, stackName)) + Eventually(session).Should(Say(`description:\s+%s`, stackDescription)) + Eventually(session).Should(Say(`state:\s+DISABLED`)) + Eventually(session).Should(Say(`reason:\s+This stack is no longer supported\.`)) + Eventually(session).Should(Exit(0)) + }) + }) }) }) }) diff --git a/integration/v7/isolated/update_stack_command_test.go b/integration/v7/isolated/update_stack_command_test.go index cd8b227b96..711d8a31c4 100644 --- a/integration/v7/isolated/update_stack_command_test.go +++ b/integration/v7/isolated/update_stack_command_test.go @@ -44,9 +44,10 @@ var _ = Describe("update-stack command", func() { Eventually(session).Should(Say(`NAME:`)) Eventually(session).Should(Say(`update-stack - Transition a stack between the defined states`)) Eventually(session).Should(Say(`USAGE:`)) - Eventually(session).Should(Say(`cf update-stack STACK_NAME \[--state \(active \| restricted \| deprecated \| disabled\)\]`)) + Eventually(session).Should(Say(`cf update-stack STACK_NAME \[--state \(active \| restricted \| deprecated \| disabled\)\] \[--reason text\]`)) Eventually(session).Should(Say(`EXAMPLES:`)) Eventually(session).Should(Say(`cf update-stack cflinuxfs3 --state disabled`)) + Eventually(session).Should(Say(`cf update-stack cflinuxfs3 --state deprecated --reason "This stack is based on Ubuntu 18.04, which is no longer supported. Please migrate your applications to 'cflinuxfs4'. For more information, see: ."`)) Eventually(session).Should(Say(`OPTIONS:`)) Eventually(session).Should(Say(`--state\s+State to transition the stack to`)) Eventually(session).Should(Say(`SEE ALSO:`)) @@ -206,6 +207,32 @@ var _ = Describe("update-stack command", func() { }) }) + When("updating to a non-active state without a reason", func() { + It("shows an empty reason field in the output", func() { + session := helpers.CF("update-stack", stackName, "--state", "deprecated") + + Eventually(session).Should(Say(`Updating stack %s as %s\.\.\.`, stackName, username)) + Eventually(session).Should(Say("OK")) + Eventually(session).Should(Say(`name:\s+%s`, stackName)) + Eventually(session).Should(Say(`state:\s+DEPRECATED`)) + Eventually(session).Should(Say(`reason:\s*$`)) + Eventually(session).Should(Exit(0)) + }) + }) + + When("updating with a reason", func() { + It("shows the reason in the update-stack output", func() { + session := helpers.CF("update-stack", stackName, "--state", "disabled", "--reason", "This stack is no longer supported.") + + Eventually(session).Should(Say(`Updating stack %s as %s\.\.\.`, stackName, username)) + Eventually(session).Should(Say("OK")) + Eventually(session).Should(Say(`name:\s+%s`, stackName)) + Eventually(session).Should(Say(`state:\s+DISABLED`)) + Eventually(session).Should(Say(`reason:\s+This stack is no longer supported\.`)) + Eventually(session).Should(Exit(0)) + }) + }) + When("state value is provided in different cases", func() { It("accepts lowercase state value", func() { session := helpers.CF("update-stack", stackName, "--state", "deprecated") diff --git a/resources/stack_resource.go b/resources/stack_resource.go index 049fb187b5..395c5896a0 100644 --- a/resources/stack_resource.go +++ b/resources/stack_resource.go @@ -36,6 +36,8 @@ type Stack struct { Description string `json:"description"` // State is the state of the stack (ACTIVE, RESTRICTED, DEPRECATED, DISABLED) State string `json:"state,omitempty"` + // StateReason is the reason for the current state + StateReason string `json:"state_reason,omitempty"` // Metadata is used for custom tagging of API resources Metadata *Metadata `json:"metadata,omitempty"`