Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ This action uses the AWS SDK default credential provider chain. Configure AWS cr
This action requires the following permissions:

- cloudformation:DescribeStacks
- cloudformation:GetTemplate
- cloudformation:UpdateStack
- cloudformation:DescribeStackEvents

Expand Down Expand Up @@ -62,6 +63,6 @@ The action will monitor stack update progress and fail if update fails.
This action doesn't work for stacks that rely on template transformations
(stack template has non-empty [“Transform” section][transform]).

On such stacks UpdateStack API call does not recognize parameters-only changes and the action reports there's nothing to update.
On such stacks the UpdateStack API call does not recognize parameters-only changes. The action detects a non-empty Transform section up front and fails with a clear error instead of silently reporting there's nothing to update.

[transform]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-section-structure.html
42 changes: 42 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
Expand Down Expand Up @@ -66,6 +67,18 @@ func run(ctx context.Context, stackName string, args []string) error {
return fmt.Errorf("DescribeStacks returned %d stacks, expected 1", l)
}
stack := desc.Stacks[0]

tmpl, err := svc.GetTemplate(ctx, &cloudformation.GetTemplateInput{
StackName: &stackName,
TemplateStage: types.TemplateStageOriginal,
})
if err != nil {
return err
}
if hasTransform(unptr(tmpl.TemplateBody)) {
return errors.New("stack relies on template transformations (non-empty Transform section); parameters-only updates are not supported, see README")
}

var params []types.Parameter
for _, p := range stack.Parameters {
k := unptr(p.ParameterKey)
Expand Down Expand Up @@ -207,6 +220,35 @@ func parseKvs(list []string) (map[string]string, error) {
return out, nil
}

// hasTransform reports whether a CloudFormation template body declares a
// non-empty top-level Transform section. Templates may be JSON or YAML; YAML
// short-form intrinsics (!Ref, !GetAtt, …) make full parsing impractical, so the
// YAML path only looks for a top-level Transform key.
func hasTransform(body string) bool {
trimmed := strings.TrimSpace(body)
if strings.HasPrefix(trimmed, "{") {
var doc struct {
Transform json.RawMessage `json:"Transform"`
}
if json.Unmarshal([]byte(trimmed), &doc) != nil {
return false // let UpdateStack surface a malformed template
}
switch strings.TrimSpace(string(doc.Transform)) {
case "", "null", "[]", `""`:
return false
default:
return true
}
}
for _, line := range strings.Split(body, "\n") {
line = strings.TrimRight(line, "\r")
if strings.HasPrefix(line, "Transform:") || strings.HasPrefix(line, "Transform ") {
return true
}
}
return false
}

func unptr[T any](v *T) T {
var zero T
if v != nil {
Expand Down
24 changes: 24 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,27 @@ func Test_parseKvs(t *testing.T) {
}
}
}

func Test_hasTransform(t *testing.T) {
for _, tc := range []struct {
name string
body string
want bool
}{
{name: "yaml with transform", body: "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nResources: {}\n", want: true},
{name: "yaml transform list", body: "Transform:\n - AWS::Serverless-2016-10-31\nResources: {}\n", want: true},
{name: "yaml no transform", body: "AWSTemplateFormatVersion: '2010-09-09'\nResources:\n Q:\n Type: AWS::SQS::Queue\n", want: false},
{name: "yaml nested transform key", body: "Resources:\n Transform: something\n", want: false},
{name: "json with transform", body: `{"Transform": "AWS::Serverless-2016-10-31", "Resources": {}}`, want: true},
{name: "json transform list", body: `{"Transform": ["AWS::Serverless-2016-10-31"], "Resources": {}}`, want: true},
{name: "json no transform", body: `{"Resources": {}}`, want: false},
{name: "json null transform", body: `{"Transform": null, "Resources": {}}`, want: false},
{name: "empty", body: "", want: false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := hasTransform(tc.body); got != tc.want {
t.Errorf("hasTransform() = %v, want %v", got, tc.want)
}
})
}
}