From a60275e1b75493676654160840b8055f19f77423 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Mon, 1 Dec 2025 14:38:03 -0500 Subject: [PATCH 01/19] Add state field to cf stacks command Signed-off-by: Simon Jones --- actor/v7action/stack_test.go | 67 +-------------- command/v7/stack_command.go | 18 +++- command/v7/stack_command_test.go | 25 ++++-- integration/v7/isolated/stack_command_test.go | 83 +++++++++++++------ 4 files changed, 91 insertions(+), 102 deletions(-) diff --git a/actor/v7action/stack_test.go b/actor/v7action/stack_test.go index f401f421ee..5d5a59637c 100644 --- a/actor/v7action/stack_test.go +++ b/actor/v7action/stack_test.go @@ -134,7 +134,7 @@ var _ = Describe("Stack", func() { Expect(stack.GUID).To(Equal(expectedStack.GUID)) Expect(stack.Name).To(Equal(expectedStack.Name)) Expect(stack.Description).To(Equal(expectedStack.Description)) - Expect(stack.State).To(Equal(resources.StackStateActive)) + Expect(stack.State).To(Equal("ACTIVE")) Expect(err).To(BeNil()) Expect(warnings).To(ConsistOf("warning-1", "warning-2")) }) @@ -233,69 +233,4 @@ var _ = Describe("Stack", func() { }) }) }) - - Describe("UpdateStack", func() { - var ( - stackGUID string - state string - stack resources.Stack - warnings Warnings - executeErr error - ) - - BeforeEach(func() { - stackGUID = "some-stack-guid" - state = "DEPRECATED" - }) - - JustBeforeEach(func() { - stack, warnings, executeErr = actor.UpdateStack(stackGUID, state) - }) - - When("the cloud controller request is successful", func() { - BeforeEach(func() { - fakeCloudControllerClient.UpdateStackReturns( - resources.Stack{ - GUID: "some-stack-guid", - Name: "some-stack", - Description: "some description", - State: "DEPRECATED", - }, - ccv3.Warnings{"warning-1", "warning-2"}, - nil, - ) - }) - - It("returns the updated stack and warnings", func() { - Expect(executeErr).ToNot(HaveOccurred()) - Expect(warnings).To(ConsistOf("warning-1", "warning-2")) - Expect(stack).To(Equal(resources.Stack{ - GUID: "some-stack-guid", - Name: "some-stack", - Description: "some description", - State: "DEPRECATED", - })) - - Expect(fakeCloudControllerClient.UpdateStackCallCount()).To(Equal(1)) - actualGUID, actualState := fakeCloudControllerClient.UpdateStackArgsForCall(0) - Expect(actualGUID).To(Equal(stackGUID)) - Expect(actualState).To(Equal(state)) - }) - }) - - When("the cloud controller request fails", func() { - BeforeEach(func() { - fakeCloudControllerClient.UpdateStackReturns( - resources.Stack{}, - ccv3.Warnings{"warning-1"}, - errors.New("some-error"), - ) - }) - - It("returns the error and warnings", func() { - Expect(executeErr).To(MatchError("some-error")) - Expect(warnings).To(ConsistOf("warning-1")) - }) - }) - }) }) diff --git a/command/v7/stack_command.go b/command/v7/stack_command.go index d451fa9d49..7e8f018dff 100644 --- a/command/v7/stack_command.go +++ b/command/v7/stack_command.go @@ -2,6 +2,7 @@ package v7 import ( "code.cloudfoundry.org/cli/v8/command/flag" + "code.cloudfoundry.org/cli/v8/command/translatableerror" "code.cloudfoundry.org/cli/v8/resources" ) @@ -11,7 +12,7 @@ type StackCommand struct { RequiredArgs flag.StackName `positional-args:"yes"` GUID bool `long:"guid" description:"Retrieve and display the given stack's guid. All other output for the stack is suppressed."` usage interface{} `usage:"CF_NAME stack STACK_NAME"` - relatedCommands interface{} `related_commands:"app, push, stacks, update-stack"` + relatedCommands interface{} `related_commands:"app, push, stacks"` } func (cmd *StackCommand) Execute(args []string) error { @@ -60,6 +61,21 @@ func (cmd *StackCommand) displayStackInfo() error { return err } + // Validate state if present + if stack.State != "" { + validStates := []string{"ACTIVE", "RESTRICTED", "DEPRECATED", "DISABLED"} + isValid := false + for _, validState := range validStates { + if stack.State == validState { + isValid = true + break + } + } + if !isValid { + return translatableerror.InvalidStackStateError{State: stack.State} + } + } + // Build display table displayTable := [][]string{ {cmd.UI.TranslateText("name:"), stack.Name}, diff --git a/command/v7/stack_command_test.go b/command/v7/stack_command_test.go index 643cd162c3..096d92a055 100644 --- a/command/v7/stack_command_test.go +++ b/command/v7/stack_command_test.go @@ -141,14 +141,10 @@ var _ = Describe("Stack Command", 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).NotTo(Say("state:")) }) }) - Context("When the stack has a state", func() { + Context("When the stack has a valid state", func() { BeforeEach(func() { stack := resources.Stack{ Name: "some-stack-name", @@ -163,10 +159,23 @@ var _ = Describe("Stack Command", 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+ACTIVE")) + Context("When the stack has an invalid state", func() { + BeforeEach(func() { + stack := resources.Stack{ + Name: "some-stack-name", + GUID: "some-stack-guid", + Description: "some-stack-desc", + State: "INVALID_STATE", + } + fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil) + }) + + It("returns an error", func() { + Expect(executeErr).To(HaveOccurred()) + Expect(executeErr.Error()).To(ContainSubstring("Invalid stack state")) }) }) diff --git a/integration/v7/isolated/stack_command_test.go b/integration/v7/isolated/stack_command_test.go index 6f819fd00c..98c8f138df 100644 --- a/integration/v7/isolated/stack_command_test.go +++ b/integration/v7/isolated/stack_command_test.go @@ -33,7 +33,7 @@ var _ = Describe("stack command", func() { It("appears in cf help -a", func() { session := helpers.CF("help", "-a") Eventually(session).Should(Exit(0)) - Expect(session).To(HaveCommandInCategoryWithDescription("stack", "APPS", "Show information for a stack (a stack is a pre-built file system, including an operating system, that can run apps) and current state")) + Expect(session).To(HaveCommandInCategoryWithDescription("stack", "APPS", "Show information for a stack (a stack is a pre-built file system, including an operating system, that can run apps)")) }) It("Displays command usage to output", func() { @@ -100,42 +100,71 @@ var _ = Describe("stack command", func() { }) }) - When("the stack exists with valid state", func() { + When("the stack exists", func() { var stackGUID string - BeforeEach(func() { - jsonBody := fmt.Sprintf(`{"name": "%s", "description": "%s", "state": "ACTIVE"}`, stackName, stackDescription) - session := helpers.CF("curl", "-d", jsonBody, "-X", "POST", "/v3/stacks") - Eventually(session).Should(Exit(0)) + Context("when the stack has no state", func() { + BeforeEach(func() { + jsonBody := fmt.Sprintf(`{"name": "%s", "description": "%s"}`, stackName, stackDescription) + session := helpers.CF("curl", "-d", jsonBody, "-X", "POST", "/v3/stacks") + Eventually(session).Should(Exit(0)) - r := regexp.MustCompile(`[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}`) - stackGUID = string(r.Find(session.Out.Contents())) - }) + r := regexp.MustCompile(`[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}`) + stackGUID = string(r.Find(session.Out.Contents())) + }) - AfterEach(func() { - session := helpers.CF("curl", "-X", "DELETE", fmt.Sprintf("/v3/stacks/%s", stackGUID)) - Eventually(session).Should(Exit(0)) + AfterEach(func() { + session := helpers.CF("curl", "-X", "DELETE", fmt.Sprintf("/v3/stacks/%s", stackGUID)) + Eventually(session).Should(Exit(0)) + }) + + It("Shows the details for the stack without state", 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)) + Consistently(session).ShouldNot(Say(`state:`)) + Eventually(session).Should(Exit(0)) + }) }) - It("Shows the details for the stack with state", func() { - session := helpers.CF("stack", stackName) + Context("when the stack has a valid state", func() { + BeforeEach(func() { + jsonBody := fmt.Sprintf(`{"name": "%s", "description": "%s", "state": "ACTIVE"}`, stackName, stackDescription) + session := helpers.CF("curl", "-d", jsonBody, "-X", "POST", "/v3/stacks") + Eventually(session).Should(Exit(0)) - 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+ACTIVE`)) - Eventually(session).Should(Exit(0)) + r := regexp.MustCompile(`[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}`) + stackGUID = string(r.Find(session.Out.Contents())) + }) + + AfterEach(func() { + session := helpers.CF("curl", "-X", "DELETE", fmt.Sprintf("/v3/stacks/%s", stackGUID)) + Eventually(session).Should(Exit(0)) + }) + + It("Shows the details for the stack with state", 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+ACTIVE`)) + Eventually(session).Should(Exit(0)) + }) }) - It("prints nothing but the guid when --guid flag is passed", func() { - session := helpers.CF("stack", stackName, "--guid") + When("the stack exists and the --guid flag is passed", func() { + It("prints nothing but the guid", func() { + session := helpers.CF("stack", stackName, "--guid") - Consistently(session).ShouldNot(Say(`Getting info for stack %s as %s\.\.\.`, stackName, username)) - Consistently(session).ShouldNot(Say(`name:\s+%s`, stackName)) - Consistently(session).ShouldNot(Say(`description:\s+%s`, stackDescription)) - Consistently(session).ShouldNot(Say(`state:`)) - 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)) + Consistently(session).ShouldNot(Say(`Getting info for stack %s as %s\.\.\.`, stackName, username)) + Consistently(session).ShouldNot(Say(`name:\s+%s`, stackName)) + Consistently(session).ShouldNot(Say(`description:\s+%s`, stackDescription)) + 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)) + }) }) }) }) From a9caa97138dbf4e1347937d957e9cb1a0da734fd Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Tue, 2 Dec 2025 11:46:22 -0500 Subject: [PATCH 02/19] Remove state validation from cf state command Signed-off-by: Simon Jones --- command/v7/stack_command.go | 16 ---------------- command/v7/stack_command_test.go | 19 +------------------ 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/command/v7/stack_command.go b/command/v7/stack_command.go index 7e8f018dff..910902c61a 100644 --- a/command/v7/stack_command.go +++ b/command/v7/stack_command.go @@ -2,7 +2,6 @@ package v7 import ( "code.cloudfoundry.org/cli/v8/command/flag" - "code.cloudfoundry.org/cli/v8/command/translatableerror" "code.cloudfoundry.org/cli/v8/resources" ) @@ -61,21 +60,6 @@ func (cmd *StackCommand) displayStackInfo() error { return err } - // Validate state if present - if stack.State != "" { - validStates := []string{"ACTIVE", "RESTRICTED", "DEPRECATED", "DISABLED"} - isValid := false - for _, validState := range validStates { - if stack.State == validState { - isValid = true - break - } - } - if !isValid { - return translatableerror.InvalidStackStateError{State: stack.State} - } - } - // Build display table displayTable := [][]string{ {cmd.UI.TranslateText("name:"), stack.Name}, diff --git a/command/v7/stack_command_test.go b/command/v7/stack_command_test.go index 096d92a055..32568dd6a7 100644 --- a/command/v7/stack_command_test.go +++ b/command/v7/stack_command_test.go @@ -144,7 +144,7 @@ var _ = Describe("Stack Command", func() { }) }) - Context("When the stack has a valid state", func() { + Context("When the stack has a state", func() { BeforeEach(func() { stack := resources.Stack{ Name: "some-stack-name", @@ -162,23 +162,6 @@ var _ = Describe("Stack Command", func() { }) }) - Context("When the stack has an invalid state", func() { - BeforeEach(func() { - stack := resources.Stack{ - Name: "some-stack-name", - GUID: "some-stack-guid", - Description: "some-stack-desc", - State: "INVALID_STATE", - } - fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil) - }) - - It("returns an error", func() { - Expect(executeErr).To(HaveOccurred()) - Expect(executeErr.Error()).To(ContainSubstring("Invalid stack state")) - }) - }) - When("The Stack does not Exist", func() { expectedError := actionerror.StackNotFoundError{Name: "some-stack-name"} BeforeEach(func() { From f905efa64ae9848b438abe6a7b6195057d8a7ed4 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Mon, 8 Dec 2025 13:08:50 -0500 Subject: [PATCH 03/19] first pass of update-stack command Signed-off-by: Simon Jones --- actor/v7action/stack_test.go | 67 ++++++++++++++- .../fake_cloud_controller_client.go | 84 +++++++++++++++++++ api/cloudcontroller/ccv3/stack_test.go | 6 +- command/v7/stack_command.go | 2 +- command/v7/update_stack_command.go | 11 +-- command/v7/update_stack_command_test.go | 57 +++++++------ command/v7/v7fakes/fake_actor.go | 84 +++++++++++++++++++ resources/stack_resource.go | 5 ++ 8 files changed, 273 insertions(+), 43 deletions(-) diff --git a/actor/v7action/stack_test.go b/actor/v7action/stack_test.go index 5d5a59637c..f401f421ee 100644 --- a/actor/v7action/stack_test.go +++ b/actor/v7action/stack_test.go @@ -134,7 +134,7 @@ var _ = Describe("Stack", func() { Expect(stack.GUID).To(Equal(expectedStack.GUID)) Expect(stack.Name).To(Equal(expectedStack.Name)) Expect(stack.Description).To(Equal(expectedStack.Description)) - Expect(stack.State).To(Equal("ACTIVE")) + Expect(stack.State).To(Equal(resources.StackStateActive)) Expect(err).To(BeNil()) Expect(warnings).To(ConsistOf("warning-1", "warning-2")) }) @@ -233,4 +233,69 @@ var _ = Describe("Stack", func() { }) }) }) + + Describe("UpdateStack", func() { + var ( + stackGUID string + state string + stack resources.Stack + warnings Warnings + executeErr error + ) + + BeforeEach(func() { + stackGUID = "some-stack-guid" + state = "DEPRECATED" + }) + + JustBeforeEach(func() { + stack, warnings, executeErr = actor.UpdateStack(stackGUID, state) + }) + + When("the cloud controller request is successful", func() { + BeforeEach(func() { + fakeCloudControllerClient.UpdateStackReturns( + resources.Stack{ + GUID: "some-stack-guid", + Name: "some-stack", + Description: "some description", + State: "DEPRECATED", + }, + ccv3.Warnings{"warning-1", "warning-2"}, + nil, + ) + }) + + It("returns the updated stack and warnings", func() { + Expect(executeErr).ToNot(HaveOccurred()) + Expect(warnings).To(ConsistOf("warning-1", "warning-2")) + Expect(stack).To(Equal(resources.Stack{ + GUID: "some-stack-guid", + Name: "some-stack", + Description: "some description", + State: "DEPRECATED", + })) + + Expect(fakeCloudControllerClient.UpdateStackCallCount()).To(Equal(1)) + actualGUID, actualState := fakeCloudControllerClient.UpdateStackArgsForCall(0) + Expect(actualGUID).To(Equal(stackGUID)) + Expect(actualState).To(Equal(state)) + }) + }) + + When("the cloud controller request fails", func() { + BeforeEach(func() { + fakeCloudControllerClient.UpdateStackReturns( + resources.Stack{}, + ccv3.Warnings{"warning-1"}, + errors.New("some-error"), + ) + }) + + It("returns the error and warnings", func() { + Expect(executeErr).To(MatchError("some-error")) + Expect(warnings).To(ConsistOf("warning-1")) + }) + }) + }) }) diff --git a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go index b8abbda3f6..c682d4f262 100644 --- a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go +++ b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go @@ -1912,6 +1912,22 @@ type FakeCloudControllerClient struct { result2 ccv3.Warnings result3 error } + UpdateStackStub func(string, string) (resources.Stack, ccv3.Warnings, error) + updateStackMutex sync.RWMutex + updateStackArgsForCall []struct { + arg1 string + arg2 string + } + updateStackReturns struct { + result1 resources.Stack + result2 ccv3.Warnings + result3 error + } + updateStackReturnsOnCall map[int]struct { + result1 resources.Stack + result2 ccv3.Warnings + result3 error + } GetStagingSecurityGroupsStub func(string, ...ccv3.Query) ([]resources.SecurityGroup, ccv3.Warnings, error) getStagingSecurityGroupsMutex sync.RWMutex getStagingSecurityGroupsArgsForCall []struct { @@ -11214,6 +11230,74 @@ func (fake *FakeCloudControllerClient) GetStacksReturnsOnCall(i int, result1 []r }{result1, result2, result3} } +func (fake *FakeCloudControllerClient) UpdateStack(arg1 string, arg2 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}) + stub := fake.UpdateStackStub + fakeReturns := fake.updateStackReturns + fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2}) + fake.updateStackMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeCloudControllerClient) UpdateStackCallCount() int { + fake.updateStackMutex.RLock() + defer fake.updateStackMutex.RUnlock() + return len(fake.updateStackArgsForCall) +} + +func (fake *FakeCloudControllerClient) UpdateStackCalls(stub func(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) { + fake.updateStackMutex.RLock() + defer fake.updateStackMutex.RUnlock() + argsForCall := fake.updateStackArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeCloudControllerClient) UpdateStackReturns(result1 resources.Stack, result2 ccv3.Warnings, result3 error) { + fake.updateStackMutex.Lock() + defer fake.updateStackMutex.Unlock() + fake.UpdateStackStub = nil + fake.updateStackReturns = struct { + result1 resources.Stack + result2 ccv3.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeCloudControllerClient) UpdateStackReturnsOnCall(i int, result1 resources.Stack, result2 ccv3.Warnings, result3 error) { + fake.updateStackMutex.Lock() + defer fake.updateStackMutex.Unlock() + fake.UpdateStackStub = nil + if fake.updateStackReturnsOnCall == nil { + fake.updateStackReturnsOnCall = make(map[int]struct { + result1 resources.Stack + result2 ccv3.Warnings + result3 error + }) + } + fake.updateStackReturnsOnCall[i] = struct { + result1 resources.Stack + result2 ccv3.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeCloudControllerClient) GetStagingSecurityGroups(arg1 string, arg2 ...ccv3.Query) ([]resources.SecurityGroup, ccv3.Warnings, error) { fake.getStagingSecurityGroupsMutex.Lock() ret, specificReturn := fake.getStagingSecurityGroupsReturnsOnCall[len(fake.getStagingSecurityGroupsArgsForCall)] diff --git a/api/cloudcontroller/ccv3/stack_test.go b/api/cloudcontroller/ccv3/stack_test.go index 447eed4302..2f2cea90da 100644 --- a/api/cloudcontroller/ccv3/stack_test.go +++ b/api/cloudcontroller/ccv3/stack_test.go @@ -155,7 +155,7 @@ var _ = Describe("Stacks", func() { BeforeEach(func() { stackGUID = "some-stack-guid" - state = "DEPRECATED" + state = resources.StackStateDeprecated }) JustBeforeEach(func() { @@ -168,7 +168,7 @@ var _ = Describe("Stacks", func() { CombineHandlers( VerifyRequest(http.MethodPatch, "/v3/stacks/some-stack-guid"), VerifyJSONRepresenting(map[string]string{ - "state": "DEPRECATED", + "state": resources.StackStateDeprecated, }), RespondWith(http.StatusOK, `{ "guid": "some-stack-guid", @@ -187,7 +187,7 @@ var _ = Describe("Stacks", func() { GUID: "some-stack-guid", Name: "some-stack", Description: "some description", - State: "DEPRECATED", + State: resources.StackStateDeprecated, })) }) }) diff --git a/command/v7/stack_command.go b/command/v7/stack_command.go index 910902c61a..d451fa9d49 100644 --- a/command/v7/stack_command.go +++ b/command/v7/stack_command.go @@ -11,7 +11,7 @@ type StackCommand struct { RequiredArgs flag.StackName `positional-args:"yes"` GUID bool `long:"guid" description:"Retrieve and display the given stack's guid. All other output for the stack is suppressed."` usage interface{} `usage:"CF_NAME stack STACK_NAME"` - relatedCommands interface{} `related_commands:"app, push, stacks"` + relatedCommands interface{} `related_commands:"app, push, stacks, update-stack"` } func (cmd *StackCommand) Execute(args []string) error { diff --git a/command/v7/update_stack_command.go b/command/v7/update_stack_command.go index 35e2f86c39..561093407d 100644 --- a/command/v7/update_stack_command.go +++ b/command/v7/update_stack_command.go @@ -4,8 +4,6 @@ import ( "slices" "strings" - "code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccversion" - "code.cloudfoundry.org/cli/v8/command" "code.cloudfoundry.org/cli/v8/command/flag" "code.cloudfoundry.org/cli/v8/resources" ) @@ -15,17 +13,12 @@ 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"` + usage interface{} `usage:"CF_NAME update-stack STACK_NAME [--state active|restricted|deprecated|disabled]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled"` relatedCommands interface{} `related_commands:"stack, stacks"` } func (cmd UpdateStackCommand) Execute(args []string) error { - err := command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionUpdateStack) - if err != nil { - return err - } - - err = cmd.SharedActor.CheckTarget(false, false) + err := cmd.SharedActor.CheckTarget(false, false) if err != nil { return err } diff --git a/command/v7/update_stack_command_test.go b/command/v7/update_stack_command_test.go index e3d4f63e60..3ecb4626f5 100644 --- a/command/v7/update_stack_command_test.go +++ b/command/v7/update_stack_command_test.go @@ -51,7 +51,6 @@ var _ = Describe("update-stack Command", func() { binaryName = "faceman" fakeConfig.BinaryNameReturns(binaryName) - fakeConfig.APIVersionReturns("3.210.0") }) Context("When the environment is not setup correctly", func() { @@ -157,43 +156,43 @@ var _ = Describe("update-stack Command", func() { }) }) - 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)) }) }) + }) }) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 46e5bcbc23..131dd1e61d 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -3649,6 +3649,22 @@ type FakeActor struct { result1 v7action.Warnings result2 error } + UpdateStackStub func(string, string) (resources.Stack, v7action.Warnings, error) + updateStackMutex sync.RWMutex + updateStackArgsForCall []struct { + arg1 string + arg2 string + } + updateStackReturns struct { + result1 resources.Stack + result2 v7action.Warnings + result3 error + } + updateStackReturnsOnCall map[int]struct { + result1 resources.Stack + result2 v7action.Warnings + result3 error + } UpdateUserPasswordStub func(string, string, string) error updateUserPasswordMutex sync.RWMutex updateUserPasswordArgsForCall []struct { @@ -19658,6 +19674,74 @@ func (fake *FakeActor) UpdateStackLabelsByStackNameReturnsOnCall(i int, result1 }{result1, result2} } +func (fake *FakeActor) UpdateStack(arg1 string, arg2 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}) + stub := fake.UpdateStackStub + fakeReturns := fake.updateStackReturns + fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2}) + fake.updateStackMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeActor) UpdateStackCallCount() int { + fake.updateStackMutex.RLock() + defer fake.updateStackMutex.RUnlock() + return len(fake.updateStackArgsForCall) +} + +func (fake *FakeActor) UpdateStackCalls(stub func(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) { + fake.updateStackMutex.RLock() + defer fake.updateStackMutex.RUnlock() + argsForCall := fake.updateStackArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeActor) UpdateStackReturns(result1 resources.Stack, result2 v7action.Warnings, result3 error) { + fake.updateStackMutex.Lock() + defer fake.updateStackMutex.Unlock() + fake.UpdateStackStub = nil + fake.updateStackReturns = struct { + result1 resources.Stack + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeActor) UpdateStackReturnsOnCall(i int, result1 resources.Stack, result2 v7action.Warnings, result3 error) { + fake.updateStackMutex.Lock() + defer fake.updateStackMutex.Unlock() + fake.UpdateStackStub = nil + if fake.updateStackReturnsOnCall == nil { + fake.updateStackReturnsOnCall = make(map[int]struct { + result1 resources.Stack + result2 v7action.Warnings + result3 error + }) + } + fake.updateStackReturnsOnCall[i] = struct { + result1 resources.Stack + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeActor) UpdateUserPassword(arg1 string, arg2 string, arg3 string) error { fake.updateUserPasswordMutex.Lock() ret, specificReturn := fake.updateUserPasswordReturnsOnCall[len(fake.updateUserPasswordArgsForCall)] diff --git a/resources/stack_resource.go b/resources/stack_resource.go index 049fb187b5..a26746b021 100644 --- a/resources/stack_resource.go +++ b/resources/stack_resource.go @@ -27,6 +27,11 @@ func ValidStackStatesLowercase() []string { return lowercase } +// ValidStackStatesString returns a pipe-separated string of valid states in lowercase +func ValidStackStatesString() string { + return strings.Join(ValidStackStatesLowercase(), "|") +} + type Stack struct { // GUID is a unique stack identifier. GUID string `json:"guid"` From 01338d06cc6382039849324d61145ac0c0b8e060 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Tue, 9 Dec 2025 12:19:39 -0500 Subject: [PATCH 04/19] Include reference to state in help text for cf stack & stacks Signed-off-by: Simon Jones --- api/cloudcontroller/ccv3/stack_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/cloudcontroller/ccv3/stack_test.go b/api/cloudcontroller/ccv3/stack_test.go index 2f2cea90da..447eed4302 100644 --- a/api/cloudcontroller/ccv3/stack_test.go +++ b/api/cloudcontroller/ccv3/stack_test.go @@ -155,7 +155,7 @@ var _ = Describe("Stacks", func() { BeforeEach(func() { stackGUID = "some-stack-guid" - state = resources.StackStateDeprecated + state = "DEPRECATED" }) JustBeforeEach(func() { @@ -168,7 +168,7 @@ var _ = Describe("Stacks", func() { CombineHandlers( VerifyRequest(http.MethodPatch, "/v3/stacks/some-stack-guid"), VerifyJSONRepresenting(map[string]string{ - "state": resources.StackStateDeprecated, + "state": "DEPRECATED", }), RespondWith(http.StatusOK, `{ "guid": "some-stack-guid", @@ -187,7 +187,7 @@ var _ = Describe("Stacks", func() { GUID: "some-stack-guid", Name: "some-stack", Description: "some description", - State: resources.StackStateDeprecated, + State: "DEPRECATED", })) }) }) From 19a5585bab9a57642a9404e731cba7618a513157 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Tue, 9 Dec 2025 12:45:33 -0500 Subject: [PATCH 05/19] Add update stack command integration tests Signed-off-by: Simon Jones --- integration/v7/isolated/update_stack_command_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration/v7/isolated/update_stack_command_test.go b/integration/v7/isolated/update_stack_command_test.go index cd8b227b96..2e5c0ba893 100644 --- a/integration/v7/isolated/update_stack_command_test.go +++ b/integration/v7/isolated/update_stack_command_test.go @@ -7,6 +7,7 @@ import ( . "code.cloudfoundry.org/cli/v8/cf/util/testhelpers/matchers" "code.cloudfoundry.org/cli/v8/integration/helpers" + "code.cloudfoundry.org/cli/v8/resources" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gbytes" @@ -44,7 +45,7 @@ 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\]`)) Eventually(session).Should(Say(`EXAMPLES:`)) Eventually(session).Should(Say(`cf update-stack cflinuxfs3 --state disabled`)) Eventually(session).Should(Say(`OPTIONS:`)) From d1b26c544600bb3a4eddf0658c2ece2ada8f97f9 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Tue, 20 Jan 2026 10:06:16 -0500 Subject: [PATCH 06/19] Stack related fakes generated correctly by counterfeiter Signed-off-by: Simon Jones --- .../fake_cloud_controller_client.go | 84 --------- command/v7/v7fakes/fake_actor.go | 166 ------------------ 2 files changed, 250 deletions(-) diff --git a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go index c682d4f262..b8abbda3f6 100644 --- a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go +++ b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go @@ -1912,22 +1912,6 @@ type FakeCloudControllerClient struct { result2 ccv3.Warnings result3 error } - UpdateStackStub func(string, string) (resources.Stack, ccv3.Warnings, error) - updateStackMutex sync.RWMutex - updateStackArgsForCall []struct { - arg1 string - arg2 string - } - updateStackReturns struct { - result1 resources.Stack - result2 ccv3.Warnings - result3 error - } - updateStackReturnsOnCall map[int]struct { - result1 resources.Stack - result2 ccv3.Warnings - result3 error - } GetStagingSecurityGroupsStub func(string, ...ccv3.Query) ([]resources.SecurityGroup, ccv3.Warnings, error) getStagingSecurityGroupsMutex sync.RWMutex getStagingSecurityGroupsArgsForCall []struct { @@ -11230,74 +11214,6 @@ func (fake *FakeCloudControllerClient) GetStacksReturnsOnCall(i int, result1 []r }{result1, result2, result3} } -func (fake *FakeCloudControllerClient) UpdateStack(arg1 string, arg2 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}) - stub := fake.UpdateStackStub - fakeReturns := fake.updateStackReturns - fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2}) - fake.updateStackMutex.Unlock() - if stub != nil { - return stub(arg1, arg2) - } - if specificReturn { - return ret.result1, ret.result2, ret.result3 - } - return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 -} - -func (fake *FakeCloudControllerClient) UpdateStackCallCount() int { - fake.updateStackMutex.RLock() - defer fake.updateStackMutex.RUnlock() - return len(fake.updateStackArgsForCall) -} - -func (fake *FakeCloudControllerClient) UpdateStackCalls(stub func(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) { - fake.updateStackMutex.RLock() - defer fake.updateStackMutex.RUnlock() - argsForCall := fake.updateStackArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 -} - -func (fake *FakeCloudControllerClient) UpdateStackReturns(result1 resources.Stack, result2 ccv3.Warnings, result3 error) { - fake.updateStackMutex.Lock() - defer fake.updateStackMutex.Unlock() - fake.UpdateStackStub = nil - fake.updateStackReturns = struct { - result1 resources.Stack - result2 ccv3.Warnings - result3 error - }{result1, result2, result3} -} - -func (fake *FakeCloudControllerClient) UpdateStackReturnsOnCall(i int, result1 resources.Stack, result2 ccv3.Warnings, result3 error) { - fake.updateStackMutex.Lock() - defer fake.updateStackMutex.Unlock() - fake.UpdateStackStub = nil - if fake.updateStackReturnsOnCall == nil { - fake.updateStackReturnsOnCall = make(map[int]struct { - result1 resources.Stack - result2 ccv3.Warnings - result3 error - }) - } - fake.updateStackReturnsOnCall[i] = struct { - result1 resources.Stack - result2 ccv3.Warnings - result3 error - }{result1, result2, result3} -} - func (fake *FakeCloudControllerClient) GetStagingSecurityGroups(arg1 string, arg2 ...ccv3.Query) ([]resources.SecurityGroup, ccv3.Warnings, error) { fake.getStagingSecurityGroupsMutex.Lock() ret, specificReturn := fake.getStagingSecurityGroupsReturnsOnCall[len(fake.getStagingSecurityGroupsArgsForCall)] diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 131dd1e61d..619fbb221a 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -2503,21 +2503,6 @@ type FakeActor struct { result1 resources.User result2 error } - ListServiceAppBindingsStub func(v7action.ListServiceAppBindingParams) ([]resources.ServiceCredentialBinding, v7action.Warnings, error) - listServiceAppBindingsMutex sync.RWMutex - listServiceAppBindingsArgsForCall []struct { - arg1 v7action.ListServiceAppBindingParams - } - listServiceAppBindingsReturns struct { - result1 []resources.ServiceCredentialBinding - result2 v7action.Warnings - result3 error - } - listServiceAppBindingsReturnsOnCall map[int]struct { - result1 []resources.ServiceCredentialBinding - result2 v7action.Warnings - result3 error - } MakeCurlRequestStub func(string, string, []string, string, bool) ([]byte, *http.Response, error) makeCurlRequestMutex sync.RWMutex makeCurlRequestArgsForCall []struct { @@ -3649,22 +3634,6 @@ type FakeActor struct { result1 v7action.Warnings result2 error } - UpdateStackStub func(string, string) (resources.Stack, v7action.Warnings, error) - updateStackMutex sync.RWMutex - updateStackArgsForCall []struct { - arg1 string - arg2 string - } - updateStackReturns struct { - result1 resources.Stack - result2 v7action.Warnings - result3 error - } - updateStackReturnsOnCall map[int]struct { - result1 resources.Stack - result2 v7action.Warnings - result3 error - } UpdateUserPasswordStub func(string, string, string) error updateUserPasswordMutex sync.RWMutex updateUserPasswordArgsForCall []struct { @@ -14575,73 +14544,6 @@ func (fake *FakeActor) GetUserReturnsOnCall(i int, result1 resources.User, resul }{result1, result2} } -func (fake *FakeActor) ListServiceAppBindings(arg1 v7action.ListServiceAppBindingParams) ([]resources.ServiceCredentialBinding, v7action.Warnings, error) { - fake.listServiceAppBindingsMutex.Lock() - ret, specificReturn := fake.listServiceAppBindingsReturnsOnCall[len(fake.listServiceAppBindingsArgsForCall)] - fake.listServiceAppBindingsArgsForCall = append(fake.listServiceAppBindingsArgsForCall, struct { - arg1 v7action.ListServiceAppBindingParams - }{arg1}) - stub := fake.ListServiceAppBindingsStub - fakeReturns := fake.listServiceAppBindingsReturns - fake.recordInvocation("ListServiceAppBindings", []interface{}{arg1}) - fake.listServiceAppBindingsMutex.Unlock() - if stub != nil { - return stub(arg1) - } - if specificReturn { - return ret.result1, ret.result2, ret.result3 - } - return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 -} - -func (fake *FakeActor) ListServiceAppBindingsCallCount() int { - fake.listServiceAppBindingsMutex.RLock() - defer fake.listServiceAppBindingsMutex.RUnlock() - return len(fake.listServiceAppBindingsArgsForCall) -} - -func (fake *FakeActor) ListServiceAppBindingsCalls(stub func(v7action.ListServiceAppBindingParams) ([]resources.ServiceCredentialBinding, v7action.Warnings, error)) { - fake.listServiceAppBindingsMutex.Lock() - defer fake.listServiceAppBindingsMutex.Unlock() - fake.ListServiceAppBindingsStub = stub -} - -func (fake *FakeActor) ListServiceAppBindingsArgsForCall(i int) v7action.ListServiceAppBindingParams { - fake.listServiceAppBindingsMutex.RLock() - defer fake.listServiceAppBindingsMutex.RUnlock() - argsForCall := fake.listServiceAppBindingsArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeActor) ListServiceAppBindingsReturns(result1 []resources.ServiceCredentialBinding, result2 v7action.Warnings, result3 error) { - fake.listServiceAppBindingsMutex.Lock() - defer fake.listServiceAppBindingsMutex.Unlock() - fake.ListServiceAppBindingsStub = nil - fake.listServiceAppBindingsReturns = struct { - result1 []resources.ServiceCredentialBinding - result2 v7action.Warnings - result3 error - }{result1, result2, result3} -} - -func (fake *FakeActor) ListServiceAppBindingsReturnsOnCall(i int, result1 []resources.ServiceCredentialBinding, result2 v7action.Warnings, result3 error) { - fake.listServiceAppBindingsMutex.Lock() - defer fake.listServiceAppBindingsMutex.Unlock() - fake.ListServiceAppBindingsStub = nil - if fake.listServiceAppBindingsReturnsOnCall == nil { - fake.listServiceAppBindingsReturnsOnCall = make(map[int]struct { - result1 []resources.ServiceCredentialBinding - result2 v7action.Warnings - result3 error - }) - } - fake.listServiceAppBindingsReturnsOnCall[i] = struct { - result1 []resources.ServiceCredentialBinding - result2 v7action.Warnings - result3 error - }{result1, result2, result3} -} - func (fake *FakeActor) MakeCurlRequest(arg1 string, arg2 string, arg3 []string, arg4 string, arg5 bool) ([]byte, *http.Response, error) { var arg3Copy []string if arg3 != nil { @@ -19674,74 +19576,6 @@ func (fake *FakeActor) UpdateStackLabelsByStackNameReturnsOnCall(i int, result1 }{result1, result2} } -func (fake *FakeActor) UpdateStack(arg1 string, arg2 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}) - stub := fake.UpdateStackStub - fakeReturns := fake.updateStackReturns - fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2}) - fake.updateStackMutex.Unlock() - if stub != nil { - return stub(arg1, arg2) - } - if specificReturn { - return ret.result1, ret.result2, ret.result3 - } - return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 -} - -func (fake *FakeActor) UpdateStackCallCount() int { - fake.updateStackMutex.RLock() - defer fake.updateStackMutex.RUnlock() - return len(fake.updateStackArgsForCall) -} - -func (fake *FakeActor) UpdateStackCalls(stub func(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) { - fake.updateStackMutex.RLock() - defer fake.updateStackMutex.RUnlock() - argsForCall := fake.updateStackArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 -} - -func (fake *FakeActor) UpdateStackReturns(result1 resources.Stack, result2 v7action.Warnings, result3 error) { - fake.updateStackMutex.Lock() - defer fake.updateStackMutex.Unlock() - fake.UpdateStackStub = nil - fake.updateStackReturns = struct { - result1 resources.Stack - result2 v7action.Warnings - result3 error - }{result1, result2, result3} -} - -func (fake *FakeActor) UpdateStackReturnsOnCall(i int, result1 resources.Stack, result2 v7action.Warnings, result3 error) { - fake.updateStackMutex.Lock() - defer fake.updateStackMutex.Unlock() - fake.UpdateStackStub = nil - if fake.updateStackReturnsOnCall == nil { - fake.updateStackReturnsOnCall = make(map[int]struct { - result1 resources.Stack - result2 v7action.Warnings - result3 error - }) - } - fake.updateStackReturnsOnCall[i] = struct { - result1 resources.Stack - result2 v7action.Warnings - result3 error - }{result1, result2, result3} -} - func (fake *FakeActor) UpdateUserPassword(arg1 string, arg2 string, arg3 string) error { fake.updateUserPasswordMutex.Lock() ret, specificReturn := fake.updateUserPasswordReturnsOnCall[len(fake.updateUserPasswordArgsForCall)] From c01b9d36dd99bbede7e970821afc4c064dbfbc7f Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Tue, 20 Jan 2026 12:12:12 -0500 Subject: [PATCH 07/19] Add update-stack to help categories in APPS section --- command/common/internal/help_all_display.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/command/common/internal/help_all_display.go b/command/common/internal/help_all_display.go index 5ca0633112..3fdf589f33 100644 --- a/command/common/internal/help_all_display.go +++ b/command/common/internal/help_all_display.go @@ -20,9 +20,9 @@ var HelpCategoryList = []HelpCategory{ {"revision", "revisions", "rollback"}, {"droplets", "set-droplet", "download-droplet"}, {"events", "logs"}, - {"env", "set-env", "unset-env"}, - {"stacks", "stack", "update-stack"}, - {"copy-source", "create-app-manifest"}, + {"env", "set-env", "unset-env"}, + {"stacks", "stack", "update-stack"}, + {"copy-source", "create-app-manifest"}, {"get-health-check", "set-health-check", "get-readiness-health-check"}, {"enable-ssh", "disable-ssh", "ssh-enabled", "ssh"}, }, From 2a0df8af2116348c147229e61cbd60e7e60eccfb Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Wed, 28 Jan 2026 11:37:43 -0500 Subject: [PATCH 08/19] Add parentheses and spaces to update-stack usage command Signed-off-by: Simon Jones --- command/v7/update_stack_command.go | 2 +- integration/v7/isolated/update_stack_command_test.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/command/v7/update_stack_command.go b/command/v7/update_stack_command.go index 561093407d..fbe2adffbb 100644 --- a/command/v7/update_stack_command.go +++ b/command/v7/update_stack_command.go @@ -13,7 +13,7 @@ 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"` + usage interface{} `usage:"CF_NAME update-stack STACK_NAME [--state (active | restricted | deprecated | disabled)]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled"` relatedCommands interface{} `related_commands:"stack, stacks"` } diff --git a/integration/v7/isolated/update_stack_command_test.go b/integration/v7/isolated/update_stack_command_test.go index 2e5c0ba893..cd8b227b96 100644 --- a/integration/v7/isolated/update_stack_command_test.go +++ b/integration/v7/isolated/update_stack_command_test.go @@ -7,7 +7,6 @@ import ( . "code.cloudfoundry.org/cli/v8/cf/util/testhelpers/matchers" "code.cloudfoundry.org/cli/v8/integration/helpers" - "code.cloudfoundry.org/cli/v8/resources" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gbytes" @@ -45,7 +44,7 @@ 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\)\]`)) Eventually(session).Should(Say(`EXAMPLES:`)) Eventually(session).Should(Say(`cf update-stack cflinuxfs3 --state disabled`)) Eventually(session).Should(Say(`OPTIONS:`)) From fb11404d1e51836d7dc2fa2b38b1968907d8f115 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Fri, 30 Jan 2026 12:09:49 -0500 Subject: [PATCH 09/19] Add minimum API version check for update-stack command (3.210.0) --- command/v7/update_stack_command.go | 9 ++++++++- command/v7/update_stack_command_test.go | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/command/v7/update_stack_command.go b/command/v7/update_stack_command.go index fbe2adffbb..35e2f86c39 100644 --- a/command/v7/update_stack_command.go +++ b/command/v7/update_stack_command.go @@ -4,6 +4,8 @@ import ( "slices" "strings" + "code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccversion" + "code.cloudfoundry.org/cli/v8/command" "code.cloudfoundry.org/cli/v8/command/flag" "code.cloudfoundry.org/cli/v8/resources" ) @@ -18,7 +20,12 @@ type UpdateStackCommand struct { } func (cmd UpdateStackCommand) Execute(args []string) error { - err := cmd.SharedActor.CheckTarget(false, false) + err := command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionUpdateStack) + if err != nil { + return err + } + + err = cmd.SharedActor.CheckTarget(false, false) if err != nil { return err } diff --git a/command/v7/update_stack_command_test.go b/command/v7/update_stack_command_test.go index 3ecb4626f5..1ee341052a 100644 --- a/command/v7/update_stack_command_test.go +++ b/command/v7/update_stack_command_test.go @@ -51,6 +51,7 @@ var _ = Describe("update-stack Command", func() { binaryName = "faceman" fakeConfig.BinaryNameReturns(binaryName) + fakeConfig.APIVersionReturns("3.210.0") }) Context("When the environment is not setup correctly", func() { From 384c357dac742778dc39481ad4dc02c0e71770bf Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Mon, 2 Feb 2026 13:03:19 -0500 Subject: [PATCH 10/19] Add assertions for state output in stack command tests --- command/v7/stack_command_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/command/v7/stack_command_test.go b/command/v7/stack_command_test.go index 32568dd6a7..643cd162c3 100644 --- a/command/v7/stack_command_test.go +++ b/command/v7/stack_command_test.go @@ -141,6 +141,10 @@ var _ = Describe("Stack Command", 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).NotTo(Say("state:")) }) }) @@ -159,6 +163,10 @@ var _ = Describe("Stack Command", 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+ACTIVE")) }) }) From c8c651273574fb222515efa434c56826445f90aa Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Mon, 9 Feb 2026 13:24:16 -0500 Subject: [PATCH 11/19] Fix indentation in help_all_display.go APPS section Co-authored-by: Cursor --- command/common/internal/help_all_display.go | 6 +-- command/v7/update_stack_command_test.go | 56 ++++++++++----------- resources/stack_resource.go | 5 -- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/command/common/internal/help_all_display.go b/command/common/internal/help_all_display.go index 3fdf589f33..5ca0633112 100644 --- a/command/common/internal/help_all_display.go +++ b/command/common/internal/help_all_display.go @@ -20,9 +20,9 @@ var HelpCategoryList = []HelpCategory{ {"revision", "revisions", "rollback"}, {"droplets", "set-droplet", "download-droplet"}, {"events", "logs"}, - {"env", "set-env", "unset-env"}, - {"stacks", "stack", "update-stack"}, - {"copy-source", "create-app-manifest"}, + {"env", "set-env", "unset-env"}, + {"stacks", "stack", "update-stack"}, + {"copy-source", "create-app-manifest"}, {"get-health-check", "set-health-check", "get-readiness-health-check"}, {"enable-ssh", "disable-ssh", "ssh-enabled", "ssh"}, }, diff --git a/command/v7/update_stack_command_test.go b/command/v7/update_stack_command_test.go index 1ee341052a..e3d4f63e60 100644 --- a/command/v7/update_stack_command_test.go +++ b/command/v7/update_stack_command_test.go @@ -157,43 +157,43 @@ var _ = Describe("update-stack Command", func() { }) }) - 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)) + }) }) }) - }) }) diff --git a/resources/stack_resource.go b/resources/stack_resource.go index a26746b021..049fb187b5 100644 --- a/resources/stack_resource.go +++ b/resources/stack_resource.go @@ -27,11 +27,6 @@ func ValidStackStatesLowercase() []string { return lowercase } -// ValidStackStatesString returns a pipe-separated string of valid states in lowercase -func ValidStackStatesString() string { - return strings.Join(ValidStackStatesLowercase(), "|") -} - type Stack struct { // GUID is a unique stack identifier. GUID string `json:"guid"` From d963434a203b7e8201991df0dc4906a4193951c7 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Thu, 12 Feb 2026 11:35:15 -0500 Subject: [PATCH 12/19] Update stack and stacks integration test expectations for state support Co-authored-by: Cursor --- integration/v7/isolated/stack_command_test.go | 83 ++++++------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/integration/v7/isolated/stack_command_test.go b/integration/v7/isolated/stack_command_test.go index 98c8f138df..6f819fd00c 100644 --- a/integration/v7/isolated/stack_command_test.go +++ b/integration/v7/isolated/stack_command_test.go @@ -33,7 +33,7 @@ var _ = Describe("stack command", func() { It("appears in cf help -a", func() { session := helpers.CF("help", "-a") Eventually(session).Should(Exit(0)) - Expect(session).To(HaveCommandInCategoryWithDescription("stack", "APPS", "Show information for a stack (a stack is a pre-built file system, including an operating system, that can run apps)")) + Expect(session).To(HaveCommandInCategoryWithDescription("stack", "APPS", "Show information for a stack (a stack is a pre-built file system, including an operating system, that can run apps) and current state")) }) It("Displays command usage to output", func() { @@ -100,71 +100,42 @@ var _ = Describe("stack command", func() { }) }) - When("the stack exists", func() { + When("the stack exists with valid state", func() { var stackGUID string - Context("when the stack has no state", func() { - BeforeEach(func() { - jsonBody := fmt.Sprintf(`{"name": "%s", "description": "%s"}`, stackName, stackDescription) - session := helpers.CF("curl", "-d", jsonBody, "-X", "POST", "/v3/stacks") - Eventually(session).Should(Exit(0)) - - r := regexp.MustCompile(`[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}`) - stackGUID = string(r.Find(session.Out.Contents())) - }) - - AfterEach(func() { - session := helpers.CF("curl", "-X", "DELETE", fmt.Sprintf("/v3/stacks/%s", stackGUID)) - Eventually(session).Should(Exit(0)) - }) - - It("Shows the details for the stack without state", func() { - session := helpers.CF("stack", stackName) + BeforeEach(func() { + jsonBody := fmt.Sprintf(`{"name": "%s", "description": "%s", "state": "ACTIVE"}`, stackName, stackDescription) + session := helpers.CF("curl", "-d", jsonBody, "-X", "POST", "/v3/stacks") + Eventually(session).Should(Exit(0)) - 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)) - Consistently(session).ShouldNot(Say(`state:`)) - Eventually(session).Should(Exit(0)) - }) + r := regexp.MustCompile(`[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}`) + stackGUID = string(r.Find(session.Out.Contents())) }) - Context("when the stack has a valid state", func() { - BeforeEach(func() { - jsonBody := fmt.Sprintf(`{"name": "%s", "description": "%s", "state": "ACTIVE"}`, stackName, stackDescription) - session := helpers.CF("curl", "-d", jsonBody, "-X", "POST", "/v3/stacks") - Eventually(session).Should(Exit(0)) - - r := regexp.MustCompile(`[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}`) - stackGUID = string(r.Find(session.Out.Contents())) - }) - - AfterEach(func() { - session := helpers.CF("curl", "-X", "DELETE", fmt.Sprintf("/v3/stacks/%s", stackGUID)) - Eventually(session).Should(Exit(0)) - }) + AfterEach(func() { + session := helpers.CF("curl", "-X", "DELETE", fmt.Sprintf("/v3/stacks/%s", stackGUID)) + Eventually(session).Should(Exit(0)) + }) - It("Shows the details for the stack with state", func() { - session := helpers.CF("stack", stackName) + It("Shows the details for the stack with state", 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+ACTIVE`)) - Eventually(session).Should(Exit(0)) - }) + 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+ACTIVE`)) + Eventually(session).Should(Exit(0)) }) - When("the stack exists and the --guid flag is passed", func() { - It("prints nothing but the guid", func() { - session := helpers.CF("stack", stackName, "--guid") + It("prints nothing but the guid when --guid flag is passed", func() { + session := helpers.CF("stack", stackName, "--guid") - Consistently(session).ShouldNot(Say(`Getting info for stack %s as %s\.\.\.`, stackName, username)) - Consistently(session).ShouldNot(Say(`name:\s+%s`, stackName)) - Consistently(session).ShouldNot(Say(`description:\s+%s`, stackDescription)) - 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)) - }) + Consistently(session).ShouldNot(Say(`Getting info for stack %s as %s\.\.\.`, stackName, username)) + Consistently(session).ShouldNot(Say(`name:\s+%s`, stackName)) + Consistently(session).ShouldNot(Say(`description:\s+%s`, stackDescription)) + Consistently(session).ShouldNot(Say(`state:`)) + 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)) }) }) }) From e972c885f347e27ab608b5ee6591eae79e2969a2 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Mon, 23 Feb 2026 12:06:57 -0500 Subject: [PATCH 13/19] Update minimum API version for update-stack to 3.211.0 Co-authored-by: Cursor --- api/cloudcontroller/ccversion/minimum_version.go | 2 +- command/v7/update_stack_command_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/update_stack_command_test.go b/command/v7/update_stack_command_test.go index e3d4f63e60..c3aa259df3 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() { From bec3181884dec5de06fd8423b535dd76e2e564cc Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Thu, 5 Feb 2026 11:30:24 -0500 Subject: [PATCH 14/19] Add state_reason field to stack resource and display logic --- command/v7/stack_command.go | 5 +++ command/v7/stack_command_test.go | 54 ++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/command/v7/stack_command.go b/command/v7/stack_command.go index d451fa9d49..696aa0601b 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 only if state is not ACTIVE and reason is present + if stack.State != resources.StackStateActive && stack.StateReason != "" { + 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..92717d390e 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 but no 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).NotTo(Say("reason:")) + }) + }) + }) + When("The Stack does not Exist", func() { expectedError := actionerror.StackNotFoundError{Name: "some-stack-name"} BeforeEach(func() { From f7ea046ff51b23aca734200654327d93a66f65b1 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Thu, 5 Feb 2026 11:45:42 -0500 Subject: [PATCH 15/19] Add --reason flag to update-stack command --- actor/v7action/stack.go | 4 +- api/cloudcontroller/ccv3/stack.go | 9 +- api/cloudcontroller/ccv3/stack_test.go | 38 ++++++++- command/v7/update_stack_command.go | 16 +++- command/v7/update_stack_command_test.go | 85 ++++++++++++------- .../v7/isolated/update_stack_command_test.go | 3 +- 6 files changed, 114 insertions(+), 41 deletions(-) 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/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/command/v7/update_stack_command.go b/command/v7/update_stack_command.go index 35e2f86c39..fcec5c8f93 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 REASON]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled\n CF_NAME update-stack cflinuxfs3 --state deprecated --reason 'Use cflinuxfs4 instead'"` 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 if it's present + if updatedStack.StateReason != "" { + 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 c3aa259df3..a98d33fc7c 100644 --- a/command/v7/update_stack_command_test.go +++ b/command/v7/update_stack_command_test.go @@ -149,51 +149,78 @@ 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() { + executeErr = cmd.Execute(args) + + 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/integration/v7/isolated/update_stack_command_test.go b/integration/v7/isolated/update_stack_command_test.go index cd8b227b96..f4c9d828aa 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 REASON\]`)) 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 'Use cflinuxfs4 instead'`)) Eventually(session).Should(Say(`OPTIONS:`)) Eventually(session).Should(Say(`--state\s+State to transition the stack to`)) Eventually(session).Should(Say(`SEE ALSO:`)) From 8ce77bc6adc2da80175eb0d5e0d2274fb61adab8 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Thu, 5 Feb 2026 11:53:06 -0500 Subject: [PATCH 16/19] Update --reason flag usage example with detailed migration message --- command/v7/update_stack_command.go | 2 +- integration/v7/isolated/update_stack_command_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/v7/update_stack_command.go b/command/v7/update_stack_command.go index fcec5c8f93..1fc46de889 100644 --- a/command/v7/update_stack_command.go +++ b/command/v7/update_stack_command.go @@ -16,7 +16,7 @@ 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"` 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 REASON]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled\n CF_NAME update-stack cflinuxfs3 --state deprecated --reason 'Use cflinuxfs4 instead'"` + 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"` } diff --git a/integration/v7/isolated/update_stack_command_test.go b/integration/v7/isolated/update_stack_command_test.go index f4c9d828aa..ba278c0da0 100644 --- a/integration/v7/isolated/update_stack_command_test.go +++ b/integration/v7/isolated/update_stack_command_test.go @@ -44,10 +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\)\] \[--reason REASON\]`)) + 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 'Use cflinuxfs4 instead'`)) + 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:`)) From 1144edd721def6f713fc8fcef4ad916e6ea39a47 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Thu, 12 Feb 2026 11:53:58 -0500 Subject: [PATCH 17/19] Show reason field for non-active stack states and fix UpdateStack interface - Display reason: whenever stack state is non-ACTIVE, even if reason is empty - Update CloudControllerClient and Actor interfaces for 3-arg UpdateStack - Add StateReason field to Stack resource - Regenerate fakes for updated interfaces - Fix duplicate Execute call in update-stack reason test Co-authored-by: Cursor --- actor/v7action/cloud_controller_client.go | 2 +- actor/v7action/stack_test.go | 7 +++++-- .../fake_cloud_controller_client.go | 18 ++++++++++-------- command/v7/actor.go | 2 +- command/v7/stack_command.go | 4 ++-- command/v7/stack_command_test.go | 4 ++-- command/v7/update_stack_command.go | 4 ++-- command/v7/update_stack_command_test.go | 2 -- command/v7/v7fakes/fake_actor.go | 18 ++++++++++-------- resources/stack_resource.go | 2 ++ 10 files changed, 35 insertions(+), 28 deletions(-) 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_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/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 696aa0601b..bc15d7300c 100644 --- a/command/v7/stack_command.go +++ b/command/v7/stack_command.go @@ -70,8 +70,8 @@ func (cmd *StackCommand) displayStackInfo() error { if stack.State != "" { displayTable = append(displayTable, []string{cmd.UI.TranslateText("state:"), stack.State}) - // Add reason only if state is not ACTIVE and reason is present - if stack.State != resources.StackStateActive && stack.StateReason != "" { + // Add reason whenever state is not ACTIVE + if stack.State != resources.StackStateActive { displayTable = append(displayTable, []string{cmd.UI.TranslateText("reason:"), stack.StateReason}) } } diff --git a/command/v7/stack_command_test.go b/command/v7/stack_command_test.go index 92717d390e..9b5192c4f2 100644 --- a/command/v7/stack_command_test.go +++ b/command/v7/stack_command_test.go @@ -207,7 +207,7 @@ var _ = Describe("Stack Command", func() { fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil) }) - It("Displays the stack information with state but no reason", func() { + 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)) @@ -215,7 +215,7 @@ 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+RESTRICTED")) - Expect(testUI.Out).NotTo(Say("reason:")) + Expect(testUI.Out).To(Say("reason:")) }) }) }) diff --git a/command/v7/update_stack_command.go b/command/v7/update_stack_command.go index 1fc46de889..afe3688acd 100644 --- a/command/v7/update_stack_command.go +++ b/command/v7/update_stack_command.go @@ -73,8 +73,8 @@ func (cmd UpdateStackCommand) Execute(args []string) error { {cmd.UI.TranslateText("state:"), updatedStack.State}, } - // Add reason if it's present - if updatedStack.StateReason != "" { + // Add reason whenever state is not ACTIVE + if updatedStack.State != resources.StackStateActive { displayTable = append(displayTable, []string{cmd.UI.TranslateText("reason:"), updatedStack.StateReason}) } diff --git a/command/v7/update_stack_command_test.go b/command/v7/update_stack_command_test.go index a98d33fc7c..71a2f16926 100644 --- a/command/v7/update_stack_command_test.go +++ b/command/v7/update_stack_command_test.go @@ -210,8 +210,6 @@ var _ = Describe("update-stack Command", func() { }) It("passes the reason to the actor and displays it", func() { - executeErr = cmd.Execute(args) - Expect(executeErr).ToNot(HaveOccurred()) Expect(fakeActor.UpdateStackCallCount()).To(Equal(1)) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 619fbb221a..30b99ce80d 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -3604,11 +3604,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 @@ -19443,19 +19444,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 @@ -19469,17 +19471,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/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"` From b4f6b4a7e48ccd1b06553a0552fe9425b546d961 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Thu, 12 Feb 2026 12:08:49 -0500 Subject: [PATCH 18/19] Add integration tests for stack reason display scenarios - update-stack: test empty reason for non-active state, test reason with --reason flag - stack: test no reason for active state, test empty reason for non-active state, test reason for non-active state with reason Co-authored-by: Cursor --- integration/v7/isolated/stack_command_test.go | 45 +++++++++++++++++++ .../v7/isolated/update_stack_command_test.go | 26 +++++++++++ 2 files changed, 71 insertions(+) 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 ba278c0da0..711d8a31c4 100644 --- a/integration/v7/isolated/update_stack_command_test.go +++ b/integration/v7/isolated/update_stack_command_test.go @@ -207,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") From becc9df232d30d42bbdbb8efd9c4e3b4fcfaa08c Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Mon, 23 Feb 2026 12:45:48 -0500 Subject: [PATCH 19/19] Regenerate fakeActor Signed-off-by: Simon Jones --- command/v7/v7fakes/fake_actor.go | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 30b99ce80d..fdd4d54254 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -2503,6 +2503,21 @@ type FakeActor struct { result1 resources.User result2 error } + ListServiceAppBindingsStub func(v7action.ListServiceAppBindingParams) ([]resources.ServiceCredentialBinding, v7action.Warnings, error) + listServiceAppBindingsMutex sync.RWMutex + listServiceAppBindingsArgsForCall []struct { + arg1 v7action.ListServiceAppBindingParams + } + listServiceAppBindingsReturns struct { + result1 []resources.ServiceCredentialBinding + result2 v7action.Warnings + result3 error + } + listServiceAppBindingsReturnsOnCall map[int]struct { + result1 []resources.ServiceCredentialBinding + result2 v7action.Warnings + result3 error + } MakeCurlRequestStub func(string, string, []string, string, bool) ([]byte, *http.Response, error) makeCurlRequestMutex sync.RWMutex makeCurlRequestArgsForCall []struct { @@ -14545,6 +14560,73 @@ func (fake *FakeActor) GetUserReturnsOnCall(i int, result1 resources.User, resul }{result1, result2} } +func (fake *FakeActor) ListServiceAppBindings(arg1 v7action.ListServiceAppBindingParams) ([]resources.ServiceCredentialBinding, v7action.Warnings, error) { + fake.listServiceAppBindingsMutex.Lock() + ret, specificReturn := fake.listServiceAppBindingsReturnsOnCall[len(fake.listServiceAppBindingsArgsForCall)] + fake.listServiceAppBindingsArgsForCall = append(fake.listServiceAppBindingsArgsForCall, struct { + arg1 v7action.ListServiceAppBindingParams + }{arg1}) + stub := fake.ListServiceAppBindingsStub + fakeReturns := fake.listServiceAppBindingsReturns + fake.recordInvocation("ListServiceAppBindings", []interface{}{arg1}) + fake.listServiceAppBindingsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeActor) ListServiceAppBindingsCallCount() int { + fake.listServiceAppBindingsMutex.RLock() + defer fake.listServiceAppBindingsMutex.RUnlock() + return len(fake.listServiceAppBindingsArgsForCall) +} + +func (fake *FakeActor) ListServiceAppBindingsCalls(stub func(v7action.ListServiceAppBindingParams) ([]resources.ServiceCredentialBinding, v7action.Warnings, error)) { + fake.listServiceAppBindingsMutex.Lock() + defer fake.listServiceAppBindingsMutex.Unlock() + fake.ListServiceAppBindingsStub = stub +} + +func (fake *FakeActor) ListServiceAppBindingsArgsForCall(i int) v7action.ListServiceAppBindingParams { + fake.listServiceAppBindingsMutex.RLock() + defer fake.listServiceAppBindingsMutex.RUnlock() + argsForCall := fake.listServiceAppBindingsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeActor) ListServiceAppBindingsReturns(result1 []resources.ServiceCredentialBinding, result2 v7action.Warnings, result3 error) { + fake.listServiceAppBindingsMutex.Lock() + defer fake.listServiceAppBindingsMutex.Unlock() + fake.ListServiceAppBindingsStub = nil + fake.listServiceAppBindingsReturns = struct { + result1 []resources.ServiceCredentialBinding + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeActor) ListServiceAppBindingsReturnsOnCall(i int, result1 []resources.ServiceCredentialBinding, result2 v7action.Warnings, result3 error) { + fake.listServiceAppBindingsMutex.Lock() + defer fake.listServiceAppBindingsMutex.Unlock() + fake.ListServiceAppBindingsStub = nil + if fake.listServiceAppBindingsReturnsOnCall == nil { + fake.listServiceAppBindingsReturnsOnCall = make(map[int]struct { + result1 []resources.ServiceCredentialBinding + result2 v7action.Warnings + result3 error + }) + } + fake.listServiceAppBindingsReturnsOnCall[i] = struct { + result1 []resources.ServiceCredentialBinding + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeActor) MakeCurlRequest(arg1 string, arg2 string, arg3 []string, arg4 string, arg5 bool) ([]byte, *http.Response, error) { var arg3Copy []string if arg3 != nil {