Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8b5d848
Add state field to cf stacks command
simonjjones Dec 1, 2025
50be8a9
Remove state validation from cf state command
simonjjones Dec 2, 2025
8a92ec2
first pass of update-stack command
simonjjones Dec 8, 2025
d092908
Include reference to state in help text for cf stack & stacks
simonjjones Dec 9, 2025
aa986f5
Add update stack command integration tests
simonjjones Dec 9, 2025
b9138e3
Stack related fakes generated correctly by counterfeiter
simonjjones Jan 20, 2026
48c1ffb
Fix import paths from v8 to v9 for module compatibility
simonjjones Jan 20, 2026
55d39c9
Add update-stack command to help categories
simonjjones Jan 20, 2026
897d53d
Add parentheses and spaces to update-stack usage command
simonjjones Jan 28, 2026
a66bb13
Configure minimum CC API version for update-stack command
simonjjones Jan 30, 2026
4841895
Add assertions for state output in stack command tests
simonjjones Feb 2, 2026
fbe2b45
Fix indentation in help_all_display.go APPS section
simonjjones Feb 9, 2026
2abe16c
Update stack and stacks integration test expectations for state support
simonjjones Feb 12, 2026
e12af51
Update minimum API version for update-stack to 3.211.0
simonjjones Feb 23, 2026
e68c23e
Add state_reason field to stack resource and display logic
simonjjones Feb 5, 2026
d3358a4
Add --reason flag to update-stack command
simonjjones Feb 5, 2026
f0bc9fa
Update --reason flag usage example with detailed migration message
simonjjones Feb 5, 2026
733087f
Show reason field for non-active stack states and fix UpdateStack int…
simonjjones Feb 12, 2026
981ee31
Add integration tests for stack reason display scenarios
simonjjones Feb 12, 2026
fa9ad74
Regenerate fakeActor
simonjjones Feb 23, 2026
0d43d07
Merge branch 'main' into stack-management-reason
simonjjones Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion actor/v7action/cloud_controller_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions actor/v7action/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
7 changes: 5 additions & 2 deletions actor/v7action/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ var _ = Describe("Stack", func() {
var (
stackGUID string
state string
reason string
stack resources.Stack
warnings Warnings
executeErr error
Expand All @@ -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() {
Expand Down Expand Up @@ -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))
})
})

Expand Down
18 changes: 10 additions & 8 deletions actor/v7action/v7actionfakes/fake_cloud_controller_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions api/cloudcontroller/ccv3/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand Down
38 changes: 37 additions & 1 deletion api/cloudcontroller/ccv3/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ var _ = Describe("Stacks", func() {
var (
stackGUID string
state string
reason string
stack resources.Stack
warnings Warnings
err error
Expand All @@ -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() {
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion api/cloudcontroller/ccversion/minimum_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ const (

MinVersionServiceBindingStrategy = "3.205.0"

MinVersionUpdateStack = "3.210.0"
MinVersionUpdateStack = "3.211.0"
)
2 changes: 1 addition & 1 deletion command/v7/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions command/v7/stack_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ func (cmd *StackCommand) displayStackInfo() error {
// Add state only if it's present
if stack.State != "" {
displayTable = append(displayTable, []string{cmd.UI.TranslateText("state:"), stack.State})

// Add reason whenever state is not ACTIVE
if stack.State != resources.StackStateActive {
displayTable = append(displayTable, []string{cmd.UI.TranslateText("reason:"), stack.StateReason})
}
}

cmd.UI.DisplayKeyValueTable("", displayTable, 3)
Expand Down
56 changes: 53 additions & 3 deletions command/v7/stack_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,26 +149,76 @@ var _ = Describe("Stack Command", 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",
GUID: "some-stack-guid",
Description: "some-stack-desc",
State: "ACTIVE",
}
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+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: "ACTIVE",
State: "RESTRICTED",
}
fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil)
})

It("Displays the stack information with state", 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))

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).To(Say("state:\\s+RESTRICTED"))
Expect(testUI.Out).To(Say("reason:"))
})
})
})

When("The Stack does not Exist", func() {
expectedError := actionerror.StackNotFoundError{Name: "some-stack-name"}
Expand Down
16 changes: 12 additions & 4 deletions command/v7/update_stack_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ type UpdateStackCommand struct {

RequiredArgs flag.StackName `positional-args:"yes"`
State string `long:"state" description:"State to transition the stack to (active, restricted, deprecated, disabled)" required:"true"`
usage interface{} `usage:"CF_NAME update-stack STACK_NAME [--state (active | restricted | deprecated | disabled)]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled"`
Reason string `long:"reason" description:"Optional plain text describing the stack state change"`
usage interface{} `usage:"CF_NAME update-stack STACK_NAME [--state (active | restricted | deprecated | disabled)] [--reason text]\n\nEXAMPLES:\n CF_NAME update-stack cflinuxfs3 --state disabled\n CF_NAME update-stack cflinuxfs3 --state deprecated --reason \"This stack is based on Ubuntu 18.04, which is no longer supported. Please migrate your applications to 'cflinuxfs4'. For more information, see: <link-to-docs>.\""`
relatedCommands interface{} `related_commands:"stack, stacks"`
}

Expand Down Expand Up @@ -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
Expand All @@ -66,11 +67,18 @@ func (cmd UpdateStackCommand) Execute(args []string) error {
cmd.UI.DisplayNewline()

// Display the updated stack info
cmd.UI.DisplayKeyValueTable("", [][]string{
displayTable := [][]string{
{cmd.UI.TranslateText("name:"), updatedStack.Name},
{cmd.UI.TranslateText("description:"), updatedStack.Description},
{cmd.UI.TranslateText("state:"), updatedStack.State},
}, 3)
}

// Add reason whenever state is not ACTIVE
if updatedStack.State != resources.StackStateActive {
displayTable = append(displayTable, []string{cmd.UI.TranslateText("reason:"), updatedStack.StateReason})
}

cmd.UI.DisplayKeyValueTable("", displayTable, 3)

return nil
}
Expand Down
Loading
Loading