├── .gitignore ├── .golangci.yml ├── cmd └── cfnctl │ └── main.go ├── CONTRIBUTING.md ├── didyoumean ├── suggestions.go └── suggestions_test.go ├── aws └── aws.go ├── commands ├── validate.go ├── version_test.go ├── testdata │ └── template.yaml ├── plan_test.go ├── output_test.go ├── helper.go ├── destroy_test.go ├── output.go ├── apply_test.go ├── version.go ├── apply.go ├── destroy.go └── plan.go ├── Makefile ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ └── ci.yml ├── .goreleaser.yaml ├── utils └── utils.go ├── go.mod ├── cli ├── types.go ├── params │ └── params.go └── cli.go ├── pkg └── client │ ├── types.go │ └── api.go ├── internal ├── interactive │ └── table.go └── mock │ └── mocksvc.go ├── README.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── go.sum └── cfnctl.svg /.gitignore: -------------------------------------------------------------------------------- 1 | slask 2 | main 3 | cfnctl 4 | .vscode 5 | tags 6 | 7 | dist/ 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | staticcheck: 3 | checks: 4 | - all 5 | - '-SA1006' # disable the rule SA1000 6 | 7 | -------------------------------------------------------------------------------- /cmd/cfnctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rogerwelin/cfnctl/cli" 7 | ) 8 | 9 | func main() { 10 | cli.RunCLI(os.Args) 11 | } 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions are welcome! To request a feature create a new issue with the label feature-request. Find a bug? Please add an issue with the label bugs. Pull requests are also welcomed but please add an issue on the requested feature first (unless it's a simple bug fix or readme change) 2 | 3 | -------------------------------------------------------------------------------- /didyoumean/suggestions.go: -------------------------------------------------------------------------------- 1 | package didyoumean 2 | 3 | import ( 4 | "github.com/agext/levenshtein" 5 | ) 6 | 7 | // NameSuggestion takes available commands and the mis-spelled command and returns closest match 8 | func NameSuggestion(given string, suggestions []string) string { 9 | for _, suggestion := range suggestions { 10 | dist := levenshtein.Distance(given, suggestion, nil) 11 | if dist < 3 { 12 | return suggestion 13 | } 14 | } 15 | return "" 16 | } 17 | -------------------------------------------------------------------------------- /aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/config" 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 8 | ) 9 | 10 | // NewAWS returns a new cloudformation client 11 | func NewAWS() (*cloudformation.Client, error) { 12 | cfg, err := config.LoadDefaultConfig(context.TODO()) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | // if we pass in a profile 18 | // apa, err := config.LoadSharedConfigProfile() 19 | 20 | svc := cloudformation.NewFromConfig(cfg) 21 | 22 | return svc, nil 23 | } 24 | -------------------------------------------------------------------------------- /commands/validate.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/rogerwelin/cfnctl/aws" 5 | "github.com/rogerwelin/cfnctl/pkg/client" 6 | "github.com/rogerwelin/cfnctl/utils" 7 | ) 8 | 9 | // Validate validates a given CF template 10 | func Validate(templatePath string) error { 11 | svc, err := aws.NewAWS() 12 | if err != nil { 13 | return err 14 | } 15 | dat, err := utils.ReadFile(templatePath) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | ctl := &client.Cfnctl{ 21 | Svc: svc, 22 | TemplateBody: string(dat), 23 | } 24 | 25 | err = ctl.ValidateCFTemplate() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | check: test lint vet 2 | 3 | .PHONY: test 4 | test: 5 | go test -cover -race -v ./... 6 | 7 | .PHONY: lint 8 | lint: 9 | golint ./... 10 | 11 | .PHONY: vet 12 | vet: 13 | go vet ./... 14 | 15 | .PHONY: build 16 | build: 17 | CGO_ENABLED=0 go build ./cmd/cfnctl 18 | 19 | # ==================================================================================== # 20 | # QUALITY CONTROL 21 | # ==================================================================================== # 22 | 23 | .PHONY: audit 24 | audit: 25 | @echo 'Tidying and verifying module dependencies...' 26 | go mod tidy 27 | go mod verify 28 | @echo 'Vetting code...' 29 | go vet ./... 30 | staticcheck ./... 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /commands/version_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestVersion(t *testing.T) { 9 | 10 | expectedStr := "Cfnctl version v0.1.1\n" 11 | buf := &bytes.Buffer{} 12 | err := OutputVersion("0.1.1", buf) 13 | if err != nil { 14 | t.Errorf("Expected ok but got: %v", err) 15 | } 16 | 17 | if buf.String() != expectedStr { 18 | t.Errorf("Expected str:\n %s but got:\n %s", expectedStr, buf.String()) 19 | } 20 | 21 | expectedStr2 := "Cfnctl version v1.0.0\n" 22 | buf.Reset() 23 | 24 | err = OutputVersion("1.0.0", buf) 25 | if err != nil { 26 | t.Errorf("Expected ok but got: %v", err) 27 | } 28 | 29 | if buf.String() != expectedStr2 { 30 | t.Errorf("Expected str:\n%s but got:\n %s", expectedStr2, buf.String()) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /didyoumean/suggestions_test.go: -------------------------------------------------------------------------------- 1 | package didyoumean 2 | 3 | import "testing" 4 | 5 | var suggestions = []struct { 6 | in []string 7 | given string 8 | expected string 9 | }{ 10 | {[]string{"apply", "delete", "plan", "validate", "version"}, "verzion", "version"}, 11 | {[]string{"apply", "delete", "plan", "validate", "version"}, "aply", "apply"}, 12 | {[]string{"apply", "delete", "plan", "validate", "version"}, "pan", "plan"}, 13 | {[]string{"apply", "delete", "plan", "validate", "version"}, "gibberish", ""}, 14 | } 15 | 16 | func TestSuggestions(t *testing.T) { 17 | for i, tt := range suggestions { 18 | actual := NameSuggestion(tt.given, tt.in) 19 | if actual != tt.expected { 20 | t.Errorf("test %d: NameSuggestion(%s, %v): expected %s but got %s", i+1, tt.given, tt.in, tt.expected, actual) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /commands/testdata/template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | 4 | 5 | Resources: 6 | 7 | CloudwatchRole: 8 | Type: AWS::IAM::Role 9 | Properties: 10 | RoleName: CloudWatch-Role 11 | AssumeRolePolicyDocument: 12 | Version: '2012-10-17' 13 | Statement: 14 | - Effect: Allow 15 | Principal: 16 | Service: 17 | - ec2.amazonaws.com 18 | Action: 19 | - sts:AssumeRole 20 | Path: "/" 21 | ManagedPolicyArns: 22 | - arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess 23 | - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess 24 | 25 | Bucket: 26 | Type: AWS::S3::Bucket 27 | 28 | 29 | Outputs: 30 | Bucket: 31 | Description: S3 Bucket arn 32 | Value: !GetAtt Bucket.Arn 33 | Export: 34 | Name: Bucket 35 | 36 | -------------------------------------------------------------------------------- /commands/plan_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/rogerwelin/cfnctl/internal/mock" 8 | "github.com/rogerwelin/cfnctl/pkg/client" 9 | ) 10 | 11 | func TestPlan(t *testing.T) { 12 | 13 | expectedStr := "\nCfnctl will perform the following actions:\n\n\nPlan: 0 to add, 0 to change, 0 to destroy\n\n" 14 | 15 | svc := mock.NewMockAPI() 16 | buf := &bytes.Buffer{} 17 | 18 | ctl := client.New( 19 | client.WithSvc(svc), 20 | client.WithStackName("stack"), 21 | client.WithChangesetName("change-stack"), 22 | client.WithTemplatePath("testdata/template.yaml"), 23 | client.WithAutoApprove(true), 24 | client.WithOutput(buf), 25 | ) 26 | 27 | _, err := Plan(ctl, false) 28 | if err != nil { 29 | t.Errorf("Expected err to be nil but got: %v", err) 30 | } 31 | 32 | if buf.String() != expectedStr { 33 | t.Errorf("Expected str:\n%s but got:\n %s", expectedStr, buf.String()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /commands/output_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/rogerwelin/cfnctl/internal/mock" 9 | "github.com/rogerwelin/cfnctl/pkg/client" 10 | ) 11 | 12 | func TestOutput(t *testing.T) { 13 | 14 | svc := mock.NewMockAPI() 15 | buf := &bytes.Buffer{} 16 | 17 | ctl := client.New( 18 | client.WithSvc(svc), 19 | client.WithStackName("stack"), 20 | client.WithChangesetName("change-stack"), 21 | client.WithTemplatePath("testdata/template.yaml"), 22 | client.WithAutoApprove(true), 23 | client.WithOutput(buf), 24 | ) 25 | 26 | err := Output(ctl) 27 | if err != nil { 28 | t.Errorf("Expected err to be nil but got: %v\n", err) 29 | } 30 | 31 | if !strings.Contains(buf.String(), "Bucket") { 32 | t.Error("Output did not contain expected name: Bucket") 33 | } 34 | 35 | if !strings.Contains(buf.String(), "TestBucket") { 36 | t.Error("Output did not contain expected value: TestBucket") 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - linux 9 | - windows 10 | - darwin 11 | main: ./cmd/cfnctl 12 | binary: cfnctl 13 | archives: 14 | - format: tar.gz 15 | name_template: >- 16 | {{ .ProjectName }}_ 17 | {{- title .Os }}_ 18 | {{- if eq .Arch "amd64" }}x86_64 19 | {{- else if eq .Arch "386" }}i386 20 | {{- else }}{{ .Arch }}{{ end }} 21 | {{- if .Arm }}v{{ .Arm }}{{ end }} 22 | # use zip for windows archives 23 | format_overrides: 24 | - goos: windows 25 | format: zip 26 | checksum: 27 | name_template: 'checksums.txt' 28 | snapshot: 29 | name_template: "{{ incpatch .Version }}-next" 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | - 'LICENSE' 37 | - 'Makefile' 38 | - 'README.md' 39 | - 'CONTRIBUTING.md' 40 | - 'CODE_OF_CONDUCT.md' 41 | -------------------------------------------------------------------------------- /commands/helper.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rogerwelin/cfnctl/aws" 7 | "github.com/rogerwelin/cfnctl/pkg/client" 8 | "github.com/rogerwelin/cfnctl/utils" 9 | ) 10 | 11 | // CommandBuilder returns a new Cfnctl svc 12 | func CommandBuilder(templateFile, varsFile string, autoApprove bool) (*client.Cfnctl, error) { 13 | 14 | svc, err := aws.NewAWS() 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | templateBody, err := utils.ReadFile(templateFile) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | stackName := utils.TrimFileSuffix(templateFile) 25 | 26 | ctl := client.New( 27 | client.WithSvc(svc), 28 | client.WithTemplateBody(string(templateBody)), 29 | client.WithTemplatePath(templateFile), 30 | client.WithStackName(stackName), 31 | client.WithChangesetName(stackName), 32 | client.WithVarsFile(varsFile), 33 | client.WithAutoApprove(autoApprove), 34 | client.WithOutput(os.Stdout), 35 | ) 36 | 37 | return ctl, nil 38 | } 39 | -------------------------------------------------------------------------------- /commands/destroy_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/rogerwelin/cfnctl/internal/mock" 8 | "github.com/rogerwelin/cfnctl/pkg/client" 9 | ) 10 | 11 | func TestDestroy(t *testing.T) { 12 | 13 | expectedStr := "\nNo changes. No objects need to be destroyed\n\nEither you have not created any objects yet, there is no Stack named stack or the existing objects were already deleted outside of Cfnctl\n\nDestroy complete! Resources: 0 destroyed\n" 14 | 15 | svc := mock.NewMockAPI() 16 | buf := &bytes.Buffer{} 17 | 18 | ctl := client.New( 19 | client.WithSvc(svc), 20 | client.WithStackName("stack"), 21 | client.WithChangesetName("change-stack"), 22 | client.WithTemplatePath("testdata/template.yaml"), 23 | client.WithAutoApprove(true), 24 | client.WithOutput(buf), 25 | ) 26 | 27 | err := Destroy(ctl) 28 | if err != nil { 29 | t.Errorf("Expected err to be nil but got: %v", err) 30 | } 31 | 32 | if buf.String() != expectedStr { 33 | t.Errorf("Expected str:\n%s but got:\n %s", expectedStr, buf.String()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /commands/output.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | "github.com/olekukonko/tablewriter" 9 | "github.com/rogerwelin/cfnctl/pkg/client" 10 | ) 11 | 12 | func outputExportTable(values []types.Export, writer io.Writer) { 13 | tableData := [][]string{} 14 | table := tablewriter.NewWriter(writer) 15 | table.SetHeader([]string{"Name", "Value"}) 16 | table.SetHeaderColor( 17 | tablewriter.Colors{tablewriter.Bold}, 18 | tablewriter.Colors{tablewriter.Bold}, 19 | ) 20 | 21 | for _, item := range values { 22 | arr := []string{ 23 | *item.Name, 24 | *item.Value, 25 | } 26 | tableData = append(tableData, arr) 27 | } 28 | 29 | for _, item := range tableData { 30 | table.Append(item) 31 | } 32 | 33 | fmt.Printf("\n") 34 | table.Render() 35 | fmt.Printf("\n") 36 | } 37 | 38 | func Output(ctl *client.Cfnctl) error { 39 | out, err := ctl.ListExportValues() 40 | 41 | if err != nil { 42 | return nil 43 | } 44 | 45 | if len(out) == 0 { 46 | fmt.Fprintf(ctl.Output, "No exported values in the selected region") 47 | } 48 | 49 | outputExportTable(out, ctl.Output) 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // ReadFile is a utility function for reading files 12 | func ReadFile(path string) ([]byte, error) { 13 | dat, err := os.ReadFile(path) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return dat, nil 18 | } 19 | 20 | // ReturnRandom returns a ranomized string with the length based on input 21 | func ReturnRandom(value int) string { 22 | stringArr := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T"} 23 | newString := "" 24 | 25 | for i := 0; i <= value; i++ { 26 | s1 := rand.NewSource(time.Now().UnixNano()) 27 | r1 := rand.New(s1) 28 | randIndex := r1.Intn(len(stringArr)) 29 | newString = newString + stringArr[randIndex] 30 | } 31 | return newString 32 | } 33 | 34 | // TrimFileSuffix trims the suffix on the file 35 | func TrimFileSuffix(path string) string { 36 | file := filepath.Base(path) 37 | return strings.TrimSuffix(file, filepath.Ext(file)) 38 | } 39 | -------------------------------------------------------------------------------- /commands/apply_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/rogerwelin/cfnctl/internal/mock" 8 | "github.com/rogerwelin/cfnctl/pkg/client" 9 | ) 10 | 11 | func TestApply(t *testing.T) { 12 | 13 | var expectedStr = ` 14 | Cfnctl will perform the following actions: 15 | 16 | 17 | Plan: 0 to add, 0 to change, 0 to destroy 18 | 19 | 20 | No changes. Your infrastructure matches the configuration 21 | 22 | Cfnctl has compared your real infrastructure against your configuration and found no differences, so no changes are needed. 23 | 24 | Apply complete! Resources: 0 added, 0 changed, 0 destroyed 25 | ` 26 | svc := mock.NewMockAPI() 27 | buf := &bytes.Buffer{} 28 | 29 | ctl := client.New( 30 | client.WithSvc(svc), 31 | client.WithStackName("stack"), 32 | client.WithChangesetName("change-stack"), 33 | client.WithTemplatePath("testdata/template.yaml"), 34 | client.WithAutoApprove(true), 35 | client.WithOutput(buf), 36 | ) 37 | 38 | err := Apply(ctl) 39 | if err != nil { 40 | t.Errorf("Expected str:\n%s but got:\n %s", expectedStr, buf.String()) 41 | } 42 | 43 | if buf.String() != expectedStr { 44 | t.Fatal("apply output was not expected string") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/hashicorp/go-version" 10 | ) 11 | 12 | var latestVersion = "https://api.github.com/repos/rogerwelin/cfnctl/releases/latest" 13 | 14 | type githubRelease struct { 15 | TagName string `json:"tag_name"` 16 | Name string `json:"name"` 17 | } 18 | 19 | // OutputVersion queries github releases and will output whenever there are a newer release available 20 | func OutputVersion(ver string, writer io.Writer) error { 21 | resp, err := http.Get(latestVersion) 22 | if err != nil { 23 | return err 24 | } 25 | defer resp.Body.Close() 26 | v := &githubRelease{} 27 | err = json.NewDecoder(resp.Body).Decode(v) 28 | 29 | if err != nil { 30 | return err 31 | } 32 | 33 | v1, err := version.NewVersion(ver) 34 | if err != nil { 35 | return err 36 | } 37 | v2, err := version.NewVersion(v.TagName) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if v1.LessThan(v2) { 43 | fmt.Fprintf(writer, "Cfnctl version v%s\n\nYour version of Cfnctl is out of date. The latest version is v%s\n", v1, v2) 44 | } else { 45 | fmt.Fprintf(writer, "Cfnctl version v%s\n", v1) 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref_name }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | golangci: 17 | name: lint 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 5 20 | strategy: 21 | fail-fast: true 22 | matrix: 23 | go: ['1.20.x'] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Install Go 30 | uses: actions/setup-go@v4 31 | with: 32 | go-version: ${{ matrix.go }} 33 | check-latest: true 34 | 35 | - name: Go Tidy 36 | run: go mod tidy && git diff --exit-code 37 | 38 | - name: Go Mod 39 | run: go mod download 40 | 41 | # catch supply chain attacks 42 | - name: Go Mod Verify 43 | run: go mod verify 44 | 45 | - name: Lint 46 | uses: golangci/golangci-lint-action@v2 47 | with: 48 | version: latest 49 | 50 | test: 51 | name: test 52 | runs-on: ubuntu-latest 53 | timeout-minutes: 5 54 | strategy: 55 | fail-fast: true 56 | matrix: 57 | go: ['stable', 'oldstable'] 58 | 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v3 62 | 63 | - name: Install Go 64 | uses: actions/setup-go@v4 65 | with: 66 | go-version: ${{ matrix.go }} 67 | check-latest: true 68 | 69 | - name: Test 70 | run: make test 71 | 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rogerwelin/cfnctl 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/agext/levenshtein v1.2.3 7 | github.com/aws/aws-sdk-go-v2 v1.4.0 8 | github.com/aws/aws-sdk-go-v2/config v1.1.7 9 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.4.0 10 | github.com/aws/smithy-go v1.4.0 11 | github.com/awslabs/goformation/v4 v4.19.1 12 | github.com/buger/goterm v1.0.4 13 | github.com/fatih/color v1.10.0 14 | github.com/hashicorp/go-version v1.3.0 15 | github.com/manifoldco/promptui v0.8.0 16 | github.com/olekukonko/tablewriter v0.0.5 17 | github.com/urfave/cli/v2 v2.3.0 18 | gopkg.in/yaml.v2 v2.4.0 19 | ) 20 | 21 | require ( 22 | github.com/aws/aws-sdk-go-v2/credentials v1.1.7 // indirect 23 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.7 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.7 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/sso v1.1.6 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.3.1 // indirect 27 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 28 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 29 | github.com/imdario/mergo v0.3.12 // indirect 30 | github.com/jmespath/go-jmespath v0.4.0 // indirect 31 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect 32 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect 33 | github.com/mattn/go-colorable v0.1.8 // indirect 34 | github.com/mattn/go-isatty v0.0.17 // indirect 35 | github.com/mattn/go-runewidth v0.0.14 // indirect 36 | github.com/rivo/uniseg v0.2.0 // indirect 37 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 38 | github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b // indirect 39 | github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 // indirect 40 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 41 | golang.org/x/sys v0.6.0 // indirect 42 | golang.org/x/text v0.3.7 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /cli/types.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | "github.com/rogerwelin/cfnctl/aws" 9 | "github.com/rogerwelin/cfnctl/commands" 10 | "github.com/rogerwelin/cfnctl/pkg/client" 11 | ) 12 | 13 | type Validate struct { 14 | TemplatePath string 15 | } 16 | 17 | type Plan struct { 18 | TemplatePath string 19 | ParamFile string 20 | } 21 | 22 | type Apply struct { 23 | AutoApprove bool 24 | TemplatePath string 25 | ParamFile string 26 | } 27 | 28 | type Destroy struct { 29 | AutoApprove bool 30 | TemplatePath string 31 | } 32 | 33 | type Output struct{} 34 | 35 | type Version struct { 36 | Version string 37 | } 38 | 39 | // Runner interface simplifies command interaction 40 | type Runner interface { 41 | Run() error 42 | } 43 | 44 | // Run executes the function receives command 45 | func (p *Plan) Run() error { 46 | ctl, err := commands.CommandBuilder(p.TemplatePath, p.ParamFile, false) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | _, err = commands.Plan(ctl, true) 52 | 53 | return err 54 | } 55 | 56 | // Run executes the function receives command 57 | func (v *Validate) Run() error { 58 | greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() 59 | err := commands.Validate(v.TemplatePath) 60 | if err != nil { 61 | return err 62 | } 63 | fmt.Printf("%s The configuration is valid.\n", greenBold("Success!")) 64 | return nil 65 | } 66 | 67 | // Run executes the function receives command 68 | func (a *Apply) Run() error { 69 | ctl, err := commands.CommandBuilder(a.TemplatePath, a.ParamFile, a.AutoApprove) 70 | if err != nil { 71 | return err 72 | } 73 | err = commands.Apply(ctl) 74 | return err 75 | } 76 | 77 | // Run executes the function receives command 78 | func (d *Destroy) Run() error { 79 | ctl, err := commands.CommandBuilder(d.TemplatePath, "", d.AutoApprove) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | err = commands.Destroy(ctl) 85 | return err 86 | } 87 | 88 | // Run executes the function receives command 89 | func (v *Version) Run() error { 90 | err := commands.OutputVersion(v.Version, os.Stdout) 91 | return err 92 | } 93 | 94 | // Run executes the function receives command 95 | func (o *Output) Run() error { 96 | svc, err := aws.NewAWS() 97 | if err != nil { 98 | return err 99 | } 100 | 101 | ctl := client.New( 102 | client.WithSvc(svc), 103 | client.WithOutput(os.Stdout), 104 | ) 105 | 106 | err = commands.Output(ctl) 107 | return err 108 | } 109 | -------------------------------------------------------------------------------- /pkg/client/types.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 8 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 9 | ) 10 | 11 | // CloudformationAPI provides access to AWS CloudFormation API 12 | type CloudformationAPI interface { 13 | ExecuteChangeSet(ctx context.Context, params *cloudformation.ExecuteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ExecuteChangeSetOutput, error) 14 | CreateChangeSet(ctx context.Context, params *cloudformation.CreateChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.CreateChangeSetOutput, error) 15 | DescribeChangeSet(ctx context.Context, params *cloudformation.DescribeChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeChangeSetOutput, error) 16 | DeleteChangeSet(ctx context.Context, params *cloudformation.DeleteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteChangeSetOutput, error) 17 | DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) 18 | DescribeStackResources(ctx context.Context, params *cloudformation.DescribeStackResourcesInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) 19 | ListChangeSets(ctx context.Context, params *cloudformation.ListChangeSetsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListChangeSetsOutput, error) 20 | ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) 21 | ValidateTemplate(ctx context.Context, params *cloudformation.ValidateTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ValidateTemplateOutput, error) 22 | ListExports(ctx context.Context, params *cloudformation.ListExportsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListExportsOutput, error) 23 | DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) 24 | } 25 | 26 | // Cfnctl provides access to all actions in the programs lifecycle 27 | type Cfnctl struct { 28 | AutoApprove bool 29 | VarsFile string 30 | StackName string 31 | ChangesetName string 32 | TemplateBody string 33 | TemplatePath string 34 | Parameters []types.Parameter 35 | Output io.Writer 36 | Svc CloudformationAPI 37 | DoDeleteChangeset bool 38 | } 39 | -------------------------------------------------------------------------------- /cli/params/params.go: -------------------------------------------------------------------------------- 1 | package params 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | "github.com/awslabs/goformation/v4" 9 | "github.com/fatih/color" 10 | "github.com/manifoldco/promptui" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | type parameters []struct { 15 | ParameterKey string `yaml:"ParameterKey"` 16 | ParameterValue string `yaml:"ParameterValue"` 17 | } 18 | 19 | // BuildInputParams builds CF parameter struct from given input 20 | func BuildInputParams(params []string) ([]types.Parameter, error) { 21 | res := make(map[string]string) 22 | var cfParams []types.Parameter 23 | whiteBold := color.New(color.Bold).SprintfFunc() 24 | 25 | if len(params) > 1 { 26 | fmt.Printf("%s\n\n", whiteBold("Enter parameter values:")) 27 | } else { 28 | fmt.Printf("%s\n\n", whiteBold("Enter parameter value:")) 29 | } 30 | 31 | for _, val := range params { 32 | p := promptui.Prompt{ 33 | Label: val, 34 | } 35 | result, err := p.Run() 36 | if err != nil { 37 | return nil, err 38 | } 39 | res[val] = result 40 | } 41 | 42 | for key, val := range res { 43 | param := types.Parameter{ 44 | ParameterKey: &key, 45 | ParameterValue: &val, 46 | } 47 | cfParams = append(cfParams, param) 48 | } 49 | 50 | return cfParams, nil 51 | } 52 | 53 | // CheckInputParams checks if the given CF template contains parameters or not 54 | func CheckInputParams(path string) (bool, []string, error) { 55 | var params []string 56 | 57 | template, err := goformation.Open(path) 58 | if err != nil { 59 | return false, nil, fmt.Errorf("could not open template, %w", err) 60 | } 61 | 62 | if len(template.Parameters) == 0 { 63 | return false, nil, nil 64 | } 65 | 66 | for key, val := range template.Parameters { 67 | if val.Default == nil { 68 | params = append(params, key) 69 | } 70 | } 71 | 72 | return true, params, nil 73 | } 74 | 75 | // MergeFileParams reads parameters from a separate file and returns CF API parameter type 76 | func MergeFileParams(path string) ([]types.Parameter, error) { 77 | var params []types.Parameter 78 | var paramStruct parameters 79 | paramFile, err := os.ReadFile(path) 80 | if err != nil { 81 | fmt.Println(err) 82 | return nil, err 83 | } 84 | 85 | err = yaml.Unmarshal(paramFile, ¶mStruct) 86 | if err != nil { 87 | fmt.Println(err) 88 | return nil, err 89 | } 90 | 91 | for _, val := range paramStruct { 92 | param := types.Parameter{ 93 | ParameterKey: &val.ParameterKey, 94 | ParameterValue: &val.ParameterValue, 95 | } 96 | params = append(params, param) 97 | } 98 | 99 | return params, nil 100 | } 101 | -------------------------------------------------------------------------------- /commands/apply.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/buger/goterm" 11 | "github.com/fatih/color" 12 | "github.com/rogerwelin/cfnctl/internal/interactive" 13 | "github.com/rogerwelin/cfnctl/pkg/client" 14 | ) 15 | 16 | // Apply executes a given CF template 17 | func Apply(ctl *client.Cfnctl) error { 18 | greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() 19 | whiteBold := color.New(color.Bold).SprintfFunc() 20 | 21 | eventsChan := make(chan interactive.StackResourceEvents) 22 | doneChan := make(chan bool) 23 | 24 | pc, err := Plan(ctl, false) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if !pc.containsChanges { 30 | fmt.Fprintf(ctl.Output, "\n%s. %s\n\n", greenBold("No changes"), whiteBold("Your infrastructure matches the configuration")) 31 | fmt.Fprintf(ctl.Output, "Cfnctl has compared your real infrastructure against your configuration and found no differences, so no changes are needed.\n") 32 | fmt.Fprintf(ctl.Output, "\n%s %d added, %d changed, %d destroyed\n", greenBold("Apply complete! Resources:"), (pc.changes["add"]), pc.changes["change"], pc.changes["destroy"]) 33 | return nil 34 | } 35 | 36 | if !ctl.AutoApprove { 37 | reader := bufio.NewReader(os.Stdin) 38 | 39 | fmt.Fprintf(ctl.Output, "%s\n"+ 40 | " Cfnctl will perform the actions described above.\n"+ 41 | " Only 'yes' will be accepted to approve.\n\n"+ 42 | " %s", whiteBold("Do you want to perform the following actions?"), whiteBold("Enter a value: ")) 43 | 44 | choice, err := reader.ReadString('\n') 45 | if err != nil { 46 | return err 47 | } 48 | 49 | choice = strings.TrimSuffix(choice, "\n") 50 | 51 | if choice != "yes" { 52 | fmt.Fprintf(ctl.Output, "\nApply cancelled.\n") 53 | return nil 54 | } 55 | } 56 | 57 | goterm.Clear() 58 | 59 | err = ctl.ApplyChangeSet() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | go interactive.StreamStackResources(eventsChan, doneChan) 65 | 66 | // to be improved 67 | for { 68 | time.Sleep(500 * time.Millisecond) 69 | status, err := ctl.DescribeStack() 70 | if err != nil { 71 | return err 72 | } 73 | if status == "UPDATE_COMPLETE" || status == "CREATE_FAILED" || status == "CREATE_COMPLETE" { 74 | break 75 | } else { 76 | event, err := ctl.DescribeStackResources() 77 | if err != nil { 78 | return err 79 | } 80 | eventsChan <- interactive.StackResourceEvents{Events: event} 81 | } 82 | } 83 | 84 | close(eventsChan) 85 | doneChan <- true 86 | 87 | // this is a really dirty hack 88 | // insert newlines so table does not dissapear 89 | numAdd := pc.changes["add"] 90 | numChange := pc.changes["change"] 91 | numDestroy := pc.changes["destroy"] 92 | total := numAdd + numChange + numDestroy + 4 // for header and padding 93 | //lint:ignore SA1006 I know what i'm doing 94 | fmt.Printf(strings.Repeat("\n", total)) 95 | fmt.Fprintf(ctl.Output, "\n%s %d added, %d changed, %d destroyed\n", greenBold("Apply complete! Resources:"), (pc.changes["add"]), pc.changes["change"], pc.changes["destroy"]) 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/interactive/table.go: -------------------------------------------------------------------------------- 1 | package interactive 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | "github.com/buger/goterm" 9 | "github.com/olekukonko/tablewriter" 10 | ) 11 | 12 | type StackResourceEvents struct { 13 | Events []types.StackResource 14 | } 15 | 16 | func StreamStackResources(ch <-chan StackResourceEvents, done <-chan bool) { 17 | for { 18 | select { 19 | case <-done: 20 | return 21 | case item := <-ch: 22 | tableOutputter(item.Events, os.Stdout) 23 | } 24 | } 25 | } 26 | 27 | func tableOutputter(events []types.StackResource, writer io.Writer) { 28 | if events == nil { 29 | return 30 | } 31 | 32 | if len(events) == 0 { 33 | return 34 | } 35 | 36 | tableData := [][]string{} 37 | table := tablewriter.NewWriter(writer) 38 | table.SetHeader([]string{"Logical ID", "Physical ID", "Type", "Status", "Status Reason"}) 39 | table.SetHeaderColor( 40 | tablewriter.Colors{tablewriter.Bold}, 41 | tablewriter.Colors{tablewriter.Bold}, 42 | tablewriter.Colors{tablewriter.Bold}, 43 | tablewriter.Colors{tablewriter.Bold}, 44 | tablewriter.Colors{tablewriter.Bold}, 45 | ) 46 | table.SetAutoFormatHeaders(true) 47 | table.SetHeaderAlignment(tablewriter.ALIGN_RIGHT) 48 | table.SetAlignment(tablewriter.ALIGN_RIGHT) 49 | 50 | goterm.MoveCursor(1, 1) 51 | 52 | for _, item := range events { 53 | var physicalID string 54 | var statusReason string 55 | var logicalResourceId string 56 | var ResourceType string 57 | 58 | if item.PhysicalResourceId != nil { 59 | physicalID = *item.PhysicalResourceId 60 | } else { 61 | physicalID = "-" 62 | } 63 | 64 | if item.ResourceStatusReason != nil { 65 | statusReason = *item.ResourceStatusReason 66 | } else { 67 | statusReason = "-" 68 | } 69 | 70 | if item.LogicalResourceId != nil { 71 | logicalResourceId = *item.LogicalResourceId 72 | } else { 73 | logicalResourceId = "-" 74 | } 75 | 76 | if item.ResourceType != nil { 77 | ResourceType = *item.ResourceType 78 | } else { 79 | ResourceType = "-" 80 | } 81 | 82 | arr := []string{ 83 | logicalResourceId, 84 | physicalID, 85 | ResourceType, 86 | string(item.ResourceStatus), 87 | statusReason, 88 | } 89 | tableData = append(tableData, arr) 90 | } 91 | 92 | for i := range tableData { 93 | switch tableData[i][3] { 94 | case "CREATE_COMPLETE": 95 | table.Rich(tableData[i], []tablewriter.Colors{{}, {}, {}, {tablewriter.Normal, tablewriter.FgHiGreenColor}, {}}) 96 | case "CREATE_IN_PROGRESS": 97 | table.Rich(tableData[i], []tablewriter.Colors{{}, {}, {}, {tablewriter.Normal, tablewriter.FgHiBlueColor}, {}}) 98 | case "DELETE_IN_PROGRESS": 99 | table.Rich(tableData[i], []tablewriter.Colors{{}, {}, {}, {tablewriter.Normal, tablewriter.FgHiYellowColor}, {}}) 100 | case "DELETE_COMPLETE": 101 | table.Rich(tableData[i], []tablewriter.Colors{{}, {}, {}, {tablewriter.Normal, tablewriter.FgHiRedColor}, {}}) 102 | case "CREATE_FAILED": 103 | table.Rich(tableData[i], []tablewriter.Colors{{tablewriter.Normal, tablewriter.FgHiRedColor}}) 104 | default: 105 | table.Append(tableData[i]) 106 | } 107 | } 108 | 109 | table.Render() 110 | goterm.Flush() 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ☁️ Cfnctl 2 | 3 |

4 | Go Report Card 5 | Build status 6 | Go version 7 | Current Release 8 | License 9 |

10 | 11 | [[ _pronounced_ cfn control _or_ cloudformation control ]] 12 | 13 | Are you a fan of Terraform but forced to use Cloudformation due to organizational or technical reasons? Introducing **cfnctl**, a CLI that brings the Terraform cli experience to Cloudformation. You'll never need to use the AWS Console for managing stacks again! 14 | 15 | With *cfnctl*, you write Cloudformation templates as usual but use the cli workflow that you are already used to from Terraform, including: 16 | 17 | * **apply** 18 | * **plan** 19 | * **destroy** 20 | * **output** 21 | 22 | 23 | ### Demo 24 | 25 | [![Cfnctl](./cfnctl.svg)](https://asciinema.org/a/abFfMrlLp3MTDHjrrzWpbyDDI?autoplay=1) 26 | 27 | 28 | ### Installation 29 | 30 | Grab a pre-built binary from the [GitHub Releases page](https://github.com/rogerwelin/cfnctl/releases) for your OS of choice 31 | 32 | 33 | ### Usage 34 | 35 | If you are a terraform user the *cfnctl* cli works as you would expect. Running the binary without flags will give you the help output: 36 | 37 | ```bash 38 | ✗ ./cfnctl 39 | NAME: 40 | cfnctl - ✨ Terraform cli experience for AWS Cloudformation 41 | 42 | COMMANDS: 43 | apply Create or update infrastructure 44 | plan Show changes required by the current configuration 45 | destroy Destroy previously-created infrastructure 46 | output Show all exported output values of the selected account and region 47 | validate Check whether the configuration is valid 48 | version Show the current Cfnctl version 49 | help, h Shows a list of commands or help for one command 50 | 51 | GLOBAL OPTIONS: 52 | --help, -h show help (default: false) 53 | 54 | Examples 55 | Apply infrastructure using the "apply" command. 56 | $ cfnctl apply --template-file mycfntmpl.yaml --auto-approve 57 | ``` 58 | 59 | 60 | ### Notice 61 | 62 | Cfnctl is under early development and is missing several features such as: 63 | 64 | * user cancellation does not clean up stacks, [Issue](https://github.com/rogerwelin/cfnctl/issues/1) 65 | * does not support uploading large templates to S3 yet, [Issue](https://github.com/rogerwelin/cfnctl/issues/2) 66 | * does not support drift detection yet, [Issue](https://github.com/rogerwelin/cfnctl/issues/5) 67 | 68 | Feature request and pull requests are welcome. Please see the [Contributing doc](https://github.com/rogerwelin/cfnctl/blob/master/CONTRIBUTING.md) 69 | 70 | If you read this far consider hitting the star ⭐ 71 | 72 | 73 | -------------------------------------------------------------------------------- /commands/destroy.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 13 | "github.com/buger/goterm" 14 | "github.com/fatih/color" 15 | "github.com/olekukonko/tablewriter" 16 | "github.com/rogerwelin/cfnctl/internal/interactive" 17 | "github.com/rogerwelin/cfnctl/pkg/client" 18 | ) 19 | 20 | func destroytOutput(input []types.StackResource, writer io.Writer) int { 21 | tableData := [][]string{} 22 | table := tablewriter.NewWriter(os.Stdout) 23 | table.SetHeader([]string{"Action", "Logical ID", "Physical ID", "Resource type"}) 24 | table.SetHeaderColor( 25 | tablewriter.Colors{tablewriter.Bold}, 26 | tablewriter.Colors{tablewriter.Bold}, 27 | tablewriter.Colors{tablewriter.Bold}, 28 | tablewriter.Colors{tablewriter.Bold}, 29 | ) 30 | 31 | for _, v := range input { 32 | arr := []string{ 33 | "Destroy", 34 | *v.LogicalResourceId, 35 | *v.PhysicalResourceId, 36 | *v.ResourceType, 37 | } 38 | tableData = append(tableData, arr) 39 | } 40 | 41 | for i := range tableData { 42 | switch tableData[i][0] { 43 | case "Destroy": 44 | table.Rich(tableData[i], []tablewriter.Colors{{tablewriter.Normal, tablewriter.FgHiRedColor}}) 45 | default: 46 | table.Append(tableData[i]) 47 | } 48 | } 49 | 50 | fmt.Fprintf(writer, "\nCfnctl will perform the following actions:\n\n") 51 | 52 | table.Render() 53 | 54 | return len(tableData) 55 | } 56 | 57 | // Destroy destroys all the resources in a given stack 58 | func Destroy(ctl *client.Cfnctl) error { 59 | whiteBold := color.New(color.Bold).SprintfFunc() 60 | greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() 61 | 62 | eventsChan := make(chan interactive.StackResourceEvents) 63 | doneChan := make(chan bool) 64 | 65 | // check if stack exists 66 | ok, err := ctl.IsStackCreated() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if !ok { 72 | fmt.Fprintf(ctl.Output, "\n%s %s\n\n", greenBold("No changes."), whiteBold("No objects need to be destroyed")) 73 | fmt.Fprintf(ctl.Output, "Either you have not created any objects yet, there is no Stack named %s or the existing objects were already deleted outside of Cfnctl\n\n", ctl.StackName) 74 | fmt.Fprintf(ctl.Output, "%s", greenBold("Destroy complete! Resources: 0 destroyed\n")) 75 | return nil 76 | } 77 | 78 | out, err := ctl.DescribeStackResources() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | noChanges := destroytOutput(out, ctl.Output) 84 | 85 | if !ctl.AutoApprove { 86 | fmt.Fprintf(ctl.Output, "%s\n"+ 87 | " Cfnctl will destroy all your managed infrastructure, as shown above\n"+ 88 | " There is no undo. Only 'yes' will be accepted to approve.\n\n"+ 89 | " %s", whiteBold("Do you really want to destroy all resources?"), whiteBold("Enter a value: ")) 90 | 91 | reader := bufio.NewReader(os.Stdin) 92 | 93 | choice, err := reader.ReadString('\n') 94 | if err != nil { 95 | return err 96 | } 97 | 98 | choice = strings.TrimSuffix(choice, "\n") 99 | 100 | if choice != "yes" { 101 | fmt.Fprintf(ctl.Output, "\nDestroy cancelled.\n") 102 | return nil 103 | } 104 | } 105 | 106 | goterm.Clear() 107 | 108 | err = ctl.DestroyStack() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | go interactive.StreamStackResources(eventsChan, doneChan) 114 | 115 | // to be improved 116 | for { 117 | time.Sleep(500 * time.Millisecond) 118 | status, err := ctl.DescribeStack() 119 | if err != nil { 120 | if errors.Is(err, client.ErrStackNotFound) { 121 | // move on when stack is deleted and cannot be retrieved again 122 | break 123 | } 124 | return err 125 | } 126 | if status == "DELETE_COMPLETE" { 127 | break 128 | } else { 129 | event, err := ctl.DescribeStackResources() 130 | if err != nil { 131 | return err 132 | } 133 | eventsChan <- interactive.StackResourceEvents{Events: event} 134 | } 135 | } 136 | 137 | close(eventsChan) 138 | doneChan <- true 139 | 140 | //lint:ignore SA1006 I know what i'm doing 141 | fmt.Printf(strings.Repeat("\n", noChanges+4)) 142 | 143 | fmt.Fprintf(ctl.Output, "\n%s %s %d %s\n", greenBold("Destroy complete!"), greenBold("Resources:"), noChanges, greenBold("destroyed")) 144 | 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/mock/mocksvc.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 8 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 9 | "github.com/aws/smithy-go/middleware" 10 | "github.com/rogerwelin/cfnctl/pkg/client" 11 | ) 12 | 13 | type mockAPI struct{} 14 | 15 | // NewMockAPI returns a new instance of mockAPI 16 | func NewMockAPI() client.CloudformationAPI { 17 | return mockAPI{} 18 | } 19 | 20 | // ExecuteChangeSet returns a mocked response 21 | func (m mockAPI) ExecuteChangeSet(ctx context.Context, params *cloudformation.ExecuteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ExecuteChangeSetOutput, error) { 22 | 23 | res := middleware.Metadata{} 24 | res.Set("result", "ok") 25 | 26 | return &cloudformation.ExecuteChangeSetOutput{ 27 | ResultMetadata: res, 28 | }, nil 29 | } 30 | 31 | // ExecuteChangeSet returns a mocked response 32 | func (m mockAPI) CreateChangeSet(ctx context.Context, params *cloudformation.CreateChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.CreateChangeSetOutput, error) { 33 | 34 | id := "apa" 35 | stackID := "123456" 36 | res := middleware.Metadata{} 37 | res.Set("Status", "change set created") 38 | 39 | return &cloudformation.CreateChangeSetOutput{ 40 | Id: &id, 41 | StackId: &stackID, 42 | ResultMetadata: res, 43 | }, nil 44 | } 45 | 46 | // DescribeChangeSet returns a mocked response 47 | func (m mockAPI) DescribeChangeSet(ctx context.Context, params *cloudformation.DescribeChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeChangeSetOutput, error) { 48 | 49 | return &cloudformation.DescribeChangeSetOutput{ 50 | ChangeSetName: params.ChangeSetName, 51 | StackName: params.StackName, 52 | Status: "CREATE_COMPLETE", 53 | // Changes: , 54 | }, nil 55 | } 56 | 57 | // DeleteChangeSet returns a mocked response 58 | func (m mockAPI) DeleteChangeSet(ctx context.Context, params *cloudformation.DeleteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteChangeSetOutput, error) { 59 | res := middleware.Metadata{} 60 | res.Set("result", "ok") 61 | return &cloudformation.DeleteChangeSetOutput{ 62 | ResultMetadata: res, 63 | }, nil 64 | } 65 | 66 | // DescribeStacks returns a mocked response 67 | func (m mockAPI) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) { 68 | return &cloudformation.DescribeStacksOutput{ 69 | // Stacks: , 70 | }, nil 71 | } 72 | 73 | // DescribeStackResources returns a mocked response 74 | func (m mockAPI) DescribeStackResources(ctx context.Context, params *cloudformation.DescribeStackResourcesInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) { 75 | return &cloudformation.DescribeStackResourcesOutput{}, nil 76 | } 77 | 78 | // ListChangeSets returns a mocked response 79 | func (m mockAPI) ListChangeSets(ctx context.Context, params *cloudformation.ListChangeSetsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListChangeSetsOutput, error) { 80 | status := types.ChangeSetSummary{Status: "CREATE_COMPLETE"} 81 | sum := []types.ChangeSetSummary{status} 82 | 83 | return &cloudformation.ListChangeSetsOutput{Summaries: sum}, nil 84 | } 85 | 86 | // ListStacks returns a mocked response 87 | func (m mockAPI) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) { 88 | return &cloudformation.ListStacksOutput{}, nil 89 | } 90 | 91 | // ValidateTemplate returns a mocked response 92 | func (m mockAPI) ValidateTemplate(ctx context.Context, params *cloudformation.ValidateTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ValidateTemplateOutput, error) { 93 | return &cloudformation.ValidateTemplateOutput{}, nil 94 | } 95 | 96 | // ListExports returns a mocked response 97 | func (m mockAPI) ListExports(ctx context.Context, params *cloudformation.ListExportsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListExportsOutput, error) { 98 | return &cloudformation.ListExportsOutput{ 99 | Exports: []types.Export{ 100 | { 101 | ExportingStackId: aws.String("template"), 102 | Name: aws.String("Bucket"), 103 | Value: aws.String("TestBucket"), 104 | }, 105 | }, 106 | }, nil 107 | } 108 | 109 | // DeleteStack returns a mocked response 110 | func (m mockAPI) DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) { 111 | return &cloudformation.DeleteStackOutput{}, nil 112 | } 113 | -------------------------------------------------------------------------------- /commands/plan.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 9 | "github.com/fatih/color" 10 | "github.com/olekukonko/tablewriter" 11 | "github.com/rogerwelin/cfnctl/cli/params" 12 | "github.com/rogerwelin/cfnctl/pkg/client" 13 | ) 14 | 15 | type planChanges struct { 16 | containsChanges bool 17 | changes map[string]int 18 | } 19 | 20 | func planOutput(changes []types.Change, writer io.Writer) planChanges { 21 | tableData := [][]string{} 22 | table := tablewriter.NewWriter(writer) 23 | table.SetHeader([]string{"Action", "Logical ID", "Physical ID", "Resource type", "Replacement"}) 24 | table.SetHeaderColor( 25 | tablewriter.Colors{tablewriter.Bold}, 26 | tablewriter.Colors{tablewriter.Bold}, 27 | tablewriter.Colors{tablewriter.Bold}, 28 | tablewriter.Colors{tablewriter.Bold}, 29 | tablewriter.Colors{tablewriter.Bold}, 30 | ) 31 | 32 | actionMap := make(map[string]int) 33 | 34 | for _, v := range changes { 35 | var physicalID string 36 | var replacement string 37 | 38 | if v.ResourceChange.PhysicalResourceId != nil { 39 | physicalID = *v.ResourceChange.PhysicalResourceId 40 | } else { 41 | physicalID = "-" 42 | } 43 | 44 | if v.ResourceChange.Replacement != "" { 45 | replacement = string(v.ResourceChange.Replacement) 46 | } else { 47 | replacement = "-" 48 | } 49 | 50 | if v.ResourceChange.Action == "Add" { 51 | actionMap["add"]++ 52 | } else if v.ResourceChange.Action == "Remove" { 53 | actionMap["destroy"]++ 54 | } else if v.ResourceChange.Action == "Modify" { 55 | actionMap["change"]++ 56 | } 57 | 58 | arr := []string{ 59 | string(v.ResourceChange.Action), 60 | *v.ResourceChange.LogicalResourceId, 61 | physicalID, 62 | *v.ResourceChange.ResourceType, 63 | replacement, 64 | } 65 | tableData = append(tableData, arr) 66 | } 67 | 68 | for i := range tableData { 69 | switch tableData[i][0] { 70 | case "Add": 71 | table.Rich(tableData[i], []tablewriter.Colors{{tablewriter.Normal, tablewriter.FgHiGreenColor}}) 72 | case "Delete": 73 | table.Rich(tableData[i], []tablewriter.Colors{{tablewriter.Normal, tablewriter.FgHiRedColor}}) 74 | case "Modify": 75 | table.Rich(tableData[i], []tablewriter.Colors{{tablewriter.Normal, tablewriter.FgHiYellowColor}}) 76 | default: 77 | table.Append(tableData[i]) 78 | } 79 | } 80 | 81 | whiteBold := color.New(color.Bold).SprintFunc() 82 | fmt.Fprintf(writer, "\nCfnctl will perform the following actions:\n\n") 83 | 84 | var modifications bool 85 | 86 | if len(changes) != 0 { 87 | modifications = true 88 | table.Render() 89 | } else { 90 | modifications = false 91 | } 92 | 93 | fmt.Fprintf(writer, "\n%s %d to add, %d to change, %d to destroy\n\n", whiteBold("Plan:"), actionMap["add"], actionMap["change"], actionMap["destroy"]) 94 | 95 | pc := planChanges{ 96 | containsChanges: modifications, 97 | changes: actionMap, 98 | } 99 | 100 | return pc 101 | } 102 | 103 | // Plan gives a plan output of changes to be made from a given CF template 104 | func Plan(ctl *client.Cfnctl, deleteChangeSet bool) (planChanges, error) { 105 | 106 | pc := planChanges{} 107 | 108 | // if vars file is supplied 109 | if ctl.VarsFile != "" { 110 | out, err := params.MergeFileParams(ctl.VarsFile) 111 | ctl.Parameters = out 112 | if err != nil { 113 | return pc, err 114 | } 115 | err = ctl.CreateChangeSet() 116 | if err != nil { 117 | return pc, err 118 | } 119 | } else { 120 | // no vars file. check if tempalte contains params 121 | ok, outParams, err := params.CheckInputParams(ctl.TemplatePath) 122 | if err != nil { 123 | return pc, err 124 | } 125 | // no input params or default value set 126 | if !ok { 127 | // create change set 128 | err = ctl.CreateChangeSet() 129 | if err != nil { 130 | return pc, err 131 | } 132 | } else { 133 | // get user input 134 | out, err := params.BuildInputParams(outParams) 135 | if err != nil { 136 | return pc, err 137 | } 138 | ctl.Parameters = out 139 | err = ctl.CreateChangeSet() 140 | if err != nil { 141 | return pc, err 142 | } 143 | } 144 | } 145 | 146 | // needs to be improved 147 | count := 15 148 | for i := 0; i < count; i++ { 149 | time.Sleep(1 * time.Second) 150 | status, err := ctl.ListChangeSet() 151 | if err != nil { 152 | return pc, err 153 | } 154 | if status == "CREATE_COMPLETE" { 155 | break 156 | } 157 | } 158 | 159 | createEvents, err := ctl.DescribeChangeSet() 160 | if err != nil { 161 | return pc, err 162 | } 163 | 164 | pc = planOutput(createEvents, ctl.Output) 165 | 166 | // clean up changeset 167 | if deleteChangeSet { 168 | err = ctl.DeleteChangeSet() 169 | if err != nil { 170 | return pc, err 171 | } 172 | } 173 | return pc, nil 174 | } 175 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | "github.com/rogerwelin/cfnctl/didyoumean" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var ( 13 | version = "0.1.0" 14 | cmds = []string{"apply", "destroy", "plan", "validate", "version", "output", "help"} 15 | ) 16 | 17 | // RunCLI runs a new instance of cfnctl 18 | func RunCLI(args []string) { 19 | app := cli.NewApp() 20 | setCustomCLITemplate(app) 21 | app.Name = "cfnctl" 22 | app.Usage = "✨ Terraform cli experience for AWS Cloudformation" 23 | app.HelpName = "cfnctl" 24 | app.EnableBashCompletion = true 25 | app.UsageText = "cfntl [global options] [args]" 26 | app.Version = version 27 | app.HideVersion = true 28 | app.CommandNotFound = func(c *cli.Context, command string) { 29 | res := didyoumean.NameSuggestion(command, cmds) 30 | if res == "" { 31 | fmt.Println("apa") // FIX 32 | } else { 33 | fmt.Println("Cfnctl has no command named: " + command + ". Did you mean: " + res + "?") 34 | fmt.Println("\nToo see all of Cfnctl's top-level commands, run\n\tcfnctl --help") 35 | } 36 | } 37 | app.Commands = []*cli.Command{ 38 | { 39 | Name: "apply", 40 | Usage: "Create or update infrastructure", 41 | Flags: []cli.Flag{ 42 | &cli.BoolFlag{ 43 | Name: "auto-approve", 44 | Usage: "Skip interactive approval of plan before applying.", 45 | }, 46 | &cli.StringFlag{ 47 | Name: "template-file", 48 | Usage: "The path of the Cloudformation template you wish to apply", 49 | Required: true, 50 | }, 51 | &cli.StringFlag{ 52 | Name: "param-file", 53 | Usage: "filename. Load parameters from the given file", 54 | }, 55 | &cli.StringFlag{ 56 | Name: "param", 57 | Usage: "foo=bar. Set a value for one of the parameters. Use this option more than once to set more than one parameter", 58 | }, 59 | }, 60 | Action: func(c *cli.Context) error { 61 | apply := Apply{ 62 | TemplatePath: c.String("template-file"), 63 | ParamFile: c.String("param-file"), 64 | AutoApprove: c.Bool("auto-approve"), 65 | } 66 | err := apply.Run() 67 | return err 68 | }, 69 | }, 70 | { 71 | Name: "plan", 72 | Usage: "Show changes required by the current configuration", 73 | Flags: []cli.Flag{ 74 | &cli.StringFlag{ 75 | Name: "template-file", 76 | Usage: "filename", 77 | Required: true, 78 | }, 79 | &cli.StringFlag{ 80 | Name: "param-file", 81 | Usage: "filename", 82 | }, 83 | }, 84 | Action: func(c *cli.Context) error { 85 | plan := Plan{ 86 | TemplatePath: c.String("template-file"), 87 | ParamFile: c.String("param-file"), 88 | } 89 | err := plan.Run() 90 | return err 91 | }, 92 | }, 93 | { 94 | Name: "destroy", 95 | Usage: "Destroy previously-created infrastructure", 96 | Flags: []cli.Flag{ 97 | &cli.StringFlag{ 98 | Name: "template-file", 99 | Usage: "filename", 100 | Required: true, 101 | }, 102 | &cli.BoolFlag{ 103 | Name: "auto-approve", 104 | Usage: "Skip interactive approval of plan before applying.", 105 | Value: false, 106 | }, 107 | }, 108 | Action: func(c *cli.Context) error { 109 | destroy := Destroy{ 110 | AutoApprove: c.Bool("auto-approve"), 111 | TemplatePath: c.String("template-file"), 112 | } 113 | err := destroy.Run() 114 | return err 115 | }, 116 | }, 117 | { 118 | Name: "output", 119 | Usage: "Show all exported output values of the selected account and region", 120 | Action: func(c *cli.Context) error { 121 | out := Output{} 122 | err := out.Run() 123 | return err 124 | }, 125 | }, 126 | { 127 | Name: "validate", 128 | Usage: "Check whether the configuration is valid", 129 | Flags: []cli.Flag{ 130 | &cli.StringFlag{ 131 | Name: "template-file", 132 | Usage: "The path of the Cloudformation template you wish to apply", 133 | Required: true, 134 | }, 135 | }, 136 | Action: func(c *cli.Context) error { 137 | v := Validate{TemplatePath: c.String("template-file")} 138 | err := v.Run() 139 | return err 140 | }, 141 | }, 142 | { 143 | Name: "version", 144 | Usage: "Show the current Cfnctl version", 145 | Action: func(c *cli.Context) error { 146 | v := Version{Version: version} 147 | err := v.Run() 148 | return err 149 | }, 150 | }, 151 | } 152 | 153 | err := app.Run(args) 154 | if err != nil { 155 | fmt.Println(err) 156 | os.Exit(0) 157 | } 158 | } 159 | 160 | func setCustomCLITemplate(c *cli.App) { 161 | whiteBold := color.New(color.Bold).SprintfFunc() 162 | whiteUnderline := color.New(color.Bold).Add(color.Underline).SprintfFunc() 163 | cyan := color.New(color.FgCyan).SprintFunc() 164 | 165 | c.CustomAppHelpTemplate = fmt.Sprintf(` %s: 166 | {{.Name}}{{if .Usage}} - {{.Usage}}{{end}}{{if .Description}} 167 | 168 | DESCRIPTION: 169 | {{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}} 170 | 171 | AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: 172 | {{range $index, $author := .Authors}}{{if $index}} 173 | {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} 174 | 175 | %s:{{range .VisibleCategories}}{{if .Name}} 176 | {{.Name}}:{{range .VisibleCommands}} 177 | {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} 178 | {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} 179 | 180 | %s: 181 | {{range $index, $option := .VisibleFlags}}{{if $index}} 182 | {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} 183 | 184 | COPYRIGHT: 185 | {{.Copyright}}{{end}} 186 | 187 | %s 188 | Apply infrastructure using the "apply" command. 189 | %s 190 | `, whiteBold("NAME"), 191 | whiteBold("COMMANDS"), 192 | whiteBold("GLOBAL OPTIONS"), 193 | whiteUnderline("Examples"), 194 | cyan("$ cfnctl apply --template-file mycfntmpl.yaml --auto-approve")) 195 | } 196 | -------------------------------------------------------------------------------- /pkg/client/api.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 9 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 10 | "github.com/aws/smithy-go" 11 | "github.com/rogerwelin/cfnctl/utils" 12 | ) 13 | 14 | var ErrStackNotFound = errors.New("stack does not exist") 15 | 16 | // Option is used to implement Option Pattern on the client 17 | type Option func(*Cfnctl) 18 | 19 | // WithAutoApprove provides access creating a new client 20 | func WithAutoApprove(b bool) Option { 21 | return func(c *Cfnctl) { 22 | c.AutoApprove = b 23 | } 24 | } 25 | 26 | // WithVarsFile provides access creating a new client 27 | func WithVarsFile(varsFile string) Option { 28 | return func(c *Cfnctl) { 29 | c.VarsFile = varsFile 30 | } 31 | } 32 | 33 | // WithStackName provides access creating a new client 34 | func WithStackName(stackName string) Option { 35 | return func(c *Cfnctl) { 36 | c.StackName = stackName 37 | } 38 | } 39 | 40 | // WithChangesetName provides access creating a new client 41 | func WithChangesetName(changesetName string) Option { 42 | return func(c *Cfnctl) { 43 | c.ChangesetName = changesetName 44 | } 45 | } 46 | 47 | // WithSvc provides access creating a new client 48 | func WithSvc(svc CloudformationAPI) Option { 49 | return func(c *Cfnctl) { 50 | c.Svc = svc 51 | } 52 | } 53 | 54 | // WithTemplatePath provides access creating a new client 55 | func WithTemplatePath(filePath string) Option { 56 | return func(c *Cfnctl) { 57 | c.TemplatePath = filePath 58 | } 59 | } 60 | 61 | // WithTemplateBody provides access creating a new client 62 | func WithTemplateBody(file string) Option { 63 | return func(c *Cfnctl) { 64 | c.TemplateBody = file 65 | } 66 | } 67 | 68 | // WithOutput provides access creating a new client 69 | func WithOutput(out io.Writer) Option { 70 | return func(c *Cfnctl) { 71 | c.Output = out 72 | } 73 | } 74 | 75 | // New creates a new client 76 | func New(option ...Option) *Cfnctl { 77 | c := &Cfnctl{} 78 | for _, o := range option { 79 | o(c) 80 | } 81 | return c 82 | } 83 | 84 | // ApplyChangeSet executes a CloudFormation changeset 85 | func (c *Cfnctl) ApplyChangeSet() error { 86 | input := &cloudformation.ExecuteChangeSetInput{ 87 | ChangeSetName: &c.StackName, 88 | StackName: &c.StackName, 89 | } 90 | 91 | _, err := c.Svc.ExecuteChangeSet(context.TODO(), input) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // IsStackCreated checks whether a CloudFormation stack is created or not 100 | func (c *Cfnctl) IsStackCreated() (bool, error) { 101 | input := &cloudformation.ListStacksInput{ 102 | StackStatusFilter: []types.StackStatus{ 103 | "CREATE_COMPLETE", 104 | "UPDATE_COMPLETE", 105 | }, 106 | } 107 | 108 | out, err := c.Svc.ListStacks(context.TODO(), input) 109 | if err != nil { 110 | return false, nil 111 | } 112 | 113 | for _, val := range out.StackSummaries { 114 | if *val.StackName == c.StackName { 115 | return true, nil 116 | } 117 | } 118 | return false, nil 119 | } 120 | 121 | // ChangeSetExists checks whether a CloudFormation changeset exists or not 122 | func (c *Cfnctl) ChangeSetExists(stackName, changesetName string) (bool, error) { 123 | input := &cloudformation.ListChangeSetsInput{ 124 | StackName: &stackName, 125 | } 126 | out, err := c.Svc.ListChangeSets(context.TODO(), input) 127 | if err != nil { 128 | return false, err 129 | } 130 | found := false 131 | 132 | for _, val := range out.Summaries { 133 | if *val.ChangeSetName == changesetName { 134 | found = true 135 | break 136 | } 137 | } 138 | return found, nil 139 | } 140 | 141 | // ListChangeSet lists all changesets. Can be used to get status if changeset created or not 142 | func (c *Cfnctl) ListChangeSet() (types.ChangeSetStatus, error) { 143 | input := &cloudformation.ListChangeSetsInput{ 144 | StackName: &c.StackName, 145 | } 146 | 147 | output, err := c.Svc.ListChangeSets(context.TODO(), input) 148 | if err != nil { 149 | return "", err 150 | } 151 | 152 | if len(output.Summaries) == 0 { 153 | return "", errors.New("empty resultset when listing change sets") 154 | } 155 | 156 | return output.Summaries[0].Status, nil 157 | } 158 | 159 | // DescribeChangeSet describes a CloudFormation changeset 160 | func (c *Cfnctl) DescribeChangeSet() ([]types.Change, error) { 161 | input := &cloudformation.DescribeChangeSetInput{ 162 | ChangeSetName: &c.ChangesetName, 163 | StackName: &c.StackName, 164 | } 165 | 166 | out, err := c.Svc.DescribeChangeSet(context.TODO(), input) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | return out.Changes, nil 172 | } 173 | 174 | // CreateChangeSet creates a CloudFormation changeset 175 | func (c *Cfnctl) CreateChangeSet() error { 176 | 177 | capabilities := []types.Capability{"CAPABILITY_NAMED_IAM"} 178 | var changeSetType types.ChangeSetType 179 | 180 | // 1. check of stack already exists. if so choose UPDATE. if not choose CREATE 181 | // 2. if stack already exists choose new change set name 182 | created, err := c.IsStackCreated() 183 | if err != nil { 184 | return err 185 | } 186 | 187 | if created { 188 | changeSetType = "UPDATE" 189 | found, err := c.ChangeSetExists(c.ChangesetName, c.StackName) 190 | if err != nil { 191 | return err 192 | } 193 | if found { 194 | suffix := utils.ReturnRandom(5) 195 | c.ChangesetName = c.ChangesetName + "-" + suffix 196 | //changesetName = changesetName + "-" + suffix 197 | } 198 | } else { 199 | changeSetType = "CREATE" 200 | } 201 | 202 | input := &cloudformation.CreateChangeSetInput{ 203 | ChangeSetName: &c.ChangesetName, 204 | StackName: &c.StackName, 205 | ChangeSetType: changeSetType, 206 | TemplateBody: &c.TemplateBody, 207 | Capabilities: capabilities, 208 | } 209 | 210 | if c.Parameters != nil { 211 | input.Parameters = c.Parameters 212 | } 213 | 214 | _, err = c.Svc.CreateChangeSet(context.TODO(), input) 215 | if err != nil { 216 | return err 217 | } 218 | 219 | return nil 220 | } 221 | 222 | // DeleteChangeSet deletes a CloudFormation changeset 223 | func (c *Cfnctl) DeleteChangeSet() error { 224 | input := &cloudformation.DeleteChangeSetInput{ 225 | ChangeSetName: &c.ChangesetName, 226 | StackName: &c.StackName, 227 | } 228 | 229 | _, err := c.Svc.DeleteChangeSet(context.TODO(), input) 230 | if err != nil { 231 | return err 232 | } 233 | 234 | return nil 235 | } 236 | 237 | // DescribeStack describes a CloudFormation stack 238 | // If the stack doesn't exist, an ValidationError is returned. 239 | func (c *Cfnctl) DescribeStack() (string, error) { 240 | input := &cloudformation.DescribeStacksInput{ 241 | StackName: &c.StackName, 242 | } 243 | out, err := c.Svc.DescribeStacks(context.TODO(), input) 244 | if err != nil { 245 | var ae smithy.APIError 246 | if errors.As(err, &ae) { 247 | //log.Printf("code: %s, message: %s, fault: %s", ae.ErrorCode(), ae.ErrorMessage(), ae.ErrorFault().String()) 248 | if ae.ErrorCode() == "ValidationError" { 249 | return "", ErrStackNotFound 250 | 251 | } 252 | } 253 | return "", err 254 | } 255 | 256 | return string(out.Stacks[0].StackStatus), nil 257 | } 258 | 259 | // DescribeStackResources describes the resources from a particular CloudFormation stack 260 | func (c *Cfnctl) DescribeStackResources() ([]types.StackResource, error) { 261 | input := &cloudformation.DescribeStackResourcesInput{ 262 | StackName: &c.StackName, 263 | } 264 | 265 | out, err := c.Svc.DescribeStackResources(context.TODO(), input) 266 | if err != nil { 267 | return nil, err 268 | } 269 | 270 | return out.StackResources, nil 271 | } 272 | 273 | // ValidateCFTemplate validates a particular CloudFormation template 274 | func (c *Cfnctl) ValidateCFTemplate() error { 275 | input := &cloudformation.ValidateTemplateInput{ 276 | TemplateBody: &c.TemplateBody, 277 | } 278 | _, err := c.Svc.ValidateTemplate(context.TODO(), input) 279 | if err != nil { 280 | return err 281 | } 282 | return nil 283 | } 284 | 285 | func (c *Cfnctl) ListExportValues() ([]types.Export, error) { 286 | input := &cloudformation.ListExportsInput{} 287 | out, err := c.Svc.ListExports(context.TODO(), input) 288 | if err != nil { 289 | return nil, err 290 | } 291 | return out.Exports, nil 292 | } 293 | 294 | // Deletes a specified stack. Once the call completes successfully, stack deletion starts. Deleted stacks do not show up in the DescribeStacks API if the deletion has been completed successfully 295 | func (c *Cfnctl) DestroyStack() error { 296 | input := &cloudformation.DeleteStackInput{ 297 | StackName: &c.StackName, 298 | } 299 | _, err := c.Svc.DeleteStack(context.TODO(), input) 300 | if err != nil { 301 | return err 302 | } 303 | return nil 304 | } 305 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Roger Welin 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 3 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 4 | github.com/aws/aws-sdk-go-v2 v1.4.0 h1:Ryh4fNebT9SwLyCKPSk83dyEZj+KB6KzDyb1gXii7EI= 5 | github.com/aws/aws-sdk-go-v2 v1.4.0/go.mod h1:tI4KhsR5VkzlUa2DZAdwx7wCAYGwkZZ1H31PYrBFx1w= 6 | github.com/aws/aws-sdk-go-v2/config v1.1.7 h1:I9AsaodDiw1WbUBn8b4Ktvr2ltPBe7QfLq4UUZY4GsY= 7 | github.com/aws/aws-sdk-go-v2/config v1.1.7/go.mod h1:6GFyKv06rDCBCIOmWe9vLi7ofCkE7y8aqI6a3tFWNQ0= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.1.7 h1:hpg13GGT/j7Zv+xkJ+0p2Pj1NvnRSNIX1XI4WI02kq8= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.1.7/go.mod h1:xYCvIyeVCRC9DmG3Zv/pxlZEEIBYf4fY/jSUVSrr58M= 10 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.7 h1:WdPPgbL9Kl4UrEdiy6IdQioPuXSBg8HJcU9eihiCWOE= 11 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.7/go.mod h1:51hY5nMAiL2EF8ny/pFovWYoKZTcEfOw0WWKcq2E9AQ= 12 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.4.0 h1:KMrvrZPwbaOQLh4EKGBWQzKQ4djf1I8Id5pWuUdM/lI= 13 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.4.0/go.mod h1:62sOPaD3mMuhugnDIEACo8frus6RtSDTtI1uY4Ts8JA= 14 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.7 h1:/XUEk2eY/QGzCHSX5LH4vp+UMKTSVVCroUfgkzPGIzE= 15 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.7/go.mod h1:Fl3kxN4ucBIPuAHKHRn+wTGylFzMCEjRfmfHqZuEh8o= 16 | github.com/aws/aws-sdk-go-v2/service/sso v1.1.6 h1:oS6Jdb6ZwD7U1OqJYqyGbgcU0VJ15E3XbiIIp0clge8= 17 | github.com/aws/aws-sdk-go-v2/service/sso v1.1.6/go.mod h1:EO4s+JzAllrWKgNjS/Q4mj43vGimSvhWCB6BLognegc= 18 | github.com/aws/aws-sdk-go-v2/service/sts v1.3.1 h1:tAn9Kvjfq2F2rCeTbMOLtDBQVjLLSIvm21BTUFoswss= 19 | github.com/aws/aws-sdk-go-v2/service/sts v1.3.1/go.mod h1:QPpnumNNgybBiz3HXGgDRf/QypAMOOsh4+JdOhG5mLU= 20 | github.com/aws/smithy-go v1.4.0 h1:3rsQpgRe+OoQgJhEwGNpIkosl0fJLdmQqF4gSFRjg+4= 21 | github.com/aws/smithy-go v1.4.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= 22 | github.com/awslabs/goformation/v4 v4.19.1 h1:xqCDM4+gtkUNmxe1xP3LyH0X7EDMBR4HR1bqHUiMB7o= 23 | github.com/awslabs/goformation/v4 v4.19.1/go.mod h1:ygNqNsr904Q/Jan2A6ZKw9ewZWDTL9zlclZx2JzZhlM= 24 | github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= 25 | github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= 26 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 27 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 28 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 29 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 30 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 31 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 32 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 33 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 34 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 37 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 38 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 39 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 40 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 41 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 42 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 43 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 44 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 45 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 46 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 47 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 48 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 49 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 50 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 51 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 53 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 54 | github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= 55 | github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 56 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 57 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 58 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 59 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 60 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 61 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 62 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 63 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 64 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 65 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 66 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 67 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 68 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 69 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 70 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 71 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 72 | github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= 73 | github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= 74 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 75 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 76 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 77 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 78 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 79 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 80 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 81 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 82 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 83 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 84 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 85 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 86 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 87 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 88 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 89 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 90 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 91 | github.com/onsi/ginkgo v1.15.1 h1:DsXNrKujDlkMS9Rsxmd+Fg7S6Kc5lhE+qX8tY6laOxc= 92 | github.com/onsi/ginkgo v1.15.1/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= 93 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 94 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 95 | github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= 96 | github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= 97 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 98 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 99 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 100 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 101 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 102 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 103 | github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6babJtnLo1qsGvq6G9so9KMflGAm4YA= 104 | github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY= 105 | github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wqlbJtbyPzSgaxHTv8uN1pMpkG1t8= 106 | github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522/go.mod h1:tQTYKOQgxoH3v6dEmdHiz4JG+nbxWwM5fgPQUpSZqVQ= 107 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 108 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 109 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 110 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 111 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 112 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 113 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 114 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 115 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 116 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 117 | github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56 h1:yhqBHs09SmmUoNOHc9jgK4a60T3XFRtPAkYxVnqgY50= 118 | github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= 119 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 120 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 121 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 122 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 123 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 124 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 125 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 126 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 128 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 129 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 130 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 131 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 135 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 137 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 149 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 151 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 152 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 153 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 154 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 155 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 156 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 157 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 158 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 159 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 160 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 161 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 162 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 163 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 164 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 165 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 166 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 167 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 168 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 170 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 171 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 172 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 173 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 174 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 175 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 178 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 179 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 180 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 | -------------------------------------------------------------------------------- /cfnctl.svg: -------------------------------------------------------------------------------- 1 | cfnctlgit:(master)cfnctlgit:(master)./cfnctlapplycfnctlgit:(master)./cfnctlapply--template-filecfnctlgit:(master)./cfnctlapply--template-filecommands/testdata/template.yamlCfnctlwillperformthefollowingactions:+--------+----------------+-------------+-----------------+-------------+|ACTION|LOGICALID|PHYSICALID|RESOURCETYPE|REPLACEMENT||Add|Bucket|-|AWS::S3::Bucket|-||Add|CloudwatchRole|-|AWS::IAM::Role|-|Plan:2toadd,0tochange,0todestroyDoyouwanttoperformthefollowingactions?Cfnctlwillperformtheactionsdescribedabove.Only'yes'willbeacceptedtoapprove.Enteravalue:yes+----------------+-------------+-----------------+--------------------+---------------++----------------+-------------------------------+-----------------+--------------------+-----------------------------+|LOGICALID|PHYSICALID|TYPE|STATUS|STATUSREASON||Bucket|template-bucket-1lrylk29818bd|AWS::S3::Bucket|CREATE_IN_PROGRESS|ResourcecreationInitiated||CloudwatchRole|CloudWatch-Role|AWS::IAM::Role|CREATE_IN_PROGRESS|ResourcecreationInitiated||Bucket|template-bucket-1lrylk29818bd|AWS::S3::Bucket|CREATE_COMPLETE|-|+----------------+-------------------------------+-----------------+-----------------+---------------+----------------+|LOGICALID|PHYSICALID|TYPE|STATUS|STATUSREASON|STATUSREASON||Bucket|template-bucket-1lrylk29818bd|AWS::S3::Bucket|CREATE_COMPLETE|-|-||CloudwatchRole|CloudWatch-Role|AWS::IAM::Role|CREATE_COMPLETE|-|ationInitiated|Applycomplete!Resources:2added,0changed,0destroyedcfnctlgit:(master).cfnctlgit:(master)./cfnctlgit:(master)./ccfnctlgit:(master)./cfcfnctlgit:(master)./cfncfnctlgit:(master)./cfnccfnctlgit:(master)./cfnctlcfnctlgit:(master)./cfnctlacfnctlgit:(master)./cfnctlapcfnctlgit:(master)./cfnctlappcfnctlgit:(master)./cfnctlapplcfnctlgit:(master)./cfnctlapply-cfnctlgit:(master)./cfnctlapply--cfnctlgit:(master)./cfnctlapply--tcfnctlgit:(master)./cfnctlapply--tecfnctlgit:(master)./cfnctlapply--temcfnctlgit:(master)./cfnctlapply--tempcfnctlgit:(master)./cfnctlapply--templcfnctlgit:(master)./cfnctlapply--templacfnctlgit:(master)./cfnctlapply--templatcfnctlgit:(master)./cfnctlapply--templatecfnctlgit:(master)./cfnctlapply--template-cfnctlgit:(master)./cfnctlapply--template-fcfnctlgit:(master)./cfnctlapply--template-ficfnctlgit:(master)./cfnctlapply--template-filcfnctlgit:(master)./cfnctlapply--template-fileccfnctlgit:(master)./cfnctlapply--template-filecocfnctlgit:(master)./cfnctlapply--template-filecomcfnctlgit:(master)./cfnctlapply--template-filecommcfnctlgit:(master)./cfnctlapply--template-filecommands/cfnctlgit:(master)./cfnctlapply--template-filecommands/tcfnctlgit:(master)./cfnctlapply--template-filecommands/tecfnctlgit:(master)./cfnctlapply--template-filecommands/tescfnctlgit:(master)./cfnctlapply--template-filecommands/testcfnctlgit:(master)./cfnctlapply--template-filecommands/testdata/+--------+----------------+Enteravalue:Enteravalue:yEnteravalue:ye|LOGICALID|PHYSICALID||LOGICALID|PHYSICALID|TYPE|STATUS|STATUSREASON||Bucket|-|AWS::S3::Bucket|CREATE_IN_PROGRESS|-||CloudwatchRole|-|AWS::IAM::Role|CREATE_IN_PROGRESS|-||Bucket|template-bucket-1lrylk29818bd|AWS::S3::Bucket| --------------------------------------------------------------------------------