Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a60275e
Add state field to cf stacks command
simonjjones Dec 1, 2025
a9caa97
Remove state validation from cf state command
simonjjones Dec 2, 2025
f905efa
first pass of update-stack command
simonjjones Dec 8, 2025
01338d0
Include reference to state in help text for cf stack & stacks
simonjjones Dec 9, 2025
19a5585
Add update stack command integration tests
simonjjones Dec 9, 2025
d1b26c5
Stack related fakes generated correctly by counterfeiter
simonjjones Jan 20, 2026
c01b9d3
Add update-stack to help categories in APPS section
simonjjones Jan 20, 2026
2a0df8a
Add parentheses and spaces to update-stack usage command
simonjjones Jan 28, 2026
fb11404
Add minimum API version check for update-stack command (3.210.0)
simonjjones Jan 30, 2026
384c357
Add assertions for state output in stack command tests
simonjjones Feb 2, 2026
c8c6512
Fix indentation in help_all_display.go APPS section
simonjjones Feb 9, 2026
d963434
Update stack and stacks integration test expectations for state support
simonjjones Feb 12, 2026
e972c88
Update minimum API version for update-stack to 3.211.0
simonjjones Feb 23, 2026
bec3181
Add state_reason field to stack resource and display logic
simonjjones Feb 5, 2026
f7ea046
Add --reason flag to update-stack command
simonjjones Feb 5, 2026
8ce77bc
Update --reason flag usage example with detailed migration message
simonjjones Feb 5, 2026
1144edd
Show reason field for non-active stack states and fix UpdateStack int…
simonjjones Feb 12, 2026
b4f6b4a
Add integration tests for stack reason display scenarios
simonjjones Feb 12, 2026
becc9df
Regenerate fakeActor
simonjjones Feb 23, 2026
6f60ddd
Merge branch 'v8' into v8-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 @@ -25,5 +25,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
54 changes: 52 additions & 2 deletions command/v7/stack_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -159,17 +160,66 @@ 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))

Expect(testUI.Out).To(Say("name:\\s+some-stack-name"))
Expect(testUI.Out).To(Say("description:\\s+some-stack-desc"))
Expect(testUI.Out).To(Say("state:\\s+ACTIVE"))
Expect(testUI.Out).NotTo(Say("reason:"))
})
})

Context("When the state is not ACTIVE and has a reason", func() {
BeforeEach(func() {
stack := resources.Stack{
Name: "some-stack-name",
GUID: "some-stack-guid",
Description: "some-stack-desc",
State: "DEPRECATED",
StateReason: "This stack is being phased out",
}
fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil)
})

It("Displays the stack information with state and reason", func() {
Expect(executeErr).ToNot(HaveOccurred())
Expect(fakeActor.GetStackByNameArgsForCall(0)).To(Equal("some-stack-name"))
Expect(fakeActor.GetStackByNameCallCount()).To(Equal(1))

Expect(testUI.Out).To(Say("name:\\s+some-stack-name"))
Expect(testUI.Out).To(Say("description:\\s+some-stack-desc"))
Expect(testUI.Out).To(Say("state:\\s+DEPRECATED"))
Expect(testUI.Out).To(Say("reason:\\s+This stack is being phased out"))
})
})

Context("When the state is not ACTIVE but has no reason", func() {
BeforeEach(func() {
stack := resources.Stack{
Name: "some-stack-name",
GUID: "some-stack-guid",
Description: "some-stack-desc",
State: "RESTRICTED",
}
fakeActor.GetStackByNameReturns(stack, v7action.Warnings{}, nil)
})

It("Displays the stack information with state and empty reason", func() {
Expect(executeErr).ToNot(HaveOccurred())
Expect(fakeActor.GetStackByNameArgsForCall(0)).To(Equal("some-stack-name"))
Expect(fakeActor.GetStackByNameCallCount()).To(Equal(1))

Expect(testUI.Out).To(Say("name:\\s+some-stack-name"))
Expect(testUI.Out).To(Say("description:\\s+some-stack-desc"))
Expect(testUI.Out).To(Say("state:\\s+RESTRICTED"))
Expect(testUI.Out).To(Say("reason:"))
})
})
})

When("The Stack does not Exist", func() {
expectedError := actionerror.StackNotFoundError{Name: "some-stack-name"}
BeforeEach(func() {
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