Skip to content

feat(cloudformation): support UpdateTerminationProtection#1692

Open
abanna wants to merge 2 commits into
floci-io:mainfrom
abanna:feat/cloudformation-termination-protection
Open

feat(cloudformation): support UpdateTerminationProtection#1692
abanna wants to merge 2 commits into
floci-io:mainfrom
abanna:feat/cloudformation-termination-protection

Conversation

@abanna

@abanna abanna commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

CDK cdk bootstrap calls UpdateTerminationProtection on the CDKToolkit stack; Floci returned UnknownAction, so bootstrap couldn't complete.

Change

  • Stack: adds a enableTerminationProtection flag (default false) + accessors.
  • CloudFormationService.updateTerminationProtection(stackName, enabled, region): resolves the stack (ValidationError if it doesn't exist), sets the flag, and persists.
  • CloudFormationQueryHandler: dispatches UpdateTerminationProtection → returns the UpdateTerminationProtectionResult envelope with the StackId; and echoes <EnableTerminationProtection> in the DescribeStacks stack XML.
  • AwsQueryController: registers the action in the CloudFormation action set so the Query-protocol POST routes to CloudFormation (without this it fell through to another service).

Test

updateTerminationProtection_togglesFlagAndReflectsInDescribeStacks — DescribeStacks reports false by default, UpdateTerminationProtection returns the StackId, and DescribeStacks then reflects true.

Built + tested in the temurin container; the full CloudFormationIntegrationTest passes (the one unrelated failure is an AWS::ECR::Repository test that needs a Docker socket the build container doesn't have).

CDK bootstrap calls UpdateTerminationProtection on the CDKToolkit stack; Floci returned
UnknownAction. Add it: a boolean enableTerminationProtection on Stack, a
CloudFormationService.updateTerminationProtection(stackName, enabled, region) that sets and
persists it, a handler returning the UpdateTerminationProtectionResult/StackId envelope, and
the flag echoed as <EnableTerminationProtection> in DescribeStacks. Register the action in
AwsQueryController's CloudFormation action set so the Query-protocol POST routes to
CloudFormation instead of falling through. Adds an integration test toggling the flag and
asserting it round-trips through DescribeStacks.
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds UpdateTerminationProtection support to the CloudFormation emulator so that cdk bootstrap can complete without hitting an UnknownAction error. The implementation wires the action through routing, adds the service method, persists the flag on the Stack model, and echoes it in DescribeStacks XML.

  • AwsQueryController: registers UpdateTerminationProtection in the CloudFormation action set so Query-protocol POST routing reaches the correct handler.
  • CloudFormationQueryHandler: dispatches the new action, builds the UpdateTerminationProtectionResult XML envelope, and includes <EnableTerminationProtection> in the existing DescribeStacks stack fragment.
  • CloudFormationService / Stack: stores the enableTerminationProtection flag (default false) and persists it, but deleteStack does not read the flag to block deletion when protection is enabled.

Confidence Score: 4/5

Safe to merge for unblocking cdk bootstrap, but the termination protection flag has no enforcement: a DeleteStack call on a protected stack will succeed instead of returning a ValidationError.

The flag is persisted and echoed in DescribeStacks correctly, but deleteStack never consults it. A client that enables protection expecting deletion to be blocked will find the guard is absent — the stored flag is decorative. The happy path cdk bootstrap use case works, but the safety semantics of the feature are incomplete.

CloudFormationService.javadeleteStack needs to read enableTerminationProtection and throw a ValidationError before proceeding with deletion.

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationService.java Adds updateTerminationProtection which stores the flag correctly, but deleteStack never checks the flag, so protection is stored but never enforced.
src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationQueryHandler.java Adds UpdateTerminationProtection handler and echoes EnableTerminationProtection in DescribeStacks XML; handler is missing the catch (AwsException e) try-wrap (already flagged in prior thread), and the happy-path XML structure is correct.
src/main/java/io/github/hectorvent/floci/services/cloudformation/model/Stack.java Adds enableTerminationProtection field with correct default false, getter/setter. Clean change.
src/main/java/io/github/hectorvent/floci/core/common/AwsQueryController.java Adds UpdateTerminationProtection to the CloudFormation action set so Query-protocol routing reaches the correct handler. Correct and minimal change.
src/test/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationIntegrationTest.java Adds updateTerminationProtection_togglesFlagAndReflectsInDescribeStacks covering the happy path end-to-end. No test for enforced deletion rejection when protection is enabled.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CDK as CDK Client
    participant QC as AwsQueryController
    participant QH as CloudFormationQueryHandler
    participant SVC as CloudFormationService
    participant Store as StorageBackend

    CDK->>QC: "POST Action=UpdateTerminationProtection"
    QC->>QH: handle("UpdateTerminationProtection", params, region)
    QH->>SVC: updateTerminationProtection(stackName, enabled, region)
    SVC->>SVC: getStackOrThrow(stackName, region)
    SVC->>SVC: stack.setEnableTerminationProtection(enabled)
    SVC->>Store: persistStack(stack)
    SVC-->>QH: Stack
    QH-->>CDK: 200 XML UpdateTerminationProtectionResult(StackId)

    CDK->>QC: "POST Action=DescribeStacks"
    QC->>QH: handle("DescribeStacks", params, region)
    QH->>SVC: describeStacks(stackName, region)
    SVC-->>QH: List[Stack]
    QH-->>CDK: 200 XML (includes EnableTerminationProtection)

    CDK->>QC: "POST Action=DeleteStack"
    QC->>QH: handle("DeleteStack", params, region)
    QH->>SVC: deleteStack(stackName, region)
    Note over SVC: Does NOT check enableTerminationProtection
    SVC-->>CDK: Stack deleted (should be ValidationError)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CDK as CDK Client
    participant QC as AwsQueryController
    participant QH as CloudFormationQueryHandler
    participant SVC as CloudFormationService
    participant Store as StorageBackend

    CDK->>QC: "POST Action=UpdateTerminationProtection"
    QC->>QH: handle("UpdateTerminationProtection", params, region)
    QH->>SVC: updateTerminationProtection(stackName, enabled, region)
    SVC->>SVC: getStackOrThrow(stackName, region)
    SVC->>SVC: stack.setEnableTerminationProtection(enabled)
    SVC->>Store: persistStack(stack)
    SVC-->>QH: Stack
    QH-->>CDK: 200 XML UpdateTerminationProtectionResult(StackId)

    CDK->>QC: "POST Action=DescribeStacks"
    QC->>QH: handle("DescribeStacks", params, region)
    QH->>SVC: describeStacks(stackName, region)
    SVC-->>QH: List[Stack]
    QH-->>CDK: 200 XML (includes EnableTerminationProtection)

    CDK->>QC: "POST Action=DeleteStack"
    QC->>QH: handle("DeleteStack", params, region)
    QH->>SVC: deleteStack(stackName, region)
    Note over SVC: Does NOT check enableTerminationProtection
    SVC-->>CDK: Stack deleted (should be ValidationError)
Loading

Comments Outside Diff (1)

  1. src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationService.java, line 298-308 (link)

    P1 deleteStack does not enforce termination protection

    updateTerminationProtection persists the flag but deleteStack never reads it. In real AWS, calling DeleteStack on a protection-enabled stack returns a ValidationError: Stack [...] cannot be deleted while TerminationProtection is enabled. Without this guard, the flag is stored but has no behavioral effect — a client that enables protection (e.g. cdk bootstrap) will not be guarded against accidental deletion, which is the entire purpose of the feature.

Reviews (2): Last reviewed commit: "Merge branch 'main' into feat/cloudforma..." | Re-trigger Greptile

Comment on lines +157 to +170
private Response updateTerminationProtection(MultivaluedMap<String, String> params, String region) {
String stackName = params.getFirst("StackName");
boolean enabled = Boolean.parseBoolean(params.getFirst("EnableTerminationProtection"));
Stack stack = cfnService.updateTerminationProtection(stackName, enabled, region);
String xml = new XmlBuilder()
.start("UpdateTerminationProtectionResponse", CF_NS)
.start("UpdateTerminationProtectionResult")
.elem("StackId", stack.getStackId())
.end("UpdateTerminationProtectionResult")
.raw(AwsQueryResponse.responseMetadata())
.end("UpdateTerminationProtectionResponse")
.build();
return Response.ok(xml).type("text/xml").build();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The handler does not catch AwsException, so calling UpdateTerminationProtection with a non-existent stack name lets the exception propagate to AwsExceptionMapper, which always returns application/json. For a CloudFormation Query-protocol endpoint the error must be XML; the AWS SDK will fail to deserialize a JSON error body. Every other handler that calls a service method guarded by getStackOrThrow (describeStacks, describeChangeSet, executeChangeSet, deleteChangeSet) wraps the call in catch (AwsException e) { return xmlError(...); } for exactly this reason.

Suggested change
private Response updateTerminationProtection(MultivaluedMap<String, String> params, String region) {
String stackName = params.getFirst("StackName");
boolean enabled = Boolean.parseBoolean(params.getFirst("EnableTerminationProtection"));
Stack stack = cfnService.updateTerminationProtection(stackName, enabled, region);
String xml = new XmlBuilder()
.start("UpdateTerminationProtectionResponse", CF_NS)
.start("UpdateTerminationProtectionResult")
.elem("StackId", stack.getStackId())
.end("UpdateTerminationProtectionResult")
.raw(AwsQueryResponse.responseMetadata())
.end("UpdateTerminationProtectionResponse")
.build();
return Response.ok(xml).type("text/xml").build();
}
private Response updateTerminationProtection(MultivaluedMap<String, String> params, String region) {
String stackName = params.getFirst("StackName");
boolean enabled = Boolean.parseBoolean(params.getFirst("EnableTerminationProtection"));
try {
Stack stack = cfnService.updateTerminationProtection(stackName, enabled, region);
String xml = new XmlBuilder()
.start("UpdateTerminationProtectionResponse", CF_NS)
.start("UpdateTerminationProtectionResult")
.elem("StackId", stack.getStackId())
.end("UpdateTerminationProtectionResult")
.raw(AwsQueryResponse.responseMetadata())
.end("UpdateTerminationProtectionResponse")
.build();
return Response.ok(xml).type("text/xml").build();
} catch (AwsException e) {
return xmlError(e.getErrorCode(), e.getMessage(), e.getHttpStatus());
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant