-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
339 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
ISC License | ||
|
||
Copyright (c) 2024 Artyom Pervukhin | ||
|
||
Permission to use, copy, modify, and/or distribute this software for any | ||
purpose with or without fee is hereby granted, provided that the above | ||
copyright notice and this permission notice appear in all copies. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | ||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | ||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | ||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | ||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | ||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | ||
PERFORMANCE OF THIS SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Update CloudFormation Stack Parameter Action | ||
|
||
This GitHub Action updates a single parameter in an existing CloudFormation stack while preserving all other settings. | ||
|
||
## Usage | ||
|
||
```yaml | ||
- uses: artyom/update-cloudformation-stack@main | ||
with: | ||
stack: my-stack-name | ||
key: ParameterName | ||
value: NewValue | ||
``` | ||
## Inputs | ||
- `stack` - Name of the CloudFormation stack to update | ||
- `key` - Name of the stack parameter to update | ||
- `value` - New value to set for the parameter | ||
|
||
## AWS Credentials | ||
|
||
This action uses the AWS SDK default credential provider chain. Configure AWS credentials using standard GitHub Actions methods: | ||
|
||
```yaml | ||
- uses: aws-actions/configure-aws-credentials@v4 | ||
with: | ||
role-to-assume: arn:aws:iam::123456789012:role/my-role | ||
aws-region: us-east-1 | ||
``` | ||
|
||
## AWS Permissions | ||
|
||
This action requires the following permissions: | ||
|
||
- cloudformation:DescribeStacks | ||
- cloudformation:UpdateStack | ||
- cloudformation:DescribeStackEvents | ||
|
||
## Example | ||
|
||
```yaml | ||
jobs: | ||
update-stack: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: aws-actions/configure-aws-credentials@v4 | ||
with: | ||
role-to-assume: arn:aws:iam::123456789012:role/my-role | ||
aws-region: us-east-1 | ||
- uses: artyom/update-cloudformation-stack@main | ||
with: | ||
stack: production-stack | ||
key: ImageTag | ||
value: v123 | ||
``` | ||
|
||
The action will monitor stack update progress and fail if update fails. If parameter already has the requested value, action will exit with a warning message. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
name: Update a single parameter in an existing CloudFormation stack while preserving all other settings | ||
description: > | ||
Updates a single parameter in an existing CloudFormation stack while preserving all other settings. | ||
inputs: | ||
stack: | ||
description: CloudFormation stack name | ||
required: true | ||
key: | ||
description: Name of stack parameter to update | ||
required: true | ||
value: | ||
description: New value to set for a given parameter | ||
required: true | ||
|
||
runs: | ||
using: composite | ||
steps: | ||
- uses: actions/setup-go@v5 | ||
with: | ||
go-version: 'stable' | ||
cache-dependency-path: ${{ github.action_path }}/go.sum | ||
- shell: bash | ||
run: | | ||
go run -C ${{ github.action_path }} . -stack=${{ inputs.stack }} -key=${{ inputs.key }} -value=${{ inputs.value }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
module github.com/artyom/update-cloudformation-stack | ||
|
||
go 1.23.3 | ||
|
||
require ( | ||
github.com/aws/aws-sdk-go-v2 v1.32.5 | ||
github.com/aws/aws-sdk-go-v2/config v1.28.5 | ||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.0 | ||
github.com/aws/smithy-go v1.22.1 | ||
) | ||
|
||
require ( | ||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect | ||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect | ||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect | ||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect | ||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect | ||
github.com/jmespath/go-jmespath v0.4.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= | ||
github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= | ||
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= | ||
github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= | ||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= | ||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= | ||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= | ||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= | ||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= | ||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= | ||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= | ||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= | ||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= | ||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= | ||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.0 h1:zmXJiEm/fQYtFDLIUsZrcPIjTrL3R/noFICGlYBj3Ww= | ||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.0/go.mod h1:9nOjXCDKE+QMK4JaCrLl36PU+VEfJmI7WVehYmojO8s= | ||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= | ||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= | ||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= | ||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= | ||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= | ||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= | ||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= | ||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= | ||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= | ||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= | ||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= | ||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= | ||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= | ||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= | ||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= | ||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= | ||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"encoding/hex" | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"log" | ||
"os" | ||
"time" | ||
|
||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/config" | ||
"github.com/aws/aws-sdk-go-v2/service/cloudformation" | ||
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types" | ||
"github.com/aws/smithy-go" | ||
) | ||
|
||
func main() { | ||
log.SetFlags(0) | ||
var args runArgs | ||
flag.StringVar(&args.stack, "stack", args.stack, "name of the CloudFormation stack to update") | ||
flag.StringVar(&args.key, "key", args.key, "parameter name to update") | ||
flag.StringVar(&args.value, "value", args.value, "parameter value to set") | ||
flag.Parse() | ||
if err := run(context.Background(), args); err != nil { | ||
if errors.Is(err, errAlreadySet) { | ||
log.Print(githubWarnPrefix, err) | ||
return | ||
} | ||
var ae smithy.APIError | ||
if errors.As(err, &ae) && ae.ErrorCode() == "ValidationError" && ae.ErrorMessage() == "No updates are to be performed." { | ||
log.Print(githubWarnPrefix, "nothing to update") | ||
return | ||
} | ||
log.Fatal(githubErrPrefix, err) | ||
} | ||
} | ||
|
||
type runArgs struct { | ||
stack string | ||
key string | ||
value string | ||
} | ||
|
||
var errAlreadySet = errors.New("stack alerady has required parameter value") | ||
|
||
func run(ctx context.Context, args runArgs) error { | ||
if err := args.validate(); err != nil { | ||
return err | ||
} | ||
cfg, err := config.LoadDefaultConfig(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
svc := cloudformation.NewFromConfig(cfg) | ||
|
||
desc, err := svc.DescribeStacks(ctx, &cloudformation.DescribeStacksInput{StackName: &args.stack}) | ||
if err != nil { | ||
return err | ||
} | ||
if l := len(desc.Stacks); l != 1 { | ||
return fmt.Errorf("DescribeStacks returned %d stacks, expected 1", l) | ||
} | ||
|
||
var params []types.Parameter | ||
var seenKey bool | ||
for _, p := range desc.Stacks[0].Parameters { | ||
k := aws.ToString(p.ParameterKey) | ||
if k == args.key && aws.ToString(p.ParameterValue) == args.value { | ||
return errAlreadySet | ||
} | ||
if k == args.key { | ||
seenKey = true | ||
continue | ||
} | ||
params = append(params, types.Parameter{ParameterKey: &k, UsePreviousValue: aws.Bool(true)}) | ||
} | ||
if !seenKey { | ||
return errors.New("stack has no parameter with the given key") | ||
} | ||
params = append(params, types.Parameter{ParameterKey: &args.key, ParameterValue: &args.value}) | ||
|
||
token := newToken() | ||
_, err = svc.UpdateStack(ctx, &cloudformation.UpdateStackInput{ | ||
StackName: &args.stack, | ||
ClientRequestToken: &token, | ||
UsePreviousTemplate: aws.Bool(true), | ||
Parameters: params, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
log.Print("polling for stack updates until it's ready, this may take a while") | ||
debugf := func(format string, args ...any) { | ||
if !underGithub { | ||
return | ||
} | ||
log.Printf("::debug::"+format, args...) | ||
} | ||
oldEventsCutoff := time.Now().Add(-time.Hour) | ||
ticker := time.NewTicker(20 * time.Second) | ||
defer ticker.Stop() | ||
for { | ||
select { | ||
case <-ticker.C: | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
} | ||
p := cloudformation.NewDescribeStackEventsPaginator(svc, &cloudformation.DescribeStackEventsInput{StackName: &args.stack}) | ||
scanEvents: | ||
for p.HasMorePages() { | ||
page, err := p.NextPage(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
for _, evt := range page.StackEvents { | ||
if evt.Timestamp != nil && aws.ToTime(evt.Timestamp).Before(oldEventsCutoff) { | ||
break scanEvents | ||
} | ||
if evt.ClientRequestToken == nil || *evt.ClientRequestToken != token { | ||
continue | ||
} | ||
if evt.ResourceStatus == types.ResourceStatusUpdateFailed { | ||
return fmt.Errorf("%v: %s", evt.ResourceStatus, aws.ToString(evt.ResourceStatusReason)) | ||
} | ||
debugf("%s\t%s\t%v", aws.ToString(evt.ResourceType), aws.ToString(evt.LogicalResourceId), evt.ResourceStatus) | ||
if aws.ToString(evt.LogicalResourceId) == args.stack && aws.ToString(evt.ResourceType) == "AWS::CloudFormation::Stack" { | ||
switch evt.ResourceStatus { | ||
case types.ResourceStatusUpdateComplete: | ||
return nil | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
func newToken() string { | ||
b := make([]byte, 20) | ||
if _, err := rand.Read(b); err != nil { | ||
panic(err) | ||
} | ||
return "ucs-" + hex.EncodeToString(b) | ||
} | ||
|
||
func (a *runArgs) validate() error { | ||
if a.stack == "" || a.key == "" || a.value == "" { | ||
return errors.New("stack, key, and value cannot be empty") | ||
} | ||
return nil | ||
} | ||
|
||
func init() { | ||
const usage = `Updates a single parameter in an existing CloudFormation stack while preserving all other settings. | ||
Usage: update-cloudformation-stack -stack NAME -key PARAM -value VALUE | ||
` | ||
flag.Usage = func() { | ||
fmt.Fprint(flag.CommandLine.Output(), usage) | ||
flag.PrintDefaults() | ||
} | ||
} | ||
|
||
var underGithub bool | ||
var githubWarnPrefix string | ||
var githubErrPrefix string | ||
|
||
func init() { | ||
underGithub = os.Getenv("GITHUB_ACTIONS") == "true" | ||
if underGithub { | ||
githubWarnPrefix = "::warning::" | ||
githubErrPrefix = "::error::" | ||
} | ||
} |