├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── release │ │ └── action.yml ├── dependabot.yml ├── pull_request_template.md ├── release.yml └── workflows │ ├── ci.yml │ ├── manual.yml │ ├── semantic-pull-request.yml │ └── tagpr.yaml ├── .gitignore ├── .golangci.bck.yml ├── .golangci.yml ├── .goreleaser.yaml ├── .tagpr ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── cmd └── delstack │ └── main.go ├── go.mod ├── go.sum ├── install.sh ├── internal ├── app │ └── app.go ├── io │ ├── input.go │ ├── logger.go │ ├── tablewriter.go │ └── ui.go ├── operation │ ├── backup_vault.go │ ├── backup_vault_test.go │ ├── cloudformation_stack.go │ ├── cloudformation_stack_test.go │ ├── custom.go │ ├── ecr_repository.go │ ├── ecr_repository_test.go │ ├── iam_group.go │ ├── iam_group_test.go │ ├── main_test.go │ ├── operator.go │ ├── operator_collection.go │ ├── operator_collection_mock.go │ ├── operator_collection_test.go │ ├── operator_factory.go │ ├── operator_manager.go │ ├── operator_manager_mock.go │ ├── operator_manager_test.go │ ├── operator_mock.go │ ├── s3_bucket.go │ ├── s3_bucket_test.go │ ├── s3_table_bucket.go │ └── s3_table_bucket_test.go ├── resourcetype │ └── resourcetype.go └── version │ ├── main_test.go │ ├── version.go │ └── version_test.go ├── pkg └── client │ ├── aws_config.go │ ├── backup.go │ ├── backup_mock.go │ ├── backup_test.go │ ├── cloudformation.go │ ├── cloudformation_mock.go │ ├── cloudformation_test.go │ ├── ecr.go │ ├── ecr_mock.go │ ├── ecr_test.go │ ├── errors.go │ ├── iam.go │ ├── iam_mock.go │ ├── iam_test.go │ ├── main_test.go │ ├── retryer.go │ ├── s3.go │ ├── s3_mock.go │ ├── s3_tables.go │ ├── s3_tables_mock.go │ ├── s3_tables_test.go │ └── s3_test.go └── testdata ├── Dockerfile ├── README.md ├── cdk ├── .gitignore ├── cdk.go ├── cdk.json ├── go.mod ├── go.sum └── lib │ ├── nest │ ├── child_stack.go │ ├── child_stack2.go │ ├── descend_stack.go │ ├── descend_stack2.go │ └── descend_stack3.go │ └── resource │ ├── backup.go │ ├── custom_resource.go │ ├── dynamodb.go │ ├── ecr.go │ ├── iam_group.go │ ├── lambda.go │ ├── s3_bucket.go │ ├── s3_directory_bucket.go │ └── s3_table_bucket.go ├── deploy.go ├── go.mod ├── go.sum └── policy_document.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{md,markdown,json}] 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.sh] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 4 11 | indent_style = tab 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bugs]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | > A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | > Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Current workaround** 21 | > A clear and concise description of what you expected to happen. 22 | 23 | **Expected behavior** 24 | > A clear and concise description of what you expected to happen. 25 | 26 | **Environment** 27 | - OS: [e.g. macOS(arm64)] 28 | - Version: v1.0.0 29 | 30 | **Additional context** 31 | > Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: feature request 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. 12 | 13 | **Describe the solution you'd like** 14 | > A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | > Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/actions/release/action.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | inputs: 3 | github-token: 4 | required: true 5 | homebrew-tap-github-token: 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Setup Go 11 | uses: actions/setup-go@v5 12 | with: 13 | go-version-file: go.mod 14 | - name: Run GoReleaser 15 | uses: goreleaser/goreleaser-action@v6 16 | with: 17 | version: latest 18 | args: release --clean 19 | env: 20 | GITHUB_TOKEN: ${{ inputs.github-token }} 21 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ inputs.homebrew-tap-github-token }} -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | ignore: 12 | - dependency-name: "*" 13 | update-types: ["version-update:semver-patch"] -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | > REPLACE THIS TEXT BLOCK 2 | > 3 | > Describe the reason for this change, what the solution is, and any 4 | > important design decisions you made. 5 | 6 | Closes #. 7 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - tagpr 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | tags-ignore: 8 | - 'v[0-9]+.[0-9]+.[0-9]+' 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Setup Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: go.mod 22 | id: go 23 | - name: Cache 24 | uses: actions/cache@v4 25 | with: 26 | path: | 27 | ~/go/pkg/mod 28 | ~/.cache/go-build 29 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 30 | restore-keys: | 31 | ${{ runner.os }}-go- 32 | - name: Unshallow 33 | run: git fetch --prune --unshallow --tags 34 | - name: build 35 | run: make build 36 | - name: test 37 | run: make test_view 38 | - name: Archive code coverage results 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: code-coverage-report 42 | path: cover.html 43 | 44 | reviewdog: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | - name: lint 50 | uses: reviewdog/action-golangci-lint@v2 51 | with: 52 | go_version_file: go.mod 53 | github_token: ${{ secrets.GITHUB_TOKEN }} 54 | reporter: github-pr-review 55 | level: error 56 | filter_mode: nofilter 57 | golangci_lint_flags: '--config=.golangci.yml' 58 | fail_on_error: true 59 | -------------------------------------------------------------------------------- /.github/workflows/manual.yml: -------------------------------------------------------------------------------- 1 | # for manual release (No triggering when tagpr is used.) 2 | name: manual 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | jobs: 8 | manual: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - uses: ./.github/actions/release 16 | with: 17 | github-token: ${{ secrets.GITHUB_TOKEN }} 18 | homebrew-tap-github-token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR' 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | 11 | permissions: 12 | pull-requests: write 13 | 14 | jobs: 15 | lint: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: amannn/action-semantic-pull-request@v5 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | types: | 24 | feat 25 | fix 26 | chore 27 | docs 28 | test 29 | ci 30 | refactor 31 | style 32 | perf 33 | revert 34 | Revert 35 | scopes: | 36 | deps 37 | main 38 | app 39 | io 40 | operation 41 | resourcetype 42 | version 43 | client 44 | requireScope: false 45 | ignoreLabels: | 46 | tagpr 47 | 48 | label: 49 | name: Manage labels 50 | runs-on: ubuntu-latest 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | MINOR_LABEL: 'minor-release' 54 | MINOR_LABEL_COLOR: '#FBCA04' 55 | MAJOR_LABEL: 'major-release' 56 | MAJOR_LABEL_COLOR: '#D93F0B' 57 | PATCH_LABEL: 'patch-release' 58 | PATCH_LABEL_COLOR: '#C5DEF5' 59 | steps: 60 | - name: Check out the repository 61 | uses: actions/checkout@v4 62 | - name: Create labels if they do not exist 63 | run: | 64 | EXISTING_LABELS=$(gh label list --json name --jq '.[].name') 65 | 66 | if ! echo "$EXISTING_LABELS" | grep -qx "$MINOR_LABEL"; then 67 | gh label create "$MINOR_LABEL" --color "$MINOR_LABEL_COLOR" 68 | fi 69 | 70 | if ! echo "$EXISTING_LABELS" | grep -qx "$MAJOR_LABEL"; then 71 | gh label create "$MAJOR_LABEL" --color "$MAJOR_LABEL_COLOR" 72 | fi 73 | 74 | if ! echo "$EXISTING_LABELS" | grep -qx "$PATCH_LABEL"; then 75 | gh label create "$PATCH_LABEL" --color "$PATCH_LABEL_COLOR" 76 | fi 77 | - name: Manage labels based on PR title 78 | run: | 79 | TITLE=$(jq -r '.pull_request.title' < "$GITHUB_EVENT_PATH") 80 | PR_NUMBER=${{ github.event.pull_request.number }} 81 | 82 | LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name') 83 | 84 | TAGPR_LABEL=$(echo "$LABELS" | grep -qx "tagpr" && echo "true" || echo "false") 85 | HAS_MINOR_LABEL=$(echo "$LABELS" | grep -qx "$MINOR_LABEL" && echo "true" || echo "false") 86 | HAS_MAJOR_LABEL=$(echo "$LABELS" | grep -qx "$MAJOR_LABEL" && echo "true" || echo "false") 87 | HAS_PATCH_LABEL=$(echo "$LABELS" | grep -qx "$PATCH_LABEL" && echo "true" || echo "false") 88 | 89 | if [ "$TAGPR_LABEL" = "true" ]; then 90 | exit 0 91 | fi 92 | 93 | if [ "$HAS_MAJOR_LABEL" = "true" ]; then 94 | if [ "$HAS_PATCH_LABEL" = "true" ];then 95 | gh pr edit $PR_NUMBER --remove-label "$PATCH_LABEL" 96 | fi 97 | if [ "$HAS_MINOR_LABEL" = "true" ];then 98 | gh pr edit $PR_NUMBER --remove-label "$MINOR_LABEL" 99 | fi 100 | exit 0 101 | fi 102 | 103 | if [[ $TITLE =~ ^feat.* ]]; then 104 | if [ "$HAS_MINOR_LABEL" = "false" ];then 105 | gh pr edit $PR_NUMBER --add-label "$MINOR_LABEL" 106 | fi 107 | if [ "$HAS_PATCH_LABEL" = "true" ];then 108 | gh pr edit $PR_NUMBER --remove-label "$PATCH_LABEL" 109 | fi 110 | else 111 | if [ "$HAS_PATCH_LABEL" = "false" ];then 112 | gh pr edit $PR_NUMBER --add-label "$PATCH_LABEL" 113 | fi 114 | if [ "$HAS_MINOR_LABEL" = "true" ];then 115 | gh pr edit $PR_NUMBER --remove-label "$MINOR_LABEL" 116 | fi 117 | fi 118 | -------------------------------------------------------------------------------- /.github/workflows/tagpr.yaml: -------------------------------------------------------------------------------- 1 | name: tagpr 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | jobs: 7 | tagpr: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: tagpr 15 | id: tagpr 16 | uses: Songmu/tagpr@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | - uses: ./.github/actions/release 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | homebrew-tap-github-token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 23 | if: steps.tagpr.outputs.tag != '' 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | dist/ 24 | cover.out.tmp 25 | cover.html 26 | /delstack 27 | test_root_output.yaml 28 | testfiles/ 29 | 30 | main 31 | -------------------------------------------------------------------------------- /.golangci.bck.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | govet: 3 | enable: 4 | - shadow 5 | 6 | linters: 7 | disable-all: true 8 | enable: 9 | - staticcheck 10 | - gofmt 11 | - govet 12 | - gocritic 13 | - unused 14 | - errcheck 15 | - gosimple 16 | - typecheck 17 | - misspell 18 | - goimports 19 | - gosec 20 | - ineffassign 21 | - unconvert 22 | - unparam 23 | - errname 24 | - errorlint 25 | - nilerr 26 | - nilnil -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - errcheck 6 | - errname 7 | - errorlint 8 | - gocritic 9 | - gosec 10 | - govet 11 | - ineffassign 12 | - misspell 13 | - nilerr 14 | - nilnil 15 | - staticcheck 16 | - unconvert 17 | - unparam 18 | - unused 19 | settings: 20 | govet: 21 | enable: 22 | - shadow 23 | exclusions: 24 | generated: lax 25 | presets: 26 | - comments 27 | - common-false-positives 28 | - legacy 29 | - std-error-handling 30 | paths: 31 | - third_party$ 32 | - builtin$ 33 | - examples$ 34 | formatters: 35 | enable: 36 | - gofmt 37 | - goimports 38 | exclusions: 39 | generated: lax 40 | paths: 41 | - third_party$ 42 | - builtin$ 43 | - examples$ 44 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: delstack 2 | env: 3 | - GO111MODULE=on 4 | before: 5 | hooks: 6 | - go mod tidy 7 | builds: 8 | - main: ./cmd/delstack/main.go 9 | binary: delstack 10 | ldflags: 11 | - -s -w 12 | - -X github.com/go-to-k/delstack/internal/version.Version={{.Version}} 13 | env: 14 | - CGO_ENABLED=0 15 | goos: 16 | - linux 17 | - windows 18 | - darwin 19 | archives: 20 | - name_template: >- 21 | {{ .ProjectName }}_{{ .Version }}_ 22 | {{- title .Os }}_ 23 | {{- if eq .Arch "amd64" }}x86_64 24 | {{- else if eq .Arch "386" }}i386 25 | {{- else }}{{ .Arch }}{{ end }} 26 | checksum: 27 | name_template: 'checksums.txt' 28 | snapshot: 29 | name_template: "{{ .Version }}-next" 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | brews: 37 | - repository: 38 | owner: go-to-k 39 | name: homebrew-tap 40 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 41 | directory: Formula 42 | homepage: https://github.com/go-to-k/delstack 43 | description: delstack 44 | test: | 45 | system "#{bin}/delstack -v" -------------------------------------------------------------------------------- /.tagpr: -------------------------------------------------------------------------------- 1 | # config file for the tagpr in git config format 2 | # The tagpr generates the initial configuration, which you can rewrite to suit your environment. 3 | # CONFIGURATIONS: 4 | # tagpr.releaseBranch 5 | # Generally, it is "main." It is the branch for releases. The tagpr tracks this branch, 6 | # creates or updates a pull request as a release candidate, or tags when they are merged. 7 | # 8 | # tagpr.versionFile 9 | # Versioning file containing the semantic version needed to be updated at release. 10 | # It will be synchronized with the "git tag". 11 | # Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc. 12 | # Sometimes the source code file, such as version.go or Bar.pm, is used. 13 | # If you do not want to use versioning files but only git tags, specify the "-" string here. 14 | # You can specify multiple version files by comma separated strings. 15 | # 16 | # tagpr.vPrefix 17 | # Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true) 18 | # This is only a tagging convention, not how it is described in the version file. 19 | # 20 | # tagpr.changelog (Optional) 21 | # Flag whether or not changelog is added or changed during the release. 22 | # 23 | # tagpr.command (Optional) 24 | # Command to change files just before release. 25 | # 26 | # tagpr.template (Optional) 27 | # Pull request template in go template format 28 | # 29 | # tagpr.release (Optional) 30 | # GitHub Release creation behavior after tagging [true, draft, false] 31 | # If this value is not set, the release is to be created. 32 | # 33 | # tagpr.majorLabels (Optional) 34 | # Label of major update targets. Default is [major] 35 | # 36 | # tagpr.minorLabels (Optional) 37 | # Label of minor update targets. Default is [minor] 38 | # 39 | [tagpr] 40 | vPrefix = true 41 | releaseBranch = main 42 | versionFile = - 43 | majorLabels = major-release 44 | minorLabels = minor-release 45 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "streetsidesoftware.code-spell-checker" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "delstack", 4 | "jessevdk", 5 | "errgroup", 6 | "zerolog", 7 | "Msgf", 8 | "tablewriter", 9 | "ldflags", 10 | "resourcetype", 11 | "goreleaser", 12 | "Unshallow", 13 | "unshallow", 14 | "gomod", 15 | "retryable", 16 | "retryer", 17 | ], 18 | "gopls": { 19 | "analyses": { 20 | "shadow": true 21 | } 22 | }, 23 | "go.lintTool": "golangci-lint", 24 | "go.lintFlags": [ 25 | "--fast" 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 K-Goto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RED=\033[31m 2 | GREEN=\033[32m 3 | RESET=\033[0m 4 | 5 | COLORIZE_PASS = sed "s/^\([- ]*\)\(PASS\)/\1$$(printf "$(GREEN)")\2$$(printf "$(RESET)")/g" 6 | COLORIZE_FAIL = sed "s/^\([- ]*\)\(FAIL\)/\1$$(printf "$(RED)")\2$$(printf "$(RESET)")/g" 7 | 8 | VERSION := $(shell git describe --tags --abbrev=0) 9 | REVISION := $(shell git rev-parse --short HEAD) 10 | LDFLAGS := -s -w \ 11 | -X 'github.com/go-to-k/delstack/internal/version.Version=$(VERSION)' \ 12 | -X 'github.com/go-to-k/delstack/internal/version.Revision=$(REVISION)' 13 | GO_FILES := $(shell find . -type f -name '*.go' -print) 14 | 15 | DIFF_FILE := "$$(git diff --name-only --diff-filter=ACMRT | grep .go$ | xargs -I{} dirname {} | sort | uniq | xargs -I{} echo ./{})" 16 | 17 | TEST_DIFF_RESULT := "$$(go test -race -cover -v $$(echo $(DIFF_FILE)) -coverpkg=./...)" 18 | TEST_FULL_RESULT := "$$(go test -race -cover -v ./... -coverpkg=./...)" 19 | TEST_COV_RESULT := "$$(go test -race -cover -v ./... -coverpkg=./... -coverprofile=cover.out.tmp)" 20 | 21 | FAIL_CHECK := "^[^\s\t]*FAIL[^\s\t]*$$" 22 | 23 | .PHONY: test_diff test test_view lint lint_diff mockgen shadow cognit deadcode run build install clean testgen testgen_help 24 | 25 | test_diff: 26 | @! echo $(TEST_DIFF_RESULT) | $(COLORIZE_PASS) | $(COLORIZE_FAIL) | tee /dev/stderr | grep $(FAIL_CHECK) > /dev/null 27 | test: 28 | @! echo $(TEST_FULL_RESULT) | $(COLORIZE_PASS) | $(COLORIZE_FAIL) | tee /dev/stderr | grep $(FAIL_CHECK) > /dev/null 29 | test_view: 30 | @! echo $(TEST_COV_RESULT) | $(COLORIZE_PASS) | $(COLORIZE_FAIL) | tee /dev/stderr | grep $(FAIL_CHECK) > /dev/null 31 | cat cover.out.tmp | grep -v "**_mock.go" > cover.out 32 | rm cover.out.tmp 33 | go tool cover -func=cover.out 34 | go tool cover -html=cover.out -o cover.html 35 | lint: 36 | golangci-lint run 37 | lint_diff: 38 | golangci-lint run $$(echo $(DIFF_FILE)) 39 | mockgen: 40 | go generate ./... 41 | shadow: 42 | find . -type f -name '*.go' | sed -e "s/\/[^\.\/]*\.go//g" | uniq | xargs shadow 43 | cognit: 44 | gocognit -top 10 ./ | grep -v "_test.go" 45 | deadcode: 46 | deadcode ./... 47 | run: 48 | go mod tidy 49 | go run -ldflags "$(LDFLAGS)" cmd/delstack/main.go $${OPT} 50 | build: $(GO_FILES) 51 | go mod tidy 52 | go build -ldflags "$(LDFLAGS)" -o delstack cmd/delstack/main.go 53 | install: 54 | go install -ldflags "$(LDFLAGS)" github.com/go-to-k/delstack/cmd/delstack 55 | clean: 56 | go clean 57 | rm -f delstack 58 | 59 | # Test stack generation commands 60 | # ================================== 61 | 62 | # Run test stack generator 63 | testgen: 64 | @echo "Running test stack generator..." 65 | @cd testdata && go mod tidy && go run deploy.go $(OPT) 66 | 67 | # Run test stack generator for all RETAIN resources to test \`-f\` option 68 | testgen_retain: 69 | @echo "Running test stack generator for all RETAIN resources..." 70 | @cd testdata && go mod tidy && go run deploy.go -r $(OPT) 71 | 72 | # Help for test stack generation 73 | testgen_help: 74 | @echo "Test stack generation targets:" 75 | @echo " testgen - Run the test stack generator" 76 | @echo " testgen_retain - Run the test stack generator for all RETAIN resources to test \`-f\` option" 77 | @echo "" 78 | @echo "Example usage:" 79 | @echo " make testgen" 80 | @echo " make testgen OPT=\"-s my-stage\"" 81 | @echo " make testgen OPT=\"-p my-profile\"" 82 | @echo " make testgen_retain" 83 | @echo " make testgen_retain OPT=\"-s my-stage\"" 84 | @echo " make testgen_retain OPT=\"-p my-profile\"" -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "delstack-action" 2 | author: "k.goto" 3 | description: "Run delstack in GitHub Actions" 4 | branding: 5 | icon: "command" 6 | color: "blue" 7 | inputs: 8 | stack-name: 9 | description: "Names of one or multiple stacks you want to delete (comma separated)" 10 | required: false 11 | region: 12 | description: "AWS Region" 13 | default: "us-east-1" 14 | required: false 15 | force: 16 | description: "Force Mode to delete stacks including resources with the deletion policy Retain or RetainExceptOnCreate" 17 | default: false 18 | required: false 19 | runs: 20 | using: "composite" 21 | steps: 22 | - shell: bash 23 | run: | 24 | set -eu 25 | if [ ! -e /usr/local/bin/delstack ]; then 26 | DOWNLOAD_URL=$(curl https://api.github.com/repos/go-to-k/delstack/releases/latest | jq -r '.assets[].browser_download_url|select(match("Linux_x86_64."))') 27 | cd /tmp 28 | curl -sfLO ${DOWNLOAD_URL} 29 | FILENAME=$(basename $DOWNLOAD_URL) 30 | tar xzvf ${FILENAME} 31 | chmod +x delstack 32 | sudo mv delstack /usr/local/bin/ 33 | rm ${FILENAME} 34 | fi 35 | if [ -n "${{ inputs.stack-name }}" ]; then 36 | stacks="" 37 | for stack in $(echo ${{ inputs.stack-name }} | tr ',' ' '); do 38 | stacks="${stacks}-s ${stack} " 39 | done 40 | force="" 41 | if [ "${{ inputs.force }}" = "true" ]; then 42 | force="-f" 43 | fi 44 | delstack -r ${{ inputs.region }} $stacks $force 45 | fi -------------------------------------------------------------------------------- /cmd/delstack/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/go-to-k/delstack/internal/app" 8 | "github.com/go-to-k/delstack/internal/io" 9 | "github.com/go-to-k/delstack/internal/version" 10 | ) 11 | 12 | func main() { 13 | io.NewLogger(version.IsDebug()) 14 | ctx := context.Background() 15 | app := app.NewApp(version.GetVersion()) 16 | 17 | if err := app.Run(ctx); err != nil { 18 | io.Logger.Error().Msg(err.Error()) 19 | os.Exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-to-k/delstack 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.0 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go-v2 v1.36.3 9 | github.com/aws/aws-sdk-go-v2/config v1.27.27 10 | github.com/aws/aws-sdk-go-v2/service/backup v1.36.3 11 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.53.3 12 | github.com/aws/aws-sdk-go-v2/service/ecr v1.32.0 13 | github.com/aws/aws-sdk-go-v2/service/iam v1.34.3 14 | github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 15 | github.com/aws/aws-sdk-go-v2/service/s3tables v1.2.2 16 | github.com/aws/smithy-go v1.22.2 17 | github.com/charmbracelet/bubbletea v0.27.0 18 | github.com/fatih/color v1.17.0 19 | github.com/olekukonko/tablewriter v0.0.5 20 | github.com/rs/zerolog v1.33.0 21 | github.com/urfave/cli/v2 v2.27.4 22 | go.uber.org/goleak v1.3.0 23 | go.uber.org/mock v0.4.0 24 | golang.org/x/sync v0.8.0 25 | ) 26 | 27 | require ( 28 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect 29 | github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect 30 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect 31 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 32 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 33 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 34 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect 41 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect 42 | github.com/charmbracelet/x/ansi v0.1.4 // indirect 43 | github.com/charmbracelet/x/input v0.1.0 // indirect 44 | github.com/charmbracelet/x/term v0.1.1 // indirect 45 | github.com/charmbracelet/x/windows v0.1.0 // indirect 46 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 47 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 48 | github.com/jmespath/go-jmespath v0.4.0 // indirect 49 | github.com/mattn/go-colorable v0.1.13 // indirect 50 | github.com/mattn/go-isatty v0.0.20 // indirect 51 | github.com/mattn/go-localereader v0.0.1 // indirect 52 | github.com/mattn/go-runewidth v0.0.15 // indirect 53 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 54 | github.com/muesli/cancelreader v0.2.2 // indirect 55 | github.com/rivo/uniseg v0.4.7 // indirect 56 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 57 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 58 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 59 | golang.org/x/sys v0.24.0 // indirect 60 | golang.org/x/text v0.3.8 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script automates the installation of the delstack tool. 4 | # It checks for the specified version (or fetches the latest one), 5 | # downloads the binary, and installs it on the system. 6 | 7 | # Check for required tools: curl and tar. 8 | # These tools are necessary for downloading and extracting the delstack binary. 9 | if ! command -v curl &>/dev/null; then 10 | echo "curl could not be found" 11 | exit 1 12 | fi 13 | 14 | if ! command -v tar &>/dev/null; then 15 | echo "tar could not be found" 16 | exit 1 17 | fi 18 | 19 | # Determine the version of delstack to install. 20 | # If no version is specified as a command line argument, fetch the latest version. 21 | if [ -z "$1" ]; then 22 | VERSION=$(curl -s https://api.github.com/repos/go-to-k/delstack/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') 23 | if [ -z "$VERSION" ]; then 24 | echo "Failed to fetch the latest version" 25 | exit 1 26 | fi 27 | else 28 | VERSION=$1 29 | fi 30 | 31 | # Normalize the version string by removing any leading 'v'. 32 | VERSION=${VERSION#v} 33 | 34 | # Detect the architecture of the current system. 35 | # This script supports x86_64, arm64, and i386 architectures. 36 | ARCH=$(uname -m) 37 | case $ARCH in 38 | x86_64 | amd64) ARCH="x86_64" ;; 39 | arm64 | aarch64) ARCH="arm64" ;; 40 | i386 | i686) ARCH="i386" ;; 41 | *) 42 | echo "Unsupported architecture: $ARCH" 43 | exit 1 44 | ;; 45 | esac 46 | 47 | # Detect the operating system (OS) of the current system. 48 | # This script supports Linux, Darwin (macOS) and Windows operating systems. 49 | OS=$(uname -s) 50 | case $OS in 51 | Linux) OS="Linux" ;; 52 | Darwin) OS="Darwin" ;; 53 | MINGW* | MSYS* | CYGWIN*) OS="Windows" ;; 54 | *) 55 | echo "Unsupported OS: $OS" 56 | exit 1 57 | ;; 58 | esac 59 | 60 | # Construct the download URL for the delstack binary based on the version, OS, and architecture. 61 | FILE_NAME="delstack_${VERSION}_${OS}_${ARCH}.tar.gz" 62 | URL="https://github.com/go-to-k/delstack/releases/download/v${VERSION}/${FILE_NAME}" 63 | 64 | # Download the delstack binary. 65 | echo "Downloading delstack..." 66 | if ! curl -L -o "$FILE_NAME" "$URL"; then 67 | echo "Failed to download delstack" 68 | exit 1 69 | fi 70 | 71 | # Install delstack. 72 | # This involves extracting the binary and moving it to /usr/local/bin. 73 | echo "Installing delstack..." 74 | if ! tar -xzf "$FILE_NAME"; then 75 | echo "Failed to extract delstack" 76 | exit 1 77 | fi 78 | if ! sudo mv delstack /usr/local/bin/delstack; then 79 | echo "Failed to install delstack" 80 | exit 1 81 | fi 82 | 83 | # Clean up by removing the downloaded tar file. 84 | rm "$FILE_NAME" 85 | 86 | echo "delstack installation complete." 87 | echo "Run 'delstack -h' to see how to use delstack." 88 | -------------------------------------------------------------------------------- /internal/io/input.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/fatih/color" 11 | ) 12 | 13 | func GetCheckboxes(headers []string, opts []string, noSelectionMode bool) ([]string, bool, error) { 14 | for { 15 | ui := NewUI(opts, headers) 16 | p := tea.NewProgram(ui) 17 | if _, err := p.Run(); err != nil { 18 | return nil, false, err 19 | } 20 | 21 | checkboxes := []string{} 22 | for c := range ui.Choices { 23 | if _, ok := ui.Selected[c]; ok { 24 | checkboxes = append(checkboxes, ui.Choices[c]) 25 | } 26 | } 27 | 28 | switch { 29 | case ui.IsCanceled: 30 | Logger.Warn().Msg("Canceled!") 31 | case len(checkboxes) == 0 && noSelectionMode: 32 | ok := GetYesNo("No selection?") 33 | if ok { 34 | return checkboxes, true, nil 35 | } 36 | case len(checkboxes) == 0: 37 | Logger.Warn().Msg("Not selected!") 38 | } 39 | if len(checkboxes) == 0 || ui.IsCanceled { 40 | ok := GetYesNo("Do you want to finish?") 41 | if ok { 42 | Logger.Info().Msg("Finished...") 43 | return checkboxes, false, nil 44 | } 45 | continue 46 | } 47 | 48 | fmt.Fprintf(os.Stderr, " %s\n", color.CyanString(strings.Join(checkboxes, ", "))) 49 | 50 | ok := GetYesNo("OK?") 51 | if ok { 52 | return checkboxes, true, nil 53 | } 54 | } 55 | } 56 | 57 | func InputKeywordForFilter(label string) string { 58 | reader := bufio.NewReader(os.Stdin) 59 | 60 | fmt.Fprintf(os.Stderr, "%s", label) 61 | s, _ := reader.ReadString('\n') 62 | fmt.Fprintln(os.Stderr) 63 | 64 | s = strings.TrimSpace(s) 65 | 66 | return s 67 | } 68 | 69 | func GetYesNo(label string) bool { 70 | choices := "Y/n" 71 | r := bufio.NewReader(os.Stdin) 72 | var s string 73 | 74 | for { 75 | fmt.Fprintf(os.Stderr, "%s (%s) ", label, choices) 76 | s, _ = r.ReadString('\n') 77 | fmt.Fprintln(os.Stderr) 78 | 79 | s = strings.TrimSpace(s) 80 | if s == "" { 81 | return true 82 | } 83 | s = strings.ToLower(s) 84 | if s == "y" || s == "yes" { 85 | return true 86 | } 87 | if s == "n" || s == "no" { 88 | return false 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/io/logger.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | var Logger *zerolog.Logger 10 | 11 | func NewLogger(isDebug bool) { 12 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 13 | if isDebug { 14 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 15 | } 16 | 17 | consoleWriter := zerolog.ConsoleWriter{ 18 | Out: os.Stderr, 19 | PartsExclude: []string{ 20 | zerolog.TimestampFieldName, 21 | }, 22 | } 23 | 24 | l := zerolog.New(&consoleWriter) 25 | 26 | Logger = &l 27 | } 28 | -------------------------------------------------------------------------------- /internal/io/tablewriter.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/olekukonko/tablewriter" 7 | ) 8 | 9 | func ToStringAsTableFormat(header []string, data [][]string) *string { 10 | tableString := &strings.Builder{} 11 | table := tablewriter.NewWriter(tableString) 12 | table.SetHeader(header) 13 | table.SetRowLine(true) 14 | table.SetAutoWrapText(false) 15 | table.AppendBulk(data) 16 | table.Render() 17 | 18 | stringAsTableFormat := tableString.String() 19 | return &stringAsTableFormat 20 | } 21 | -------------------------------------------------------------------------------- /internal/io/ui.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/fatih/color" 9 | ) 10 | 11 | const SelectionPageSize = 20 12 | 13 | type UI struct { 14 | Choices []string 15 | Headers []string 16 | Cursor int 17 | Selected map[int]struct{} 18 | Filtered *Filtered 19 | Keyword string 20 | IsEntered bool 21 | IsCanceled bool 22 | } 23 | 24 | type Filtered struct { 25 | Choices map[int]struct{} 26 | Prev *Filtered 27 | Cursor int 28 | } 29 | 30 | var _ tea.Model = (*UI)(nil) 31 | 32 | func NewUI(choices []string, headers []string) *UI { 33 | return &UI{ 34 | Choices: choices, 35 | Headers: headers, 36 | Selected: make(map[int]struct{}), 37 | } 38 | } 39 | 40 | func (u *UI) Init() tea.Cmd { 41 | filtered := make(map[int]struct{}) 42 | for i := range u.Choices { 43 | filtered[i] = struct{}{} 44 | } 45 | u.Filtered = &Filtered{Choices: filtered} 46 | 47 | return nil 48 | } 49 | 50 | func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 51 | //nolint:gocritic 52 | switch msg := msg.(type) { 53 | 54 | case tea.KeyMsg: 55 | 56 | switch msg.Type { 57 | 58 | // Quit the selection 59 | case tea.KeyEnter: 60 | u.IsEntered = true 61 | return u, tea.Quit 62 | 63 | // Quit the selection 64 | case tea.KeyCtrlC: 65 | u.IsCanceled = true 66 | return u, tea.Quit 67 | 68 | case tea.KeyUp, tea.KeyShiftTab: 69 | if len(u.Filtered.Choices) < 2 { 70 | return u, nil 71 | } 72 | for range u.Choices { 73 | if u.Cursor == 0 { 74 | u.Cursor = len(u.Choices) - 1 75 | } else if u.Cursor > 0 { 76 | u.Cursor-- 77 | } 78 | 79 | if _, ok := u.Filtered.Choices[u.Cursor]; !ok { 80 | continue 81 | } 82 | if u.Filtered.Cursor == 0 { 83 | u.Filtered.Cursor = len(u.Filtered.Choices) - 1 84 | } else if u.Filtered.Cursor > 0 { 85 | u.Filtered.Cursor-- 86 | } 87 | 88 | f := u.Filtered 89 | for f.Prev != nil { 90 | f.Prev.Cursor = u.Filtered.Cursor 91 | f = f.Prev 92 | } 93 | break 94 | } 95 | 96 | case tea.KeyDown, tea.KeyTab: 97 | if len(u.Filtered.Choices) < 2 { 98 | return u, nil 99 | } 100 | for range u.Choices { 101 | if u.Cursor < len(u.Choices)-1 { 102 | u.Cursor++ 103 | } else if u.Cursor == len(u.Choices)-1 { 104 | u.Cursor = 0 105 | } 106 | 107 | if _, ok := u.Filtered.Choices[u.Cursor]; !ok { 108 | continue 109 | } 110 | if u.Filtered.Cursor < len(u.Filtered.Choices)-1 { 111 | u.Filtered.Cursor++ 112 | } else if u.Filtered.Cursor == len(u.Filtered.Choices)-1 { 113 | u.Filtered.Cursor = 0 114 | } 115 | 116 | f := u.Filtered 117 | for f.Prev != nil { 118 | f.Prev.Cursor = u.Filtered.Cursor 119 | f = f.Prev 120 | } 121 | break 122 | } 123 | 124 | // select or deselect an item 125 | case tea.KeySpace: 126 | if _, ok := u.Filtered.Choices[u.Cursor]; !ok { 127 | return u, nil 128 | } 129 | _, ok := u.Selected[u.Cursor] 130 | if ok { 131 | delete(u.Selected, u.Cursor) 132 | } else { 133 | u.Selected[u.Cursor] = struct{}{} 134 | } 135 | 136 | // select all items in filtered list 137 | case tea.KeyRight: 138 | for i := range u.Choices { 139 | if _, ok := u.Filtered.Choices[i]; !ok { 140 | continue 141 | } 142 | _, ok := u.Selected[i] 143 | if !ok { 144 | u.Selected[i] = struct{}{} 145 | } 146 | } 147 | 148 | // clear all selected items in filtered list 149 | case tea.KeyLeft: 150 | for i := range u.Choices { 151 | if _, ok := u.Filtered.Choices[i]; !ok { 152 | continue 153 | } 154 | _, ok := u.Selected[i] 155 | if ok { 156 | delete(u.Selected, i) 157 | } 158 | } 159 | 160 | // clear one character from the keyword 161 | case tea.KeyBackspace: 162 | u.backspace() 163 | 164 | // clear the keyword 165 | case tea.KeyCtrlW: 166 | for u.Keyword != "" { 167 | u.backspace() 168 | } 169 | 170 | // add a character to the keyword 171 | case tea.KeyRunes: 172 | str := msg.String() 173 | if !msg.Paste { 174 | for _, r := range str { 175 | u.addCharacter(string(r)) 176 | } 177 | } else { 178 | if strings.Contains(str, string('\n')) || strings.Contains(str, string('\r')) { 179 | u.IsEntered = true 180 | return u, tea.Quit 181 | } 182 | 183 | runes := []rune(str) 184 | for i, r := range runes { 185 | // characters by paste key are enclosed by '[' and ']' 186 | if i == 0 || i == len(runes)-1 { 187 | continue 188 | } 189 | if r != ' ' && r != '\t' { 190 | u.addCharacter(string(r)) 191 | } 192 | } 193 | } 194 | 195 | } 196 | } 197 | 198 | return u, nil 199 | } 200 | 201 | func (u *UI) backspace() { 202 | if len(u.Keyword) == 0 { 203 | return 204 | } 205 | 206 | keywordRunes := []rune(u.Keyword) 207 | keywordRunes = keywordRunes[:len(keywordRunes)-1] 208 | u.Keyword = string(keywordRunes) 209 | u.Filtered = u.Filtered.Prev 210 | cnt := 0 211 | for i := range u.Choices { 212 | if _, ok := u.Filtered.Choices[i]; !ok { 213 | continue 214 | } 215 | if cnt == u.Filtered.Cursor { 216 | u.Cursor = i 217 | break 218 | } 219 | cnt++ 220 | } 221 | } 222 | 223 | func (u *UI) addCharacter(c string) { 224 | u.Keyword += c 225 | u.Filtered = &Filtered{ 226 | Choices: make(map[int]struct{}), 227 | Prev: u.Filtered, 228 | } 229 | 230 | tmpCursor := u.Cursor 231 | for i, choice := range u.Choices { 232 | lk := strings.ToLower(u.Keyword) 233 | lc := strings.ToLower(choice) 234 | contains := strings.Contains(lc, lk) 235 | 236 | fLen := len(u.Filtered.Choices) 237 | if contains && fLen != 0 && fLen <= u.Filtered.Prev.Cursor { 238 | u.Filtered.Cursor++ 239 | u.Cursor = i 240 | } 241 | 242 | switch { 243 | case contains: 244 | u.Filtered.Choices[i] = struct{}{} 245 | tmpCursor = i 246 | case u.Cursor == i && u.Cursor < len(u.Choices)-1: 247 | u.Cursor++ 248 | case u.Cursor == i: 249 | u.Cursor = tmpCursor 250 | } 251 | } 252 | 253 | if len(u.Filtered.Choices) == 0 { 254 | return 255 | } 256 | f := u.Filtered 257 | for f.Prev != nil { 258 | f.Prev.Cursor = u.Filtered.Cursor 259 | f = f.Prev 260 | } 261 | } 262 | 263 | func (u *UI) View() string { 264 | bold := color.New(color.Bold) 265 | 266 | s := color.CyanString("? ") 267 | 268 | for _, header := range u.Headers { 269 | s += bold.Sprintln(header) 270 | } 271 | 272 | if u.IsEntered && len(u.Selected) != 0 { 273 | return s 274 | } 275 | 276 | s += bold.Sprintln(u.Keyword) 277 | 278 | s += color.CyanString(" [Use arrows to move, space to select, to all, to none, type to filter]") 279 | s += "\n" 280 | 281 | var contents []string 282 | for i, choice := range u.Choices { 283 | if _, ok := u.Filtered.Choices[i]; !ok { 284 | continue 285 | } 286 | 287 | cursor := " " // no cursor 288 | if u.Cursor == i { 289 | cursor = color.CyanString(bold.Sprint(">")) // cursor! 290 | } 291 | 292 | checked := bold.Sprint("[ ]") // not selected 293 | if _, ok := u.Selected[i]; ok { 294 | checked = color.GreenString("[x]") // selected! 295 | } 296 | 297 | contents = append(contents, fmt.Sprintf("%s %s %s\n", cursor, checked, choice)) 298 | } 299 | 300 | if len(contents) > SelectionPageSize { 301 | switch { 302 | case u.Filtered.Cursor < SelectionPageSize/2: 303 | contents = contents[:SelectionPageSize] 304 | case u.Filtered.Cursor > len(contents)-SelectionPageSize/2: 305 | contents = contents[len(contents)-SelectionPageSize:] 306 | default: 307 | contents = contents[u.Filtered.Cursor-SelectionPageSize/2 : u.Filtered.Cursor+SelectionPageSize/2] 308 | } 309 | } 310 | 311 | s += strings.Join(contents, "") 312 | return s 313 | } 314 | -------------------------------------------------------------------------------- /internal/operation/backup_vault.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | "github.com/go-to-k/delstack/pkg/client" 9 | "golang.org/x/sync/errgroup" 10 | "golang.org/x/sync/semaphore" 11 | ) 12 | 13 | var _ IOperator = (*BackupVaultOperator)(nil) 14 | 15 | type BackupVaultOperator struct { 16 | client client.IBackup 17 | resources []*types.StackResourceSummary 18 | } 19 | 20 | func NewBackupVaultOperator(client client.IBackup) *BackupVaultOperator { 21 | return &BackupVaultOperator{ 22 | client: client, 23 | resources: []*types.StackResourceSummary{}, 24 | } 25 | } 26 | 27 | func (o *BackupVaultOperator) AddResource(resource *types.StackResourceSummary) { 28 | o.resources = append(o.resources, resource) 29 | } 30 | 31 | func (o *BackupVaultOperator) GetResourcesLength() int { 32 | return len(o.resources) 33 | } 34 | 35 | func (o *BackupVaultOperator) DeleteResources(ctx context.Context) error { 36 | eg, ctx := errgroup.WithContext(ctx) 37 | sem := semaphore.NewWeighted(int64(runtime.NumCPU())) 38 | 39 | for _, backupVault := range o.resources { 40 | backupVault := backupVault 41 | if err := sem.Acquire(ctx, 1); err != nil { 42 | return err 43 | } 44 | eg.Go(func() error { 45 | defer sem.Release(1) 46 | 47 | return o.DeleteBackupVault(ctx, backupVault.PhysicalResourceId) 48 | }) 49 | } 50 | 51 | return eg.Wait() 52 | } 53 | 54 | func (o *BackupVaultOperator) DeleteBackupVault(ctx context.Context, backupVaultName *string) error { 55 | exists, err := o.client.CheckBackupVaultExists(ctx, backupVaultName) 56 | if err != nil { 57 | return err 58 | } 59 | if !exists { 60 | return nil 61 | } 62 | 63 | recoveryPoints, err := o.client.ListRecoveryPointsByBackupVault(ctx, backupVaultName) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if len(recoveryPoints) > 0 { 69 | if err := o.client.DeleteRecoveryPoints(ctx, backupVaultName, recoveryPoints); err != nil { 70 | return err 71 | } 72 | } 73 | 74 | if err := o.client.DeleteBackupVault(ctx, backupVaultName); err != nil { 75 | return err 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /internal/operation/backup_vault_test.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/backup/types" 10 | cfnTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 11 | "github.com/go-to-k/delstack/internal/io" 12 | "github.com/go-to-k/delstack/pkg/client" 13 | gomock "go.uber.org/mock/gomock" 14 | ) 15 | 16 | /* 17 | Test Cases 18 | */ 19 | 20 | func TestBackupVaultOperator_DeleteBackupVault(t *testing.T) { 21 | io.NewLogger(false) 22 | 23 | type args struct { 24 | ctx context.Context 25 | backupVaultName *string 26 | } 27 | 28 | cases := []struct { 29 | name string 30 | args args 31 | prepareMockFn func(m *client.MockIBackup) 32 | want error 33 | wantErr bool 34 | }{ 35 | { 36 | name: "delete backup vault successfully", 37 | args: args{ 38 | ctx: context.Background(), 39 | backupVaultName: aws.String("test"), 40 | }, 41 | prepareMockFn: func(m *client.MockIBackup) { 42 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("test")).Return(true, nil) 43 | m.EXPECT().ListRecoveryPointsByBackupVault(gomock.Any(), aws.String("test")).Return( 44 | []types.RecoveryPointByBackupVault{ 45 | { 46 | BackupVaultName: aws.String("BackupVaultName1"), 47 | BackupVaultArn: aws.String("BackupVaultArn1"), 48 | }, 49 | { 50 | BackupVaultName: aws.String("BackupVaultName2"), 51 | BackupVaultArn: aws.String("BackupVaultArn2"), 52 | }, 53 | }, nil) 54 | m.EXPECT().DeleteRecoveryPoints(gomock.Any(), aws.String("test"), gomock.Any()).Return(nil) 55 | m.EXPECT().DeleteBackupVault(gomock.Any(), aws.String("test")).Return(nil) 56 | }, 57 | want: nil, 58 | wantErr: false, 59 | }, 60 | { 61 | name: "delete backup vault failure for check backup vault exists errors", 62 | args: args{ 63 | ctx: context.Background(), 64 | backupVaultName: aws.String("test"), 65 | }, 66 | prepareMockFn: func(m *client.MockIBackup) { 67 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("test")).Return(false, fmt.Errorf("ListBackupVaultsError")) 68 | }, 69 | want: fmt.Errorf("ListBackupVaultsError"), 70 | wantErr: true, 71 | }, 72 | { 73 | name: "delete backup vault successfully for backup vault not exists", 74 | args: args{ 75 | ctx: context.Background(), 76 | backupVaultName: aws.String("test"), 77 | }, 78 | prepareMockFn: func(m *client.MockIBackup) { 79 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("test")).Return(false, nil) 80 | }, 81 | want: nil, 82 | wantErr: false, 83 | }, 84 | { 85 | name: "delete backup vault failure for list recovery points errors", 86 | args: args{ 87 | ctx: context.Background(), 88 | backupVaultName: aws.String("test"), 89 | }, 90 | prepareMockFn: func(m *client.MockIBackup) { 91 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("test")).Return(true, nil) 92 | m.EXPECT().ListRecoveryPointsByBackupVault(gomock.Any(), aws.String("test")).Return(nil, fmt.Errorf("ListRecoveryPointsByBackupVaultError")) 93 | }, 94 | want: fmt.Errorf("ListRecoveryPointsByBackupVaultError"), 95 | wantErr: true, 96 | }, 97 | { 98 | name: "delete backup vault failure for delete recovery points errors", 99 | args: args{ 100 | ctx: context.Background(), 101 | backupVaultName: aws.String("test"), 102 | }, 103 | prepareMockFn: func(m *client.MockIBackup) { 104 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("test")).Return(true, nil) 105 | m.EXPECT().ListRecoveryPointsByBackupVault(gomock.Any(), aws.String("test")).Return( 106 | []types.RecoveryPointByBackupVault{ 107 | { 108 | BackupVaultName: aws.String("BackupVaultName1"), 109 | BackupVaultArn: aws.String("BackupVaultArn1"), 110 | }, 111 | { 112 | BackupVaultName: aws.String("BackupVaultName2"), 113 | BackupVaultArn: aws.String("BackupVaultArn2"), 114 | }, 115 | }, nil) 116 | m.EXPECT().DeleteRecoveryPoints(gomock.Any(), aws.String("test"), gomock.Any()).Return(fmt.Errorf("DeleteRecoveryPointsError")) 117 | }, 118 | want: fmt.Errorf("DeleteRecoveryPointsError"), 119 | wantErr: true, 120 | }, 121 | { 122 | name: "delete backup vault successfully for delete recovery points errors after zero length", 123 | args: args{ 124 | ctx: context.Background(), 125 | backupVaultName: aws.String("test"), 126 | }, 127 | prepareMockFn: func(m *client.MockIBackup) { 128 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("test")).Return(true, nil) 129 | m.EXPECT().ListRecoveryPointsByBackupVault(gomock.Any(), aws.String("test")).Return([]types.RecoveryPointByBackupVault{}, nil) 130 | m.EXPECT().DeleteBackupVault(gomock.Any(), aws.String("test")).Return(nil) 131 | }, 132 | want: nil, 133 | wantErr: false, 134 | }, 135 | { 136 | name: "delete backup vault failure for delete backup vault errors", 137 | args: args{ 138 | ctx: context.Background(), 139 | backupVaultName: aws.String("test"), 140 | }, 141 | prepareMockFn: func(m *client.MockIBackup) { 142 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("test")).Return(true, nil) 143 | m.EXPECT().ListRecoveryPointsByBackupVault(gomock.Any(), aws.String("test")).Return( 144 | []types.RecoveryPointByBackupVault{ 145 | { 146 | BackupVaultName: aws.String("BackupVaultName1"), 147 | BackupVaultArn: aws.String("BackupVaultArn1"), 148 | }, 149 | { 150 | BackupVaultName: aws.String("BackupVaultName2"), 151 | BackupVaultArn: aws.String("BackupVaultArn2"), 152 | }, 153 | }, nil) 154 | m.EXPECT().DeleteRecoveryPoints(gomock.Any(), aws.String("test"), gomock.Any()).Return(nil) 155 | m.EXPECT().DeleteBackupVault(gomock.Any(), aws.String("test")).Return(fmt.Errorf("DeleteBackupVaultError")) 156 | }, 157 | want: fmt.Errorf("DeleteBackupVaultError"), 158 | wantErr: true, 159 | }, 160 | } 161 | 162 | for _, tt := range cases { 163 | t.Run(tt.name, func(t *testing.T) { 164 | ctrl := gomock.NewController(t) 165 | backupMock := client.NewMockIBackup(ctrl) 166 | tt.prepareMockFn(backupMock) 167 | 168 | backupOperator := NewBackupVaultOperator(backupMock) 169 | 170 | err := backupOperator.DeleteBackupVault(tt.args.ctx, tt.args.backupVaultName) 171 | if (err != nil) != tt.wantErr { 172 | t.Errorf("error = %#v, wantErr %#v", err.Error(), tt.wantErr) 173 | return 174 | } 175 | if tt.wantErr && err.Error() != tt.want.Error() { 176 | t.Errorf("err = %#v, want %#v", err.Error(), tt.want.Error()) 177 | return 178 | } 179 | }) 180 | } 181 | } 182 | 183 | func TestBackupVaultOperator_DeleteResourcesForBackupVault(t *testing.T) { 184 | io.NewLogger(false) 185 | 186 | type args struct { 187 | ctx context.Context 188 | } 189 | 190 | cases := []struct { 191 | name string 192 | args args 193 | prepareMockFn func(m *client.MockIBackup) 194 | want error 195 | wantErr bool 196 | }{ 197 | { 198 | name: "delete resources successfully", 199 | args: args{ 200 | ctx: context.Background(), 201 | }, 202 | prepareMockFn: func(m *client.MockIBackup) { 203 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("PhysicalResourceId1")).Return(true, nil) 204 | m.EXPECT().ListRecoveryPointsByBackupVault(gomock.Any(), aws.String("PhysicalResourceId1")).Return( 205 | []types.RecoveryPointByBackupVault{ 206 | { 207 | BackupVaultName: aws.String("BackupVaultName1"), 208 | BackupVaultArn: aws.String("BackupVaultArn1"), 209 | }, 210 | { 211 | BackupVaultName: aws.String("BackupVaultName2"), 212 | BackupVaultArn: aws.String("BackupVaultArn2"), 213 | }, 214 | }, nil) 215 | m.EXPECT().DeleteRecoveryPoints(gomock.Any(), aws.String("PhysicalResourceId1"), gomock.Any()).Return(nil) 216 | m.EXPECT().DeleteBackupVault(gomock.Any(), aws.String("PhysicalResourceId1")).Return(nil) 217 | }, 218 | want: nil, 219 | wantErr: false, 220 | }, 221 | { 222 | name: "delete resources failure", 223 | args: args{ 224 | ctx: context.Background(), 225 | }, 226 | prepareMockFn: func(m *client.MockIBackup) { 227 | m.EXPECT().CheckBackupVaultExists(gomock.Any(), aws.String("PhysicalResourceId1")).Return(false, fmt.Errorf("ListBackupVaultsError")) 228 | }, 229 | want: fmt.Errorf("ListBackupVaultsError"), 230 | wantErr: true, 231 | }, 232 | } 233 | 234 | for _, tt := range cases { 235 | t.Run(tt.name, func(t *testing.T) { 236 | ctrl := gomock.NewController(t) 237 | backupMock := client.NewMockIBackup(ctrl) 238 | tt.prepareMockFn(backupMock) 239 | 240 | backupOperator := NewBackupVaultOperator(backupMock) 241 | 242 | backupOperator.AddResource(&cfnTypes.StackResourceSummary{ 243 | LogicalResourceId: aws.String("LogicalResourceId1"), 244 | ResourceStatus: "DELETE_FAILED", 245 | ResourceType: aws.String("AWS::Backup::BackupVault"), 246 | PhysicalResourceId: aws.String("PhysicalResourceId1"), 247 | }) 248 | 249 | err := backupOperator.DeleteResources(tt.args.ctx) 250 | if (err != nil) != tt.wantErr { 251 | t.Errorf("error = %#v, wantErr %#v", err.Error(), tt.wantErr) 252 | return 253 | } 254 | if tt.wantErr && err.Error() != tt.want.Error() { 255 | t.Errorf("err = %#v, want %#v", err.Error(), tt.want.Error()) 256 | return 257 | } 258 | }) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /internal/operation/custom.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 7 | ) 8 | 9 | var _ IOperator = (*CustomOperator)(nil) 10 | 11 | type CustomOperator struct { 12 | resources []*types.StackResourceSummary 13 | } 14 | 15 | func NewCustomOperator() *CustomOperator { 16 | return &CustomOperator{ 17 | resources: []*types.StackResourceSummary{}, 18 | } 19 | } 20 | 21 | func (o *CustomOperator) AddResource(resource *types.StackResourceSummary) { 22 | o.resources = append(o.resources, resource) 23 | } 24 | 25 | func (o *CustomOperator) GetResourcesLength() int { 26 | return len(o.resources) 27 | } 28 | 29 | // Implicit implements (these resources will be deleted on its own) 30 | func (o *CustomOperator) DeleteResources(ctx context.Context) error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/operation/ecr_repository.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | "github.com/go-to-k/delstack/pkg/client" 9 | "golang.org/x/sync/errgroup" 10 | "golang.org/x/sync/semaphore" 11 | ) 12 | 13 | var _ IOperator = (*EcrRepositoryOperator)(nil) 14 | 15 | type EcrRepositoryOperator struct { 16 | client client.IEcr 17 | resources []*types.StackResourceSummary 18 | } 19 | 20 | func NewEcrRepositoryOperator(client client.IEcr) *EcrRepositoryOperator { 21 | return &EcrRepositoryOperator{ 22 | client: client, 23 | resources: []*types.StackResourceSummary{}, 24 | } 25 | } 26 | 27 | func (o *EcrRepositoryOperator) AddResource(resource *types.StackResourceSummary) { 28 | o.resources = append(o.resources, resource) 29 | } 30 | 31 | func (o *EcrRepositoryOperator) GetResourcesLength() int { 32 | return len(o.resources) 33 | } 34 | 35 | func (o *EcrRepositoryOperator) DeleteResources(ctx context.Context) error { 36 | eg, ctx := errgroup.WithContext(ctx) 37 | sem := semaphore.NewWeighted(int64(runtime.NumCPU())) 38 | 39 | for _, repository := range o.resources { 40 | repository := repository 41 | if err := sem.Acquire(ctx, 1); err != nil { 42 | return err 43 | } 44 | eg.Go(func() (err error) { 45 | defer sem.Release(1) 46 | 47 | return o.DeleteEcrRepository(ctx, repository.PhysicalResourceId) 48 | }) 49 | } 50 | 51 | err := eg.Wait() 52 | return err 53 | } 54 | 55 | func (o *EcrRepositoryOperator) DeleteEcrRepository(ctx context.Context, repositoryName *string) error { 56 | exists, err := o.client.CheckEcrExists(ctx, repositoryName) 57 | if err != nil { 58 | return err 59 | } 60 | if !exists { 61 | return nil 62 | } 63 | 64 | return o.client.DeleteRepository(ctx, repositoryName) 65 | } 66 | -------------------------------------------------------------------------------- /internal/operation/ecr_repository_test.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | cfnTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 10 | "github.com/go-to-k/delstack/internal/io" 11 | "github.com/go-to-k/delstack/pkg/client" 12 | gomock "go.uber.org/mock/gomock" 13 | ) 14 | 15 | /* 16 | Test Cases 17 | */ 18 | 19 | func TestEcrRepositoryOperator_DeleteEcrRepository(t *testing.T) { 20 | io.NewLogger(false) 21 | 22 | type args struct { 23 | ctx context.Context 24 | repositoryName *string 25 | } 26 | 27 | cases := []struct { 28 | name string 29 | args args 30 | prepareMockFn func(m *client.MockIEcr) 31 | want error 32 | wantErr bool 33 | }{ 34 | { 35 | name: "delete ecr repository successfully", 36 | args: args{ 37 | ctx: context.Background(), 38 | repositoryName: aws.String("test"), 39 | }, 40 | prepareMockFn: func(m *client.MockIEcr) { 41 | m.EXPECT().CheckEcrExists(gomock.Any(), aws.String("test")).Return(true, nil) 42 | m.EXPECT().DeleteRepository(gomock.Any(), aws.String("test")).Return(nil) 43 | }, 44 | want: nil, 45 | wantErr: false, 46 | }, 47 | { 48 | name: "delete ecr repository failure", 49 | args: args{ 50 | ctx: context.Background(), 51 | repositoryName: aws.String("test"), 52 | }, 53 | prepareMockFn: func(m *client.MockIEcr) { 54 | m.EXPECT().CheckEcrExists(gomock.Any(), aws.String("test")).Return(true, nil) 55 | m.EXPECT().DeleteRepository(gomock.Any(), aws.String("test")).Return(fmt.Errorf("DeleteRepositoryError")) 56 | }, 57 | want: fmt.Errorf("DeleteRepositoryError"), 58 | wantErr: true, 59 | }, 60 | { 61 | name: "delete ecr repository failure for check ecr repository exists errors", 62 | args: args{ 63 | ctx: context.Background(), 64 | repositoryName: aws.String("test"), 65 | }, 66 | prepareMockFn: func(m *client.MockIEcr) { 67 | m.EXPECT().CheckEcrExists(gomock.Any(), aws.String("test")).Return(false, fmt.Errorf("DescribeRepositoriesError")) 68 | }, 69 | want: fmt.Errorf("DescribeRepositoriesError"), 70 | wantErr: true, 71 | }, 72 | { 73 | name: "delete ecr repository successfully for ecr repository not exists", 74 | args: args{ 75 | ctx: context.Background(), 76 | repositoryName: aws.String("test"), 77 | }, 78 | prepareMockFn: func(m *client.MockIEcr) { 79 | m.EXPECT().CheckEcrExists(gomock.Any(), aws.String("test")).Return(false, nil) 80 | }, 81 | want: nil, 82 | wantErr: false, 83 | }, 84 | } 85 | 86 | for _, tt := range cases { 87 | t.Run(tt.name, func(t *testing.T) { 88 | ctrl := gomock.NewController(t) 89 | ecrMock := client.NewMockIEcr(ctrl) 90 | tt.prepareMockFn(ecrMock) 91 | 92 | ecrRepositoryOperator := NewEcrRepositoryOperator(ecrMock) 93 | 94 | err := ecrRepositoryOperator.DeleteEcrRepository(tt.args.ctx, tt.args.repositoryName) 95 | if (err != nil) != tt.wantErr { 96 | t.Errorf("error = %#v, wantErr %#v", err.Error(), tt.wantErr) 97 | return 98 | } 99 | if tt.wantErr && err.Error() != tt.want.Error() { 100 | t.Errorf("err = %#v, want %#v", err.Error(), tt.want.Error()) 101 | return 102 | } 103 | }) 104 | } 105 | } 106 | 107 | func TestEcrRepositoryOperator_DeleteResourcesForEcrRepository(t *testing.T) { 108 | io.NewLogger(false) 109 | 110 | type args struct { 111 | ctx context.Context 112 | } 113 | 114 | cases := []struct { 115 | name string 116 | args args 117 | prepareMockFn func(m *client.MockIEcr) 118 | want error 119 | wantErr bool 120 | }{ 121 | { 122 | name: "delete resources successfully", 123 | args: args{ 124 | ctx: context.Background(), 125 | }, 126 | prepareMockFn: func(m *client.MockIEcr) { 127 | m.EXPECT().CheckEcrExists(gomock.Any(), aws.String("PhysicalResourceId1")).Return(true, nil) 128 | m.EXPECT().DeleteRepository(gomock.Any(), aws.String("PhysicalResourceId1")).Return(nil) 129 | }, 130 | want: nil, 131 | wantErr: false, 132 | }, 133 | { 134 | name: "delete resources failure", 135 | args: args{ 136 | ctx: context.Background(), 137 | }, 138 | prepareMockFn: func(m *client.MockIEcr) { 139 | m.EXPECT().CheckEcrExists(gomock.Any(), aws.String("PhysicalResourceId1")).Return(false, fmt.Errorf("DescribeRepositoriesError")) 140 | }, 141 | want: fmt.Errorf("DescribeRepositoriesError"), 142 | wantErr: true, 143 | }, 144 | } 145 | 146 | for _, tt := range cases { 147 | t.Run(tt.name, func(t *testing.T) { 148 | ctrl := gomock.NewController(t) 149 | ecrMock := client.NewMockIEcr(ctrl) 150 | tt.prepareMockFn(ecrMock) 151 | 152 | ecrRepositoryOperator := NewEcrRepositoryOperator(ecrMock) 153 | ecrRepositoryOperator.AddResource(&cfnTypes.StackResourceSummary{ 154 | LogicalResourceId: aws.String("LogicalResourceId1"), 155 | ResourceStatus: "DELETE_FAILED", 156 | ResourceType: aws.String("AWS::ECR::Repository"), 157 | PhysicalResourceId: aws.String("PhysicalResourceId1"), 158 | }) 159 | 160 | err := ecrRepositoryOperator.DeleteResources(tt.args.ctx) 161 | if (err != nil) != tt.wantErr { 162 | t.Errorf("error = %#v, wantErr %#v", err.Error(), tt.wantErr) 163 | return 164 | } 165 | if tt.wantErr && err.Error() != tt.want.Error() { 166 | t.Errorf("err = %#v, want %#v", err.Error(), tt.want.Error()) 167 | return 168 | } 169 | }) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /internal/operation/iam_group.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | "github.com/go-to-k/delstack/pkg/client" 9 | "golang.org/x/sync/errgroup" 10 | "golang.org/x/sync/semaphore" 11 | ) 12 | 13 | var _ IOperator = (*IamGroupOperator)(nil) 14 | 15 | type IamGroupOperator struct { 16 | client client.IIam 17 | resources []*types.StackResourceSummary 18 | } 19 | 20 | func NewIamGroupOperator(client client.IIam) *IamGroupOperator { 21 | return &IamGroupOperator{ 22 | client: client, 23 | resources: []*types.StackResourceSummary{}, 24 | } 25 | } 26 | 27 | func (o *IamGroupOperator) AddResource(resource *types.StackResourceSummary) { 28 | o.resources = append(o.resources, resource) 29 | } 30 | 31 | func (o *IamGroupOperator) GetResourcesLength() int { 32 | return len(o.resources) 33 | } 34 | 35 | func (o *IamGroupOperator) DeleteResources(ctx context.Context) error { 36 | eg, ctx := errgroup.WithContext(ctx) 37 | sem := semaphore.NewWeighted(int64(runtime.NumCPU())) 38 | 39 | for _, Group := range o.resources { 40 | Group := Group 41 | if err := sem.Acquire(ctx, 1); err != nil { 42 | return err 43 | } 44 | eg.Go(func() error { 45 | defer sem.Release(1) 46 | 47 | return o.DeleteIamGroup(ctx, Group.PhysicalResourceId) 48 | }) 49 | } 50 | 51 | return eg.Wait() 52 | } 53 | 54 | func (o *IamGroupOperator) DeleteIamGroup(ctx context.Context, groupName *string) error { 55 | exists, err := o.client.CheckGroupExists(ctx, groupName) 56 | if err != nil { 57 | return err 58 | } 59 | if !exists { 60 | return nil 61 | } 62 | 63 | users, err := o.client.GetGroupUsers(ctx, groupName) 64 | if err != nil { 65 | return err 66 | } 67 | if len(users) > 0 { 68 | if err := o.client.RemoveUsersFromGroup(ctx, groupName, users); err != nil { 69 | return err 70 | } 71 | } 72 | 73 | if err := o.client.DeleteGroup(ctx, groupName); err != nil { 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/operation/iam_group_test.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | cfnTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 10 | "github.com/aws/aws-sdk-go-v2/service/iam/types" 11 | "github.com/go-to-k/delstack/internal/io" 12 | "github.com/go-to-k/delstack/pkg/client" 13 | gomock "go.uber.org/mock/gomock" 14 | ) 15 | 16 | /* 17 | Test Cases 18 | */ 19 | 20 | func TestIamGroupOperator_DeleteIamGroup(t *testing.T) { 21 | io.NewLogger(false) 22 | 23 | type args struct { 24 | ctx context.Context 25 | groupName *string 26 | } 27 | 28 | cases := []struct { 29 | name string 30 | args args 31 | prepareMockFn func(m *client.MockIIam) 32 | want error 33 | wantErr bool 34 | }{ 35 | { 36 | name: "delete group successfully", 37 | args: args{ 38 | ctx: context.Background(), 39 | groupName: aws.String("test"), 40 | }, 41 | prepareMockFn: func(m *client.MockIIam) { 42 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("test")).Return(true, nil) 43 | m.EXPECT().GetGroupUsers(gomock.Any(), aws.String("test")).Return( 44 | []types.User{ 45 | { 46 | UserName: aws.String("UserName1"), 47 | }, 48 | { 49 | UserName: aws.String("UserName2"), 50 | }, 51 | }, nil) 52 | m.EXPECT().RemoveUsersFromGroup(gomock.Any(), aws.String("test"), gomock.Any()).Return(nil) 53 | m.EXPECT().DeleteGroup(gomock.Any(), aws.String("test")).Return(nil) 54 | }, 55 | want: nil, 56 | wantErr: false, 57 | }, 58 | { 59 | name: "delete group failure for CheckGroupExists errors", 60 | args: args{ 61 | ctx: context.Background(), 62 | groupName: aws.String("test"), 63 | }, 64 | prepareMockFn: func(m *client.MockIIam) { 65 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("test")).Return(false, fmt.Errorf("GetGroupError")) 66 | }, 67 | want: fmt.Errorf("GetGroupError"), 68 | wantErr: true, 69 | }, 70 | { 71 | name: "delete group failure for CheckGroupExists (not exists)", 72 | args: args{ 73 | ctx: context.Background(), 74 | groupName: aws.String("test"), 75 | }, 76 | prepareMockFn: func(m *client.MockIIam) { 77 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("test")).Return(false, nil) 78 | }, 79 | want: nil, 80 | wantErr: false, 81 | }, 82 | { 83 | name: "delete group failure for GetGroupUsers errors", 84 | args: args{ 85 | ctx: context.Background(), 86 | groupName: aws.String("test"), 87 | }, 88 | prepareMockFn: func(m *client.MockIIam) { 89 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("test")).Return(true, nil) 90 | m.EXPECT().GetGroupUsers(gomock.Any(), aws.String("test")).Return(nil, fmt.Errorf("GetGroupUsersError")) 91 | }, 92 | want: fmt.Errorf("GetGroupUsersError"), 93 | wantErr: true, 94 | }, 95 | { 96 | name: "delete group failure for RemoveUsersFromGroup errors", 97 | args: args{ 98 | ctx: context.Background(), 99 | groupName: aws.String("test"), 100 | }, 101 | prepareMockFn: func(m *client.MockIIam) { 102 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("test")).Return(true, nil) 103 | m.EXPECT().GetGroupUsers(gomock.Any(), aws.String("test")).Return( 104 | []types.User{ 105 | { 106 | UserName: aws.String("UserName1"), 107 | }, 108 | { 109 | UserName: aws.String("UserName2"), 110 | }, 111 | }, nil) 112 | m.EXPECT().RemoveUsersFromGroup(gomock.Any(), aws.String("test"), gomock.Any()).Return(fmt.Errorf("RemoveUsersFromGroupError")) 113 | }, 114 | want: fmt.Errorf("RemoveUsersFromGroupError"), 115 | wantErr: true, 116 | }, 117 | { 118 | name: "delete group successfully for GetGroupUsers with zero length", 119 | args: args{ 120 | ctx: context.Background(), 121 | groupName: aws.String("test"), 122 | }, 123 | prepareMockFn: func(m *client.MockIIam) { 124 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("test")).Return(true, nil) 125 | m.EXPECT().GetGroupUsers(gomock.Any(), aws.String("test")).Return([]types.User{}, nil) 126 | m.EXPECT().DeleteGroup(gomock.Any(), aws.String("test")).Return(nil) 127 | }, 128 | want: nil, 129 | wantErr: false, 130 | }, 131 | { 132 | name: "delete group failure for DeleteGroup errors", 133 | args: args{ 134 | ctx: context.Background(), 135 | groupName: aws.String("test"), 136 | }, 137 | prepareMockFn: func(m *client.MockIIam) { 138 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("test")).Return(true, nil) 139 | m.EXPECT().GetGroupUsers(gomock.Any(), aws.String("test")).Return( 140 | []types.User{ 141 | { 142 | UserName: aws.String("UserName1"), 143 | }, 144 | { 145 | UserName: aws.String("UserName2"), 146 | }, 147 | }, nil) 148 | m.EXPECT().RemoveUsersFromGroup(gomock.Any(), aws.String("test"), gomock.Any()).Return(nil) 149 | m.EXPECT().DeleteGroup(gomock.Any(), aws.String("test")).Return(fmt.Errorf("DeleteGroupError")) 150 | }, 151 | want: fmt.Errorf("DeleteGroupError"), 152 | wantErr: true, 153 | }, 154 | } 155 | 156 | for _, tt := range cases { 157 | t.Run(tt.name, func(t *testing.T) { 158 | ctrl := gomock.NewController(t) 159 | iamMock := client.NewMockIIam(ctrl) 160 | tt.prepareMockFn(iamMock) 161 | 162 | iamGroupOperator := NewIamGroupOperator(iamMock) 163 | 164 | err := iamGroupOperator.DeleteIamGroup(tt.args.ctx, tt.args.groupName) 165 | if (err != nil) != tt.wantErr { 166 | t.Errorf("error = %#v, wantErr %#v", err.Error(), tt.wantErr) 167 | return 168 | } 169 | if tt.wantErr && err.Error() != tt.want.Error() { 170 | t.Errorf("err = %#v, want %#v", err.Error(), tt.want.Error()) 171 | return 172 | } 173 | }) 174 | } 175 | } 176 | 177 | func TestIamGroupOperator_DeleteResourcesForIamGroup(t *testing.T) { 178 | io.NewLogger(false) 179 | 180 | type args struct { 181 | ctx context.Context 182 | } 183 | 184 | cases := []struct { 185 | name string 186 | args args 187 | prepareMockFn func(m *client.MockIIam) 188 | want error 189 | wantErr bool 190 | }{ 191 | { 192 | name: "delete resources successfully", 193 | args: args{ 194 | ctx: context.Background(), 195 | }, 196 | prepareMockFn: func(m *client.MockIIam) { 197 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("PhysicalResourceId1")).Return(true, nil) 198 | m.EXPECT().GetGroupUsers(gomock.Any(), aws.String("PhysicalResourceId1")).Return( 199 | []types.User{ 200 | { 201 | UserName: aws.String("UserName1"), 202 | }, 203 | { 204 | UserName: aws.String("UserName2"), 205 | }, 206 | }, nil) 207 | m.EXPECT().RemoveUsersFromGroup(gomock.Any(), aws.String("PhysicalResourceId1"), gomock.Any()).Return(nil) 208 | m.EXPECT().DeleteGroup(gomock.Any(), aws.String("PhysicalResourceId1")).Return(nil) 209 | }, 210 | want: nil, 211 | wantErr: false, 212 | }, 213 | { 214 | name: "delete resources failure", 215 | args: args{ 216 | ctx: context.Background(), 217 | }, 218 | prepareMockFn: func(m *client.MockIIam) { 219 | m.EXPECT().CheckGroupExists(gomock.Any(), aws.String("PhysicalResourceId1")).Return(false, fmt.Errorf("GetGroupError")) 220 | }, 221 | want: fmt.Errorf("GetGroupError"), 222 | wantErr: true, 223 | }, 224 | } 225 | 226 | for _, tt := range cases { 227 | t.Run(tt.name, func(t *testing.T) { 228 | ctrl := gomock.NewController(t) 229 | iamMock := client.NewMockIIam(ctrl) 230 | tt.prepareMockFn(iamMock) 231 | 232 | iamGroupOperator := NewIamGroupOperator(iamMock) 233 | iamGroupOperator.AddResource(&cfnTypes.StackResourceSummary{ 234 | LogicalResourceId: aws.String("LogicalResourceId1"), 235 | ResourceStatus: "DELETE_FAILED", 236 | ResourceType: aws.String("AWS::IAM::Group"), 237 | PhysicalResourceId: aws.String("PhysicalResourceId1"), 238 | }) 239 | 240 | err := iamGroupOperator.DeleteResources(tt.args.ctx) 241 | if (err != nil) != tt.wantErr { 242 | t.Errorf("error = %#v, wantErr %#v", err.Error(), tt.wantErr) 243 | return 244 | } 245 | if tt.wantErr && err.Error() != tt.want.Error() { 246 | t.Errorf("err = %#v, want %#v", err.Error(), tt.want.Error()) 247 | return 248 | } 249 | }) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /internal/operation/main_test.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.uber.org/goleak" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | fmt.Println() 12 | fmt.Println("==========================================") 13 | fmt.Println("========== Start Test: operation =========") 14 | fmt.Println("==========================================") 15 | goleak.VerifyTestMain(m) 16 | } 17 | -------------------------------------------------------------------------------- /internal/operation/operator.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=operator_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package operation 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | ) 9 | 10 | type IOperator interface { 11 | AddResource(resource *types.StackResourceSummary) 12 | GetResourcesLength() int 13 | DeleteResources(ctx context.Context) error 14 | } 15 | -------------------------------------------------------------------------------- /internal/operation/operator_collection.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=operator_collection_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package operation 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 10 | "github.com/go-to-k/delstack/internal/io" 11 | "github.com/go-to-k/delstack/internal/resourcetype" 12 | ) 13 | 14 | type IOperatorCollection interface { 15 | SetOperatorCollection(stackName *string, stackResourceSummaries []types.StackResourceSummary) 16 | GetLogicalResourceIds() []string 17 | GetOperators() []IOperator 18 | RaiseUnsupportedResourceError() error 19 | } 20 | 21 | var _ IOperatorCollection = (*OperatorCollection)(nil) 22 | 23 | type OperatorCollection struct { 24 | stackName string 25 | operatorFactory *OperatorFactory 26 | logicalResourceIds []string 27 | unsupportedStackResources []types.StackResourceSummary 28 | operators []IOperator 29 | targetResourceTypes []string 30 | } 31 | 32 | func NewOperatorCollection(config aws.Config, operatorFactory *OperatorFactory, targetResourceTypes []string) *OperatorCollection { 33 | return &OperatorCollection{ 34 | operatorFactory: operatorFactory, 35 | targetResourceTypes: targetResourceTypes, 36 | } 37 | } 38 | 39 | func (c *OperatorCollection) SetOperatorCollection(stackName *string, stackResourceSummaries []types.StackResourceSummary) { 40 | c.stackName = aws.ToString(stackName) 41 | 42 | s3BucketOperator := c.operatorFactory.CreateS3BucketOperator() 43 | s3DirectoryBucketOperator := c.operatorFactory.CreateS3DirectoryBucketOperator() 44 | s3TableBucketOperator := c.operatorFactory.CreateS3TableBucketOperator() 45 | iamGroupOperator := c.operatorFactory.CreateIamGroupOperator() 46 | ecrRepositoryOperator := c.operatorFactory.CreateEcrRepositoryOperator() 47 | backupVaultOperator := c.operatorFactory.CreateBackupVaultOperator() 48 | cloudformationStackOperator := c.operatorFactory.CreateCloudFormationStackOperator(c.targetResourceTypes) 49 | customOperator := c.operatorFactory.CreateCustomOperator() 50 | 51 | for _, v := range stackResourceSummaries { 52 | if v.ResourceStatus == "DELETE_FAILED" { 53 | stackResource := v // Copy for pointer used below 54 | c.logicalResourceIds = append(c.logicalResourceIds, aws.ToString(stackResource.LogicalResourceId)) 55 | 56 | if !c.containsResourceType(*stackResource.ResourceType) { 57 | c.unsupportedStackResources = append(c.unsupportedStackResources, stackResource) 58 | } else { 59 | switch *stackResource.ResourceType { 60 | case resourcetype.S3Bucket: 61 | s3BucketOperator.AddResource(&stackResource) 62 | case resourcetype.S3DirectoryBucket: 63 | s3DirectoryBucketOperator.AddResource(&stackResource) 64 | case resourcetype.S3TableBucket: 65 | s3TableBucketOperator.AddResource(&stackResource) 66 | case resourcetype.IamGroup: 67 | iamGroupOperator.AddResource(&stackResource) 68 | case resourcetype.EcrRepository: 69 | ecrRepositoryOperator.AddResource(&stackResource) 70 | case resourcetype.BackupVault: 71 | backupVaultOperator.AddResource(&stackResource) 72 | case resourcetype.CloudformationStack: 73 | cloudformationStackOperator.AddResource(&stackResource) 74 | default: 75 | if strings.Contains(*stackResource.ResourceType, resourcetype.CustomResource) { 76 | customOperator.AddResource(&stackResource) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | c.operators = append(c.operators, s3BucketOperator) 84 | c.operators = append(c.operators, s3DirectoryBucketOperator) 85 | c.operators = append(c.operators, s3TableBucketOperator) 86 | c.operators = append(c.operators, iamGroupOperator) 87 | c.operators = append(c.operators, ecrRepositoryOperator) 88 | c.operators = append(c.operators, backupVaultOperator) 89 | c.operators = append(c.operators, cloudformationStackOperator) 90 | c.operators = append(c.operators, customOperator) 91 | } 92 | 93 | func (c *OperatorCollection) containsResourceType(resource string) bool { 94 | for _, t := range c.targetResourceTypes { 95 | if t == resource || (t == resourcetype.CustomResource && strings.Contains(resource, resourcetype.CustomResource)) { 96 | return true 97 | } 98 | } 99 | return false 100 | } 101 | 102 | func (c *OperatorCollection) GetLogicalResourceIds() []string { 103 | return c.logicalResourceIds 104 | } 105 | 106 | func (c *OperatorCollection) GetOperators() []IOperator { 107 | return c.operators 108 | } 109 | 110 | func (c *OperatorCollection) RaiseUnsupportedResourceError() error { 111 | title := fmt.Sprintf("%v deletion is FAILED !!!\n", c.stackName) 112 | 113 | unsupportedStackResourcesHeader := []string{"ResourceType", "Resource"} 114 | unsupportedStackResourcesData := [][]string{} 115 | 116 | for _, resource := range c.unsupportedStackResources { 117 | unsupportedStackResourcesData = append(unsupportedStackResourcesData, []string{*resource.ResourceType, *resource.LogicalResourceId}) 118 | } 119 | unsupportedStackResources := "\nThese are the resources unsupported (or you did not selected in the interactive prompt), so failed delete:\n" + *io.ToStringAsTableFormat(unsupportedStackResourcesHeader, unsupportedStackResourcesData) 120 | 121 | supportedStackResourcesHeader := []string{"ResourceType", "Description"} 122 | supportedStackResourcesData := [][]string{ 123 | {resourcetype.S3Bucket, "S3 Buckets, including buckets with Non-empty or Versioning enabled and DeletionPolicy not Retain."}, 124 | {resourcetype.S3DirectoryBucket, "S3 Directory Buckets for S3 Express One Zone, including buckets with Non-empty and DeletionPolicy not Retain."}, 125 | {resourcetype.S3TableBucket, "S3 Table Buckets, including buckets with any namespaces or tables and DeletionPolicy not Retain."}, 126 | {resourcetype.IamGroup, "IAM Groups, including groups with IAM users from outside the stack."}, 127 | {resourcetype.EcrRepository, "ECR Repositories, including repositories that contain images and where the `EmptyOnDelete` is not true."}, 128 | {resourcetype.BackupVault, "Backup Vaults, including vaults containing recovery points."}, 129 | {resourcetype.CloudformationStack, "Nested Child Stacks that failed to delete."}, 130 | {"Custom::Xxx", "Custom Resources, including resources that do not return a SUCCESS status."}, 131 | } 132 | supportedStackResources := "\nSupported resources for force deletion of DELETE_FAILED resources are followings.\n" + *io.ToStringAsTableFormat(supportedStackResourcesHeader, supportedStackResourcesData) 133 | 134 | issueLink := "\nIf you want to delete the unsupported resources, please create an issue at GitHub(https://github.com/go-to-k/delstack/issues).\n" 135 | 136 | unsupportedResourceError := title + unsupportedStackResources + supportedStackResources + issueLink 137 | 138 | return fmt.Errorf("UnsupportedResourceError: %v", unsupportedResourceError) 139 | } 140 | -------------------------------------------------------------------------------- /internal/operation/operator_collection_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: operator_collection.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=operator_collection.go -destination=operator_collection_mock.go -package=operation -write_package_comment=false 7 | package operation 8 | 9 | import ( 10 | reflect "reflect" 11 | 12 | types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 13 | gomock "go.uber.org/mock/gomock" 14 | ) 15 | 16 | // MockIOperatorCollection is a mock of IOperatorCollection interface. 17 | type MockIOperatorCollection struct { 18 | ctrl *gomock.Controller 19 | recorder *MockIOperatorCollectionMockRecorder 20 | } 21 | 22 | // MockIOperatorCollectionMockRecorder is the mock recorder for MockIOperatorCollection. 23 | type MockIOperatorCollectionMockRecorder struct { 24 | mock *MockIOperatorCollection 25 | } 26 | 27 | // NewMockIOperatorCollection creates a new mock instance. 28 | func NewMockIOperatorCollection(ctrl *gomock.Controller) *MockIOperatorCollection { 29 | mock := &MockIOperatorCollection{ctrl: ctrl} 30 | mock.recorder = &MockIOperatorCollectionMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockIOperatorCollection) EXPECT() *MockIOperatorCollectionMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // GetLogicalResourceIds mocks base method. 40 | func (m *MockIOperatorCollection) GetLogicalResourceIds() []string { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "GetLogicalResourceIds") 43 | ret0, _ := ret[0].([]string) 44 | return ret0 45 | } 46 | 47 | // GetLogicalResourceIds indicates an expected call of GetLogicalResourceIds. 48 | func (mr *MockIOperatorCollectionMockRecorder) GetLogicalResourceIds() *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogicalResourceIds", reflect.TypeOf((*MockIOperatorCollection)(nil).GetLogicalResourceIds)) 51 | } 52 | 53 | // GetOperators mocks base method. 54 | func (m *MockIOperatorCollection) GetOperators() []IOperator { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "GetOperators") 57 | ret0, _ := ret[0].([]IOperator) 58 | return ret0 59 | } 60 | 61 | // GetOperators indicates an expected call of GetOperators. 62 | func (mr *MockIOperatorCollectionMockRecorder) GetOperators() *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOperators", reflect.TypeOf((*MockIOperatorCollection)(nil).GetOperators)) 65 | } 66 | 67 | // RaiseUnsupportedResourceError mocks base method. 68 | func (m *MockIOperatorCollection) RaiseUnsupportedResourceError() error { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "RaiseUnsupportedResourceError") 71 | ret0, _ := ret[0].(error) 72 | return ret0 73 | } 74 | 75 | // RaiseUnsupportedResourceError indicates an expected call of RaiseUnsupportedResourceError. 76 | func (mr *MockIOperatorCollectionMockRecorder) RaiseUnsupportedResourceError() *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RaiseUnsupportedResourceError", reflect.TypeOf((*MockIOperatorCollection)(nil).RaiseUnsupportedResourceError)) 79 | } 80 | 81 | // SetOperatorCollection mocks base method. 82 | func (m *MockIOperatorCollection) SetOperatorCollection(stackName *string, stackResourceSummaries []types.StackResourceSummary) { 83 | m.ctrl.T.Helper() 84 | m.ctrl.Call(m, "SetOperatorCollection", stackName, stackResourceSummaries) 85 | } 86 | 87 | // SetOperatorCollection indicates an expected call of SetOperatorCollection. 88 | func (mr *MockIOperatorCollectionMockRecorder) SetOperatorCollection(stackName, stackResourceSummaries any) *gomock.Call { 89 | mr.mock.ctrl.T.Helper() 90 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOperatorCollection", reflect.TypeOf((*MockIOperatorCollection)(nil).SetOperatorCollection), stackName, stackResourceSummaries) 91 | } 92 | -------------------------------------------------------------------------------- /internal/operation/operator_factory.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | "github.com/aws/aws-sdk-go-v2/service/backup" 6 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 7 | "github.com/aws/aws-sdk-go-v2/service/ecr" 8 | "github.com/aws/aws-sdk-go-v2/service/iam" 9 | "github.com/aws/aws-sdk-go-v2/service/s3" 10 | "github.com/aws/aws-sdk-go-v2/service/s3tables" 11 | "github.com/go-to-k/delstack/pkg/client" 12 | ) 13 | 14 | const SDKRetryMaxAttempts = 3 15 | 16 | type OperatorFactory struct { 17 | config aws.Config 18 | } 19 | 20 | func NewOperatorFactory(config aws.Config) *OperatorFactory { 21 | return &OperatorFactory{ 22 | config, 23 | } 24 | } 25 | 26 | func (f *OperatorFactory) CreateCloudFormationStackOperator(targetResourceTypes []string) *CloudFormationStackOperator { 27 | sdkCfnClient := cloudformation.NewFromConfig(f.config, func(o *cloudformation.Options) { 28 | o.RetryMaxAttempts = SDKRetryMaxAttempts 29 | o.RetryMode = aws.RetryModeStandard 30 | }) 31 | sdkCfnDeleteWaiter := cloudformation.NewStackDeleteCompleteWaiter(sdkCfnClient) 32 | sdkCfnUpdateWaiter := cloudformation.NewStackUpdateCompleteWaiter(sdkCfnClient) 33 | return NewCloudFormationStackOperator( 34 | f.config, 35 | client.NewCloudFormation( 36 | sdkCfnClient, 37 | sdkCfnDeleteWaiter, 38 | sdkCfnUpdateWaiter, 39 | ), 40 | targetResourceTypes, 41 | ) 42 | } 43 | 44 | func (f *OperatorFactory) CreateBackupVaultOperator() *BackupVaultOperator { 45 | sdkBackupClient := backup.NewFromConfig(f.config, func(o *backup.Options) { 46 | o.RetryMaxAttempts = SDKRetryMaxAttempts 47 | o.RetryMode = aws.RetryModeStandard 48 | }) 49 | 50 | return NewBackupVaultOperator( 51 | client.NewBackup( 52 | sdkBackupClient, 53 | ), 54 | ) 55 | } 56 | 57 | func (f *OperatorFactory) CreateEcrRepositoryOperator() *EcrRepositoryOperator { 58 | sdkEcrClient := ecr.NewFromConfig(f.config, func(o *ecr.Options) { 59 | o.RetryMaxAttempts = SDKRetryMaxAttempts 60 | o.RetryMode = aws.RetryModeStandard 61 | }) 62 | 63 | return NewEcrRepositoryOperator( 64 | client.NewEcr( 65 | sdkEcrClient, 66 | ), 67 | ) 68 | } 69 | 70 | func (f *OperatorFactory) CreateIamGroupOperator() *IamGroupOperator { 71 | sdkIamClient := iam.NewFromConfig(f.config, func(o *iam.Options) { 72 | o.RetryMaxAttempts = SDKRetryMaxAttempts 73 | o.RetryMode = aws.RetryModeStandard 74 | }) 75 | 76 | return NewIamGroupOperator( 77 | client.NewIam( 78 | sdkIamClient, 79 | ), 80 | ) 81 | } 82 | 83 | func (f *OperatorFactory) CreateS3BucketOperator() *S3BucketOperator { 84 | sdkS3Client := s3.NewFromConfig(f.config, func(o *s3.Options) { 85 | o.RetryMaxAttempts = SDKRetryMaxAttempts 86 | o.RetryMode = aws.RetryModeStandard 87 | }) 88 | 89 | return NewS3BucketOperator( 90 | client.NewS3( 91 | sdkS3Client, 92 | false, 93 | ), 94 | ) 95 | } 96 | 97 | func (f *OperatorFactory) CreateS3DirectoryBucketOperator() *S3BucketOperator { 98 | sdkS3Client := s3.NewFromConfig(f.config, func(o *s3.Options) { 99 | o.RetryMaxAttempts = SDKRetryMaxAttempts 100 | o.RetryMode = aws.RetryModeStandard 101 | }) 102 | 103 | // Basically, a separate operator should be defined for each resource type, 104 | // but the S3DirectoryBucket uses the same operator as the S3BucketOperator 105 | // since the process is almost the same. 106 | operator := NewS3BucketOperator( 107 | client.NewS3( 108 | sdkS3Client, 109 | true, 110 | ), 111 | ) 112 | 113 | return operator 114 | } 115 | 116 | func (f *OperatorFactory) CreateS3TableBucketOperator() *S3TableBucketOperator { 117 | sdkS3TablesClient := s3tables.NewFromConfig(f.config, func(o *s3tables.Options) { 118 | o.RetryMaxAttempts = SDKRetryMaxAttempts 119 | o.RetryMode = aws.RetryModeStandard 120 | }) 121 | 122 | return NewS3TableBucketOperator( 123 | client.NewS3Tables( 124 | sdkS3TablesClient, 125 | ), 126 | ) 127 | } 128 | 129 | func (f *OperatorFactory) CreateCustomOperator() *CustomOperator { 130 | return NewCustomOperator() // Implicit instances that do not actually delete resources 131 | } 132 | -------------------------------------------------------------------------------- /internal/operation/operator_manager.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=operator_manager_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package operation 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | "golang.org/x/sync/errgroup" 9 | ) 10 | 11 | type IOperatorManager interface { 12 | SetOperatorCollection(stackName *string, stackResourceSummaries []types.StackResourceSummary) 13 | CheckResourceCounts() error 14 | GetLogicalResourceIds() []string 15 | DeleteResourceCollection(ctx context.Context) error 16 | } 17 | 18 | var _ IOperatorManager = (*OperatorManager)(nil) 19 | 20 | type OperatorManager struct { 21 | operatorCollection IOperatorCollection 22 | } 23 | 24 | func NewOperatorManager(operatorCollection IOperatorCollection) *OperatorManager { 25 | return &OperatorManager{ 26 | operatorCollection: operatorCollection, 27 | } 28 | } 29 | 30 | func (m *OperatorManager) SetOperatorCollection(stackName *string, stackResourceSummaries []types.StackResourceSummary) { 31 | m.operatorCollection.SetOperatorCollection(stackName, stackResourceSummaries) 32 | } 33 | 34 | func (m *OperatorManager) getOperatorResourcesLength() int { 35 | var length int 36 | for _, operator := range m.operatorCollection.GetOperators() { 37 | length += operator.GetResourcesLength() 38 | } 39 | return length 40 | } 41 | 42 | func (m *OperatorManager) CheckResourceCounts() error { 43 | logicalResourceIdsLength := len(m.operatorCollection.GetLogicalResourceIds()) 44 | operatorResourcesLength := m.getOperatorResourcesLength() 45 | 46 | if logicalResourceIdsLength != operatorResourcesLength { 47 | return m.operatorCollection.RaiseUnsupportedResourceError() 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (m *OperatorManager) GetLogicalResourceIds() []string { 54 | return m.operatorCollection.GetLogicalResourceIds() 55 | } 56 | 57 | func (m *OperatorManager) DeleteResourceCollection(ctx context.Context) error { 58 | eg, ctx := errgroup.WithContext(ctx) 59 | 60 | for _, operator := range m.operatorCollection.GetOperators() { 61 | operator := operator 62 | eg.Go(func() error { 63 | return operator.DeleteResources(ctx) 64 | }) 65 | } 66 | 67 | return eg.Wait() 68 | } 69 | -------------------------------------------------------------------------------- /internal/operation/operator_manager_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: operator_manager.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=operator_manager.go -destination=operator_manager_mock.go -package=operation -write_package_comment=false 7 | package operation 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockIOperatorManager is a mock of IOperatorManager interface. 18 | type MockIOperatorManager struct { 19 | ctrl *gomock.Controller 20 | recorder *MockIOperatorManagerMockRecorder 21 | } 22 | 23 | // MockIOperatorManagerMockRecorder is the mock recorder for MockIOperatorManager. 24 | type MockIOperatorManagerMockRecorder struct { 25 | mock *MockIOperatorManager 26 | } 27 | 28 | // NewMockIOperatorManager creates a new mock instance. 29 | func NewMockIOperatorManager(ctrl *gomock.Controller) *MockIOperatorManager { 30 | mock := &MockIOperatorManager{ctrl: ctrl} 31 | mock.recorder = &MockIOperatorManagerMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockIOperatorManager) EXPECT() *MockIOperatorManagerMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // CheckResourceCounts mocks base method. 41 | func (m *MockIOperatorManager) CheckResourceCounts() error { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "CheckResourceCounts") 44 | ret0, _ := ret[0].(error) 45 | return ret0 46 | } 47 | 48 | // CheckResourceCounts indicates an expected call of CheckResourceCounts. 49 | func (mr *MockIOperatorManagerMockRecorder) CheckResourceCounts() *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckResourceCounts", reflect.TypeOf((*MockIOperatorManager)(nil).CheckResourceCounts)) 52 | } 53 | 54 | // DeleteResourceCollection mocks base method. 55 | func (m *MockIOperatorManager) DeleteResourceCollection(ctx context.Context) error { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "DeleteResourceCollection", ctx) 58 | ret0, _ := ret[0].(error) 59 | return ret0 60 | } 61 | 62 | // DeleteResourceCollection indicates an expected call of DeleteResourceCollection. 63 | func (mr *MockIOperatorManagerMockRecorder) DeleteResourceCollection(ctx any) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResourceCollection", reflect.TypeOf((*MockIOperatorManager)(nil).DeleteResourceCollection), ctx) 66 | } 67 | 68 | // GetLogicalResourceIds mocks base method. 69 | func (m *MockIOperatorManager) GetLogicalResourceIds() []string { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "GetLogicalResourceIds") 72 | ret0, _ := ret[0].([]string) 73 | return ret0 74 | } 75 | 76 | // GetLogicalResourceIds indicates an expected call of GetLogicalResourceIds. 77 | func (mr *MockIOperatorManagerMockRecorder) GetLogicalResourceIds() *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogicalResourceIds", reflect.TypeOf((*MockIOperatorManager)(nil).GetLogicalResourceIds)) 80 | } 81 | 82 | // SetOperatorCollection mocks base method. 83 | func (m *MockIOperatorManager) SetOperatorCollection(stackName *string, stackResourceSummaries []types.StackResourceSummary) { 84 | m.ctrl.T.Helper() 85 | m.ctrl.Call(m, "SetOperatorCollection", stackName, stackResourceSummaries) 86 | } 87 | 88 | // SetOperatorCollection indicates an expected call of SetOperatorCollection. 89 | func (mr *MockIOperatorManagerMockRecorder) SetOperatorCollection(stackName, stackResourceSummaries any) *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOperatorCollection", reflect.TypeOf((*MockIOperatorManager)(nil).SetOperatorCollection), stackName, stackResourceSummaries) 92 | } 93 | -------------------------------------------------------------------------------- /internal/operation/operator_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: operator.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=operator.go -destination=operator_mock.go -package=operation -write_package_comment=false 7 | package operation 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockIOperator is a mock of IOperator interface. 18 | type MockIOperator struct { 19 | ctrl *gomock.Controller 20 | recorder *MockIOperatorMockRecorder 21 | } 22 | 23 | // MockIOperatorMockRecorder is the mock recorder for MockIOperator. 24 | type MockIOperatorMockRecorder struct { 25 | mock *MockIOperator 26 | } 27 | 28 | // NewMockIOperator creates a new mock instance. 29 | func NewMockIOperator(ctrl *gomock.Controller) *MockIOperator { 30 | mock := &MockIOperator{ctrl: ctrl} 31 | mock.recorder = &MockIOperatorMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockIOperator) EXPECT() *MockIOperatorMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // AddResource mocks base method. 41 | func (m *MockIOperator) AddResource(resource *types.StackResourceSummary) { 42 | m.ctrl.T.Helper() 43 | m.ctrl.Call(m, "AddResource", resource) 44 | } 45 | 46 | // AddResource indicates an expected call of AddResource. 47 | func (mr *MockIOperatorMockRecorder) AddResource(resource any) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddResource", reflect.TypeOf((*MockIOperator)(nil).AddResource), resource) 50 | } 51 | 52 | // DeleteResources mocks base method. 53 | func (m *MockIOperator) DeleteResources(ctx context.Context) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "DeleteResources", ctx) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // DeleteResources indicates an expected call of DeleteResources. 61 | func (mr *MockIOperatorMockRecorder) DeleteResources(ctx any) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResources", reflect.TypeOf((*MockIOperator)(nil).DeleteResources), ctx) 64 | } 65 | 66 | // GetResourcesLength mocks base method. 67 | func (m *MockIOperator) GetResourcesLength() int { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "GetResourcesLength") 70 | ret0, _ := ret[0].(int) 71 | return ret0 72 | } 73 | 74 | // GetResourcesLength indicates an expected call of GetResourcesLength. 75 | func (mr *MockIOperatorMockRecorder) GetResourcesLength() *gomock.Call { 76 | mr.mock.ctrl.T.Helper() 77 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourcesLength", reflect.TypeOf((*MockIOperator)(nil).GetResourcesLength)) 78 | } 79 | -------------------------------------------------------------------------------- /internal/operation/s3_bucket.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime" 7 | "sync" 8 | 9 | cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 10 | s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 11 | "github.com/go-to-k/delstack/pkg/client" 12 | "golang.org/x/sync/errgroup" 13 | "golang.org/x/sync/semaphore" 14 | ) 15 | 16 | var _ IOperator = (*S3BucketOperator)(nil) 17 | 18 | type S3BucketOperator struct { 19 | client client.IS3 20 | resources []*cfntypes.StackResourceSummary 21 | } 22 | 23 | func NewS3BucketOperator(client client.IS3) *S3BucketOperator { 24 | return &S3BucketOperator{ 25 | client: client, 26 | resources: []*cfntypes.StackResourceSummary{}, 27 | } 28 | } 29 | 30 | func (o *S3BucketOperator) AddResource(resource *cfntypes.StackResourceSummary) { 31 | o.resources = append(o.resources, resource) 32 | } 33 | 34 | func (o *S3BucketOperator) GetResourcesLength() int { 35 | return len(o.resources) 36 | } 37 | 38 | func (o *S3BucketOperator) DeleteResources(ctx context.Context) error { 39 | eg, ctx := errgroup.WithContext(ctx) 40 | sem := semaphore.NewWeighted(int64(runtime.NumCPU())) 41 | 42 | for _, bucket := range o.resources { 43 | bucket := bucket 44 | if err := sem.Acquire(ctx, 1); err != nil { 45 | return err 46 | } 47 | eg.Go(func() error { 48 | defer sem.Release(1) 49 | 50 | return o.DeleteS3Bucket(ctx, bucket.PhysicalResourceId) 51 | }) 52 | } 53 | 54 | return eg.Wait() 55 | } 56 | 57 | func (o *S3BucketOperator) DeleteS3Bucket(ctx context.Context, bucketName *string) error { 58 | exists, err := o.client.CheckBucketExists(ctx, bucketName) 59 | if err != nil { 60 | return err 61 | } 62 | if !exists { 63 | return nil 64 | } 65 | 66 | eg := errgroup.Group{} 67 | errorStr := "" 68 | errorsCount := 0 69 | errorsMtx := sync.Mutex{} 70 | var keyMarker *string 71 | var versionIdMarker *string 72 | for { 73 | var objects []s3types.ObjectIdentifier 74 | 75 | // ListObjectVersions/ListObjectsV2 API can only retrieve up to 1000 items, so it is good to pass it 76 | // directly to DeleteObjects, which can only delete up to 1000 items. 77 | output, err := o.client.ListObjectsOrVersionsByPage( 78 | ctx, 79 | bucketName, 80 | keyMarker, 81 | versionIdMarker, 82 | ) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | objects = output.ObjectIdentifiers 88 | keyMarker = output.NextKeyMarker 89 | versionIdMarker = output.NextVersionIdMarker 90 | 91 | if len(objects) == 0 { 92 | break 93 | } 94 | 95 | eg.Go(func() error { 96 | // One DeleteObjects is executed for each loop of the List, and it usually ends during 97 | // the next loop. Therefore, there seems to be no throttling concern, so the number of 98 | // parallels is not limited by semaphore. (Throttling occurs at about 3500 deletions 99 | // per second.) 100 | gotErrors, err := o.client.DeleteObjects(ctx, bucketName, objects) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if len(gotErrors) > 0 { 106 | errorsMtx.Lock() 107 | errorsCount += len(gotErrors) 108 | for _, error := range gotErrors { 109 | errorStr += fmt.Sprintf("\nBucketName: %v\n", *bucketName) 110 | errorStr += fmt.Sprintf("Code: %v\n", *error.Code) 111 | errorStr += fmt.Sprintf("Key: %v\n", *error.Key) 112 | errorStr += fmt.Sprintf("VersionId: %v\n", *error.VersionId) 113 | errorStr += fmt.Sprintf("Message: %v\n", *error.Message) 114 | } 115 | errorsMtx.Unlock() 116 | } 117 | 118 | return nil 119 | }) 120 | 121 | if keyMarker == nil && versionIdMarker == nil { 122 | break 123 | } 124 | } 125 | 126 | if err := eg.Wait(); err != nil { 127 | return err 128 | } 129 | 130 | if errorsCount > 0 { 131 | // The error is from `DeleteObjectsOutput.Errors`, not `err`. 132 | // However, we want to treat it as an error, so we use `client.ClientError`. 133 | return &client.ClientError{ 134 | ResourceName: bucketName, 135 | Err: fmt.Errorf("DeleteObjectsError: %v objects with errors were found. %v", errorsCount, errorStr), 136 | } 137 | } 138 | 139 | if err := o.client.DeleteBucket(ctx, bucketName); err != nil { 140 | return err 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func (o *S3BucketOperator) GetDirectoryBucketsFlag() bool { 147 | return o.client.GetDirectoryBucketsFlag() 148 | } 149 | -------------------------------------------------------------------------------- /internal/operation/s3_table_bucket.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 9 | "github.com/go-to-k/delstack/pkg/client" 10 | "golang.org/x/sync/errgroup" 11 | "golang.org/x/sync/semaphore" 12 | ) 13 | 14 | // Too Many Requests error often occurs, so limit the value 15 | const SemaphoreWeight = 4 16 | 17 | var _ IOperator = (*S3TableBucketOperator)(nil) 18 | 19 | type S3TableBucketOperator struct { 20 | client client.IS3Tables 21 | resources []*cfntypes.StackResourceSummary 22 | } 23 | 24 | func NewS3TableBucketOperator(client client.IS3Tables) *S3TableBucketOperator { 25 | return &S3TableBucketOperator{ 26 | client: client, 27 | resources: []*cfntypes.StackResourceSummary{}, 28 | } 29 | } 30 | 31 | func (o *S3TableBucketOperator) AddResource(resource *cfntypes.StackResourceSummary) { 32 | o.resources = append(o.resources, resource) 33 | } 34 | 35 | func (o *S3TableBucketOperator) GetResourcesLength() int { 36 | return len(o.resources) 37 | } 38 | 39 | func (o *S3TableBucketOperator) DeleteResources(ctx context.Context) error { 40 | eg, ctx := errgroup.WithContext(ctx) 41 | sem := semaphore.NewWeighted(int64(runtime.NumCPU())) 42 | 43 | for _, bucket := range o.resources { 44 | bucket := bucket 45 | if err := sem.Acquire(ctx, 1); err != nil { 46 | return err 47 | } 48 | eg.Go(func() error { 49 | defer sem.Release(1) 50 | 51 | return o.DeleteS3TableBucket(ctx, bucket.PhysicalResourceId) 52 | }) 53 | } 54 | 55 | return eg.Wait() 56 | } 57 | 58 | func (o *S3TableBucketOperator) DeleteS3TableBucket(ctx context.Context, tableBucketArn *string) error { 59 | exists, err := o.client.CheckTableBucketExists(ctx, tableBucketArn) 60 | if err != nil { 61 | return err 62 | } 63 | if !exists { 64 | return nil 65 | } 66 | 67 | eg := errgroup.Group{} 68 | sem := semaphore.NewWeighted(SemaphoreWeight) 69 | var continuationToken *string 70 | for { 71 | select { 72 | case <-ctx.Done(): 73 | return &client.ClientError{ 74 | ResourceName: tableBucketArn, 75 | Err: ctx.Err(), 76 | } 77 | default: 78 | } 79 | 80 | output, err := o.client.ListNamespacesByPage( 81 | ctx, 82 | tableBucketArn, 83 | continuationToken, 84 | ) 85 | if err != nil { 86 | return err 87 | } 88 | if len(output.Namespaces) == 0 { 89 | break 90 | } 91 | 92 | for _, summary := range output.Namespaces { 93 | for _, namespace := range summary.Namespace { 94 | if err := sem.Acquire(ctx, 1); err != nil { 95 | return err 96 | } 97 | eg.Go(func() error { 98 | defer sem.Release(1) 99 | return o.deleteNamespace(ctx, tableBucketArn, aws.String(namespace)) 100 | }) 101 | } 102 | } 103 | 104 | continuationToken = output.ContinuationToken 105 | if continuationToken == nil { 106 | break 107 | } 108 | } 109 | 110 | if err := eg.Wait(); err != nil { 111 | return err 112 | } 113 | 114 | if err := o.client.DeleteTableBucket(ctx, tableBucketArn); err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (o *S3TableBucketOperator) deleteNamespace( 122 | ctx context.Context, 123 | tableBucketArn *string, 124 | namespace *string, 125 | ) error { 126 | eg := errgroup.Group{} 127 | sem := semaphore.NewWeighted(SemaphoreWeight) 128 | 129 | var continuationToken *string 130 | for { 131 | select { 132 | case <-ctx.Done(): 133 | return &client.ClientError{ 134 | ResourceName: tableBucketArn, 135 | Err: ctx.Err(), 136 | } 137 | default: 138 | } 139 | 140 | output, err := o.client.ListTablesByPage(ctx, tableBucketArn, namespace, continuationToken) 141 | if err != nil { 142 | return err 143 | } 144 | if len(output.Tables) == 0 { 145 | break 146 | } 147 | 148 | for _, table := range output.Tables { 149 | if err := sem.Acquire(ctx, 1); err != nil { 150 | return err 151 | } 152 | eg.Go(func() error { 153 | defer sem.Release(1) 154 | if err := o.client.DeleteTable(ctx, table.Name, namespace, tableBucketArn); err != nil { 155 | return err 156 | } 157 | return nil 158 | }) 159 | } 160 | 161 | continuationToken = output.ContinuationToken 162 | if continuationToken == nil { 163 | break 164 | } 165 | } 166 | 167 | if err := eg.Wait(); err != nil { 168 | return err 169 | } 170 | 171 | return o.client.DeleteNamespace(ctx, namespace, tableBucketArn) 172 | } 173 | -------------------------------------------------------------------------------- /internal/resourcetype/resourcetype.go: -------------------------------------------------------------------------------- 1 | package resourcetype 2 | 3 | const ( 4 | S3Bucket = "AWS::S3::Bucket" 5 | S3DirectoryBucket = "AWS::S3Express::DirectoryBucket" 6 | S3TableBucket = "AWS::S3Tables::TableBucket" 7 | IamGroup = "AWS::IAM::Group" 8 | EcrRepository = "AWS::ECR::Repository" 9 | BackupVault = "AWS::Backup::BackupVault" 10 | CloudformationStack = "AWS::CloudFormation::Stack" 11 | CustomResource = "Custom::" 12 | ) 13 | 14 | func GetResourceTypes() []string { 15 | return []string{ 16 | S3Bucket, 17 | S3DirectoryBucket, 18 | S3TableBucket, 19 | IamGroup, 20 | EcrRepository, 21 | BackupVault, 22 | CloudformationStack, 23 | CustomResource, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/version/main_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.uber.org/goleak" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | fmt.Println() 12 | fmt.Println("==========================================") 13 | fmt.Println("========== Start Test: version ===========") 14 | fmt.Println("==========================================") 15 | goleak.VerifyTestMain(m) 16 | } 17 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime/debug" 5 | ) 6 | 7 | var Version = "" 8 | var Revision = "" 9 | 10 | func IsDebug() bool { 11 | return Version == "" || Revision != "" 12 | } 13 | 14 | func GetVersion() string { 15 | if Version != "" && Revision != "" { 16 | return Version + "-" + Revision 17 | } 18 | if Version != "" { 19 | return Version 20 | } 21 | 22 | i, ok := debug.ReadBuildInfo() 23 | if !ok { 24 | return "unknown" 25 | } 26 | return i.Main.Version 27 | } 28 | -------------------------------------------------------------------------------- /internal/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | /* 8 | Test Cases 9 | */ 10 | 11 | func Test_IsDebug(t *testing.T) { 12 | type args struct { 13 | Version string 14 | Revision string 15 | } 16 | 17 | cases := []struct { 18 | name string 19 | args args 20 | want bool 21 | wantErr bool 22 | }{ 23 | { 24 | name: "true if both Version and Revision are empty", 25 | args: args{ 26 | Version: "", 27 | Revision: "", 28 | }, 29 | want: true, 30 | wantErr: false, 31 | }, 32 | { 33 | name: "true if Version is empty and Revision is not empty", 34 | args: args{ 35 | Version: "", 36 | Revision: "abcde", 37 | }, 38 | want: true, 39 | wantErr: false, 40 | }, 41 | { 42 | name: "false if Revision is not empty and Revision is empty", 43 | args: args{ 44 | Version: "v1.0.0", 45 | Revision: "", 46 | }, 47 | want: false, 48 | wantErr: false, 49 | }, 50 | { 51 | name: "true if both Version and Revision are not empty", 52 | args: args{ 53 | Version: "v1.0.0", 54 | Revision: "abcde", 55 | }, 56 | want: true, 57 | wantErr: false, 58 | }, 59 | } 60 | 61 | for _, tt := range cases { 62 | t.Run(tt.name, func(t *testing.T) { 63 | Version = tt.args.Version 64 | Revision = tt.args.Revision 65 | got := IsDebug() 66 | 67 | if got != tt.want { 68 | t.Errorf("got = %#v, want %#v", got, tt.want) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func Test_GetVersion(t *testing.T) { 75 | type args struct { 76 | Version string 77 | Revision string 78 | } 79 | 80 | cases := []struct { 81 | name string 82 | args args 83 | want string 84 | wantErr bool 85 | }{ 86 | { 87 | name: "Both Version and Revision are empty", 88 | args: args{ 89 | Version: "", 90 | Revision: "", 91 | }, 92 | want: "", 93 | wantErr: false, 94 | }, 95 | { 96 | name: "Version is empty and Revision is not empty", 97 | args: args{ 98 | Version: "", 99 | Revision: "abcde", 100 | }, 101 | want: "", 102 | wantErr: false, 103 | }, 104 | { 105 | name: "Revision is not empty and Revision is empty", 106 | args: args{ 107 | Version: "v1.0.0", 108 | Revision: "", 109 | }, 110 | want: "v1.0.0", 111 | wantErr: false, 112 | }, 113 | { 114 | name: "Both Version and Revision are not empty", 115 | args: args{ 116 | Version: "v1.0.0", 117 | Revision: "abcde", 118 | }, 119 | want: "v1.0.0-abcde", 120 | wantErr: false, 121 | }, 122 | } 123 | 124 | for _, tt := range cases { 125 | t.Run(tt.name, func(t *testing.T) { 126 | Version = tt.args.Version 127 | Revision = tt.args.Revision 128 | got := GetVersion() 129 | 130 | if got != tt.want { 131 | t.Errorf("got = %#v, want %#v", got, tt.want) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /pkg/client/aws_config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | ) 9 | 10 | const DefaultAwsRegion = "us-east-1" 11 | 12 | func LoadAWSConfig(ctx context.Context, region string, profile string) (aws.Config, error) { 13 | var ( 14 | cfg aws.Config 15 | err error 16 | ) 17 | 18 | if profile != "" { 19 | cfg, err = config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(profile)) 20 | } else { 21 | cfg, err = config.LoadDefaultConfig(ctx) 22 | } 23 | 24 | if err != nil { 25 | return cfg, err 26 | } 27 | 28 | if region != "" { 29 | cfg.Region = region 30 | } 31 | if cfg.Region == "" { 32 | cfg.Region = DefaultAwsRegion 33 | } 34 | 35 | return cfg, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/client/backup.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=backup_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/backup" 9 | "github.com/aws/aws-sdk-go-v2/service/backup/types" 10 | ) 11 | 12 | type IBackup interface { 13 | ListRecoveryPointsByBackupVault(ctx context.Context, backupVaultName *string) ([]types.RecoveryPointByBackupVault, error) 14 | DeleteRecoveryPoints(ctx context.Context, backupVaultName *string, recoveryPoints []types.RecoveryPointByBackupVault) error 15 | DeleteBackupVault(ctx context.Context, backupVaultName *string) error 16 | CheckBackupVaultExists(ctx context.Context, backupVaultName *string) (bool, error) 17 | } 18 | 19 | var _ IBackup = (*Backup)(nil) 20 | 21 | type Backup struct { 22 | client *backup.Client 23 | } 24 | 25 | func NewBackup(client *backup.Client) *Backup { 26 | return &Backup{ 27 | client, 28 | } 29 | } 30 | 31 | func (b *Backup) ListRecoveryPointsByBackupVault(ctx context.Context, backupVaultName *string) ([]types.RecoveryPointByBackupVault, error) { 32 | var nextToken *string 33 | recoveryPoints := []types.RecoveryPointByBackupVault{} 34 | 35 | for { 36 | select { 37 | case <-ctx.Done(): 38 | return recoveryPoints, &ClientError{ 39 | ResourceName: backupVaultName, 40 | Err: ctx.Err(), 41 | } 42 | default: 43 | } 44 | 45 | input := &backup.ListRecoveryPointsByBackupVaultInput{ 46 | BackupVaultName: backupVaultName, 47 | NextToken: nextToken, 48 | } 49 | 50 | output, err := b.client.ListRecoveryPointsByBackupVault(ctx, input) 51 | if err != nil { 52 | return nil, &ClientError{ 53 | ResourceName: backupVaultName, 54 | Err: err, 55 | } 56 | } 57 | recoveryPoints = append(recoveryPoints, output.RecoveryPoints...) 58 | 59 | nextToken = output.NextToken 60 | if nextToken == nil { 61 | break 62 | } 63 | } 64 | 65 | return recoveryPoints, nil 66 | } 67 | 68 | func (b *Backup) DeleteRecoveryPoints(ctx context.Context, backupVaultName *string, recoveryPoints []types.RecoveryPointByBackupVault) error { 69 | for _, recoveryPoint := range recoveryPoints { 70 | if err := b.deleteRecoveryPoint(ctx, backupVaultName, recoveryPoint.RecoveryPointArn); err != nil { 71 | return &ClientError{ 72 | ResourceName: backupVaultName, 73 | Err: err, 74 | } 75 | } 76 | } 77 | 78 | // Deleting the backup vault immediately after deleting recovery points causes an error, so wait a certain amount of time. 79 | // See: https://github.com/go-to-k/delstack/issues/370 80 | time.Sleep(time.Duration(5) * time.Second) 81 | 82 | return nil 83 | } 84 | 85 | func (b *Backup) deleteRecoveryPoint(ctx context.Context, backupVaultName *string, recoveryPointArn *string) error { 86 | input := &backup.DeleteRecoveryPointInput{ 87 | BackupVaultName: backupVaultName, 88 | RecoveryPointArn: recoveryPointArn, 89 | } 90 | 91 | _, err := b.client.DeleteRecoveryPoint(ctx, input) 92 | if err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | func (b *Backup) DeleteBackupVault(ctx context.Context, backupVaultName *string) error { 99 | input := &backup.DeleteBackupVaultInput{ 100 | BackupVaultName: backupVaultName, 101 | } 102 | 103 | _, err := b.client.DeleteBackupVault(ctx, input) 104 | if err != nil { 105 | return &ClientError{ 106 | ResourceName: backupVaultName, 107 | Err: err, 108 | } 109 | } 110 | return nil 111 | } 112 | 113 | func (b *Backup) CheckBackupVaultExists(ctx context.Context, backupVaultName *string) (bool, error) { 114 | var nextToken *string 115 | 116 | for { 117 | select { 118 | case <-ctx.Done(): 119 | return false, &ClientError{ 120 | ResourceName: backupVaultName, 121 | Err: ctx.Err(), 122 | } 123 | default: 124 | } 125 | 126 | input := &backup.ListBackupVaultsInput{ 127 | NextToken: nextToken, 128 | } 129 | 130 | output, err := b.client.ListBackupVaults(ctx, input) 131 | if err != nil { 132 | return false, &ClientError{ 133 | ResourceName: backupVaultName, 134 | Err: err, 135 | } 136 | } 137 | 138 | for _, vault := range output.BackupVaultList { 139 | if *vault.BackupVaultName == *backupVaultName { 140 | return true, nil 141 | } 142 | } 143 | 144 | nextToken = output.NextToken 145 | 146 | if nextToken == nil { 147 | break 148 | } 149 | } 150 | 151 | return false, nil 152 | } 153 | -------------------------------------------------------------------------------- /pkg/client/backup_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: backup.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=backup.go -destination=backup_mock.go -package=client -write_package_comment=false 7 | package client 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | types "github.com/aws/aws-sdk-go-v2/service/backup/types" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockIBackup is a mock of IBackup interface. 18 | type MockIBackup struct { 19 | ctrl *gomock.Controller 20 | recorder *MockIBackupMockRecorder 21 | } 22 | 23 | // MockIBackupMockRecorder is the mock recorder for MockIBackup. 24 | type MockIBackupMockRecorder struct { 25 | mock *MockIBackup 26 | } 27 | 28 | // NewMockIBackup creates a new mock instance. 29 | func NewMockIBackup(ctrl *gomock.Controller) *MockIBackup { 30 | mock := &MockIBackup{ctrl: ctrl} 31 | mock.recorder = &MockIBackupMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockIBackup) EXPECT() *MockIBackupMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // CheckBackupVaultExists mocks base method. 41 | func (m *MockIBackup) CheckBackupVaultExists(ctx context.Context, backupVaultName *string) (bool, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "CheckBackupVaultExists", ctx, backupVaultName) 44 | ret0, _ := ret[0].(bool) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // CheckBackupVaultExists indicates an expected call of CheckBackupVaultExists. 50 | func (mr *MockIBackupMockRecorder) CheckBackupVaultExists(ctx, backupVaultName any) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckBackupVaultExists", reflect.TypeOf((*MockIBackup)(nil).CheckBackupVaultExists), ctx, backupVaultName) 53 | } 54 | 55 | // DeleteBackupVault mocks base method. 56 | func (m *MockIBackup) DeleteBackupVault(ctx context.Context, backupVaultName *string) error { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "DeleteBackupVault", ctx, backupVaultName) 59 | ret0, _ := ret[0].(error) 60 | return ret0 61 | } 62 | 63 | // DeleteBackupVault indicates an expected call of DeleteBackupVault. 64 | func (mr *MockIBackupMockRecorder) DeleteBackupVault(ctx, backupVaultName any) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBackupVault", reflect.TypeOf((*MockIBackup)(nil).DeleteBackupVault), ctx, backupVaultName) 67 | } 68 | 69 | // DeleteRecoveryPoints mocks base method. 70 | func (m *MockIBackup) DeleteRecoveryPoints(ctx context.Context, backupVaultName *string, recoveryPoints []types.RecoveryPointByBackupVault) error { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "DeleteRecoveryPoints", ctx, backupVaultName, recoveryPoints) 73 | ret0, _ := ret[0].(error) 74 | return ret0 75 | } 76 | 77 | // DeleteRecoveryPoints indicates an expected call of DeleteRecoveryPoints. 78 | func (mr *MockIBackupMockRecorder) DeleteRecoveryPoints(ctx, backupVaultName, recoveryPoints any) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRecoveryPoints", reflect.TypeOf((*MockIBackup)(nil).DeleteRecoveryPoints), ctx, backupVaultName, recoveryPoints) 81 | } 82 | 83 | // ListRecoveryPointsByBackupVault mocks base method. 84 | func (m *MockIBackup) ListRecoveryPointsByBackupVault(ctx context.Context, backupVaultName *string) ([]types.RecoveryPointByBackupVault, error) { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "ListRecoveryPointsByBackupVault", ctx, backupVaultName) 87 | ret0, _ := ret[0].([]types.RecoveryPointByBackupVault) 88 | ret1, _ := ret[1].(error) 89 | return ret0, ret1 90 | } 91 | 92 | // ListRecoveryPointsByBackupVault indicates an expected call of ListRecoveryPointsByBackupVault. 93 | func (mr *MockIBackupMockRecorder) ListRecoveryPointsByBackupVault(ctx, backupVaultName any) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRecoveryPointsByBackupVault", reflect.TypeOf((*MockIBackup)(nil).ListRecoveryPointsByBackupVault), ctx, backupVaultName) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/client/cloudformation.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=cloudformation_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "strings" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 10 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 11 | ) 12 | 13 | const CloudFormationWaitNanoSecTime = time.Duration(4500000000000) 14 | 15 | type ICloudFormation interface { 16 | DeleteStack(ctx context.Context, stackName *string, retainResources []string) error 17 | DescribeStacks(ctx context.Context, stackName *string) ([]types.Stack, error) 18 | ListStackResources(ctx context.Context, stackName *string) ([]types.StackResourceSummary, error) 19 | GetTemplate(ctx context.Context, stackName *string) (*string, error) 20 | UpdateStack(ctx context.Context, stackName *string, templateBody *string, parameters []types.Parameter) error 21 | } 22 | 23 | var _ ICloudFormation = (*CloudFormation)(nil) 24 | 25 | type CloudFormation struct { 26 | client *cloudformation.Client 27 | deleteCompleteWaiter *cloudformation.StackDeleteCompleteWaiter 28 | updateCompleteWaiter *cloudformation.StackUpdateCompleteWaiter 29 | } 30 | 31 | func NewCloudFormation(client *cloudformation.Client, deleteCompleteWaiter *cloudformation.StackDeleteCompleteWaiter, updateCompleteWaiter *cloudformation.StackUpdateCompleteWaiter) *CloudFormation { 32 | return &CloudFormation{ 33 | client, 34 | deleteCompleteWaiter, 35 | updateCompleteWaiter, 36 | } 37 | } 38 | 39 | func (c *CloudFormation) DeleteStack(ctx context.Context, stackName *string, retainResources []string) error { 40 | input := &cloudformation.DeleteStackInput{ 41 | StackName: stackName, 42 | RetainResources: retainResources, 43 | } 44 | 45 | if _, err := c.client.DeleteStack(ctx, input); err != nil { 46 | return &ClientError{ 47 | ResourceName: stackName, 48 | Err: err, 49 | } 50 | } 51 | 52 | if err := c.waitDeleteStack(ctx, stackName); err != nil { 53 | return &ClientError{ 54 | ResourceName: stackName, 55 | Err: err, 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (c *CloudFormation) DescribeStacks(ctx context.Context, stackName *string) ([]types.Stack, error) { 63 | var nextToken *string 64 | stacks := []types.Stack{} 65 | 66 | for { 67 | select { 68 | case <-ctx.Done(): 69 | return stacks, &ClientError{ 70 | ResourceName: stackName, 71 | Err: ctx.Err(), 72 | } 73 | default: 74 | } 75 | 76 | // If a stackName is nil, then return all stacks 77 | input := &cloudformation.DescribeStacksInput{ 78 | NextToken: nextToken, 79 | StackName: stackName, 80 | } 81 | 82 | output, err := c.client.DescribeStacks(ctx, input) 83 | if err != nil && strings.Contains(err.Error(), "does not exist") { 84 | return stacks, nil 85 | } 86 | if err != nil { 87 | return stacks, &ClientError{ 88 | ResourceName: stackName, 89 | Err: err, 90 | } 91 | } 92 | 93 | if len(stacks) == 0 && len(output.Stacks) == 0 { 94 | return stacks, nil 95 | } 96 | stacks = append(stacks, output.Stacks...) 97 | 98 | nextToken = output.NextToken 99 | if nextToken == nil { 100 | break 101 | } 102 | } 103 | return stacks, nil 104 | } 105 | 106 | func (c *CloudFormation) ListStackResources(ctx context.Context, stackName *string) ([]types.StackResourceSummary, error) { 107 | var nextToken *string 108 | stackResourceSummaries := []types.StackResourceSummary{} 109 | 110 | for { 111 | select { 112 | case <-ctx.Done(): 113 | return stackResourceSummaries, &ClientError{ 114 | ResourceName: stackName, 115 | Err: ctx.Err(), 116 | } 117 | default: 118 | } 119 | 120 | input := &cloudformation.ListStackResourcesInput{ 121 | StackName: stackName, 122 | NextToken: nextToken, 123 | } 124 | 125 | output, err := c.client.ListStackResources(ctx, input) 126 | if err != nil { 127 | return stackResourceSummaries, &ClientError{ 128 | ResourceName: stackName, 129 | Err: err, 130 | } 131 | } 132 | 133 | stackResourceSummaries = append(stackResourceSummaries, output.StackResourceSummaries...) 134 | nextToken = output.NextToken 135 | 136 | if nextToken == nil { 137 | break 138 | } 139 | } 140 | 141 | return stackResourceSummaries, nil 142 | } 143 | 144 | func (c *CloudFormation) GetTemplate(ctx context.Context, stackName *string) (*string, error) { 145 | input := &cloudformation.GetTemplateInput{ 146 | StackName: stackName, 147 | } 148 | 149 | output, err := c.client.GetTemplate(ctx, input) 150 | if err != nil { 151 | return nil, &ClientError{ 152 | ResourceName: stackName, 153 | Err: err, 154 | } 155 | } 156 | 157 | return output.TemplateBody, nil 158 | } 159 | 160 | func (c *CloudFormation) UpdateStack(ctx context.Context, stackName *string, templateBody *string, parameters []types.Parameter) error { 161 | input := &cloudformation.UpdateStackInput{ 162 | StackName: stackName, 163 | TemplateBody: templateBody, 164 | Capabilities: []types.Capability{ 165 | types.CapabilityCapabilityIam, 166 | types.CapabilityCapabilityNamedIam, 167 | types.CapabilityCapabilityAutoExpand, 168 | }, 169 | Parameters: parameters, 170 | } 171 | 172 | _, err := c.client.UpdateStack(ctx, input) 173 | if err != nil { 174 | return &ClientError{ 175 | ResourceName: stackName, 176 | Err: err, 177 | } 178 | } 179 | 180 | if err := c.waitUpdateStack(ctx, stackName); err != nil { 181 | return &ClientError{ 182 | ResourceName: stackName, 183 | Err: err, 184 | } 185 | } 186 | 187 | return nil 188 | } 189 | 190 | func (c *CloudFormation) waitDeleteStack(ctx context.Context, stackName *string) error { 191 | input := &cloudformation.DescribeStacksInput{ 192 | StackName: stackName, 193 | } 194 | 195 | err := c.deleteCompleteWaiter.Wait(ctx, input, CloudFormationWaitNanoSecTime) 196 | if err != nil && !strings.Contains(err.Error(), "waiter state transitioned to Failure") { 197 | return err // return non wrapping error because wrap in public callers 198 | } 199 | 200 | return nil 201 | } 202 | 203 | func (c *CloudFormation) waitUpdateStack(ctx context.Context, stackName *string) error { 204 | input := &cloudformation.DescribeStacksInput{ 205 | StackName: stackName, 206 | } 207 | 208 | err := c.updateCompleteWaiter.Wait(ctx, input, CloudFormationWaitNanoSecTime) 209 | if err != nil && !strings.Contains(err.Error(), "waiter state transitioned to Failure") { 210 | return err // return non wrapping error because wrap in public callers 211 | } 212 | 213 | return nil 214 | } 215 | -------------------------------------------------------------------------------- /pkg/client/cloudformation_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: cloudformation.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=cloudformation.go -destination=cloudformation_mock.go -package=client -write_package_comment=false 7 | package client 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockICloudFormation is a mock of ICloudFormation interface. 18 | type MockICloudFormation struct { 19 | ctrl *gomock.Controller 20 | recorder *MockICloudFormationMockRecorder 21 | } 22 | 23 | // MockICloudFormationMockRecorder is the mock recorder for MockICloudFormation. 24 | type MockICloudFormationMockRecorder struct { 25 | mock *MockICloudFormation 26 | } 27 | 28 | // NewMockICloudFormation creates a new mock instance. 29 | func NewMockICloudFormation(ctrl *gomock.Controller) *MockICloudFormation { 30 | mock := &MockICloudFormation{ctrl: ctrl} 31 | mock.recorder = &MockICloudFormationMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockICloudFormation) EXPECT() *MockICloudFormationMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // DeleteStack mocks base method. 41 | func (m *MockICloudFormation) DeleteStack(ctx context.Context, stackName *string, retainResources []string) error { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "DeleteStack", ctx, stackName, retainResources) 44 | ret0, _ := ret[0].(error) 45 | return ret0 46 | } 47 | 48 | // DeleteStack indicates an expected call of DeleteStack. 49 | func (mr *MockICloudFormationMockRecorder) DeleteStack(ctx, stackName, retainResources any) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockICloudFormation)(nil).DeleteStack), ctx, stackName, retainResources) 52 | } 53 | 54 | // DescribeStacks mocks base method. 55 | func (m *MockICloudFormation) DescribeStacks(ctx context.Context, stackName *string) ([]types.Stack, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "DescribeStacks", ctx, stackName) 58 | ret0, _ := ret[0].([]types.Stack) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // DescribeStacks indicates an expected call of DescribeStacks. 64 | func (mr *MockICloudFormationMockRecorder) DescribeStacks(ctx, stackName any) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStacks", reflect.TypeOf((*MockICloudFormation)(nil).DescribeStacks), ctx, stackName) 67 | } 68 | 69 | // GetTemplate mocks base method. 70 | func (m *MockICloudFormation) GetTemplate(ctx context.Context, stackName *string) (*string, error) { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "GetTemplate", ctx, stackName) 73 | ret0, _ := ret[0].(*string) 74 | ret1, _ := ret[1].(error) 75 | return ret0, ret1 76 | } 77 | 78 | // GetTemplate indicates an expected call of GetTemplate. 79 | func (mr *MockICloudFormationMockRecorder) GetTemplate(ctx, stackName any) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplate", reflect.TypeOf((*MockICloudFormation)(nil).GetTemplate), ctx, stackName) 82 | } 83 | 84 | // ListStackResources mocks base method. 85 | func (m *MockICloudFormation) ListStackResources(ctx context.Context, stackName *string) ([]types.StackResourceSummary, error) { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "ListStackResources", ctx, stackName) 88 | ret0, _ := ret[0].([]types.StackResourceSummary) 89 | ret1, _ := ret[1].(error) 90 | return ret0, ret1 91 | } 92 | 93 | // ListStackResources indicates an expected call of ListStackResources. 94 | func (mr *MockICloudFormationMockRecorder) ListStackResources(ctx, stackName any) *gomock.Call { 95 | mr.mock.ctrl.T.Helper() 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStackResources", reflect.TypeOf((*MockICloudFormation)(nil).ListStackResources), ctx, stackName) 97 | } 98 | 99 | // UpdateStack mocks base method. 100 | func (m *MockICloudFormation) UpdateStack(ctx context.Context, stackName, templateBody *string, parameters []types.Parameter) error { 101 | m.ctrl.T.Helper() 102 | ret := m.ctrl.Call(m, "UpdateStack", ctx, stackName, templateBody, parameters) 103 | ret0, _ := ret[0].(error) 104 | return ret0 105 | } 106 | 107 | // UpdateStack indicates an expected call of UpdateStack. 108 | func (mr *MockICloudFormationMockRecorder) UpdateStack(ctx, stackName, templateBody, parameters any) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStack", reflect.TypeOf((*MockICloudFormation)(nil).UpdateStack), ctx, stackName, templateBody, parameters) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/client/ecr.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=ecr_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/ecr" 9 | ) 10 | 11 | type IEcr interface { 12 | DeleteRepository(ctx context.Context, repositoryName *string) error 13 | CheckEcrExists(ctx context.Context, repositoryName *string) (bool, error) 14 | } 15 | 16 | var _ IEcr = (*Ecr)(nil) 17 | 18 | type Ecr struct { 19 | client *ecr.Client 20 | } 21 | 22 | func NewEcr(client *ecr.Client) *Ecr { 23 | return &Ecr{ 24 | client, 25 | } 26 | } 27 | 28 | func (e *Ecr) DeleteRepository(ctx context.Context, repositoryName *string) error { 29 | input := &ecr.DeleteRepositoryInput{ 30 | RepositoryName: repositoryName, 31 | Force: true, 32 | } 33 | 34 | _, err := e.client.DeleteRepository(ctx, input) 35 | if err != nil { 36 | return &ClientError{ 37 | ResourceName: repositoryName, 38 | Err: err, 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | func (e *Ecr) CheckEcrExists(ctx context.Context, repositoryName *string) (bool, error) { 45 | var nextToken *string 46 | 47 | for { 48 | select { 49 | case <-ctx.Done(): 50 | return false, &ClientError{ 51 | ResourceName: repositoryName, 52 | Err: ctx.Err(), 53 | } 54 | default: 55 | } 56 | 57 | input := &ecr.DescribeRepositoriesInput{ 58 | NextToken: nextToken, 59 | RepositoryNames: []string{ 60 | *repositoryName, 61 | }, 62 | } 63 | 64 | output, err := e.client.DescribeRepositories(ctx, input) 65 | if err != nil && strings.Contains(err.Error(), "does not exist") { 66 | return false, nil 67 | } 68 | if err != nil { 69 | return false, &ClientError{ 70 | ResourceName: repositoryName, 71 | Err: err, 72 | } 73 | } 74 | 75 | for _, repository := range output.Repositories { 76 | if *repository.RepositoryName == *repositoryName { 77 | return true, nil 78 | } 79 | } 80 | 81 | nextToken = output.NextToken 82 | 83 | if nextToken == nil { 84 | break 85 | } 86 | } 87 | 88 | return false, nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/client/ecr_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ecr.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=ecr.go -destination=ecr_mock.go -package=client -write_package_comment=false 7 | package client 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | gomock "go.uber.org/mock/gomock" 14 | ) 15 | 16 | // MockIEcr is a mock of IEcr interface. 17 | type MockIEcr struct { 18 | ctrl *gomock.Controller 19 | recorder *MockIEcrMockRecorder 20 | } 21 | 22 | // MockIEcrMockRecorder is the mock recorder for MockIEcr. 23 | type MockIEcrMockRecorder struct { 24 | mock *MockIEcr 25 | } 26 | 27 | // NewMockIEcr creates a new mock instance. 28 | func NewMockIEcr(ctrl *gomock.Controller) *MockIEcr { 29 | mock := &MockIEcr{ctrl: ctrl} 30 | mock.recorder = &MockIEcrMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockIEcr) EXPECT() *MockIEcrMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // CheckEcrExists mocks base method. 40 | func (m *MockIEcr) CheckEcrExists(ctx context.Context, repositoryName *string) (bool, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "CheckEcrExists", ctx, repositoryName) 43 | ret0, _ := ret[0].(bool) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // CheckEcrExists indicates an expected call of CheckEcrExists. 49 | func (mr *MockIEcrMockRecorder) CheckEcrExists(ctx, repositoryName any) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckEcrExists", reflect.TypeOf((*MockIEcr)(nil).CheckEcrExists), ctx, repositoryName) 52 | } 53 | 54 | // DeleteRepository mocks base method. 55 | func (m *MockIEcr) DeleteRepository(ctx context.Context, repositoryName *string) error { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "DeleteRepository", ctx, repositoryName) 58 | ret0, _ := ret[0].(error) 59 | return ret0 60 | } 61 | 62 | // DeleteRepository indicates an expected call of DeleteRepository. 63 | func (mr *MockIEcrMockRecorder) DeleteRepository(ctx, repositoryName any) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepository", reflect.TypeOf((*MockIEcr)(nil).DeleteRepository), ctx, repositoryName) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/client/ecr_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/aws/aws-sdk-go-v2/service/ecr" 12 | "github.com/aws/aws-sdk-go-v2/service/ecr/types" 13 | "github.com/aws/smithy-go/middleware" 14 | ) 15 | 16 | /* 17 | Test Cases 18 | */ 19 | 20 | func TestEcr_DeleteRepository(t *testing.T) { 21 | type args struct { 22 | ctx context.Context 23 | repositoryName *string 24 | withAPIOptionsFunc func(*middleware.Stack) error 25 | } 26 | 27 | cases := []struct { 28 | name string 29 | args args 30 | want error 31 | wantErr bool 32 | }{ 33 | { 34 | name: "delete repository successfully", 35 | args: args{ 36 | ctx: context.Background(), 37 | repositoryName: aws.String("test"), 38 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 39 | return stack.Finalize.Add( 40 | middleware.FinalizeMiddlewareFunc( 41 | "DeleteRepositoryMock", 42 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 43 | return middleware.FinalizeOutput{ 44 | Result: &ecr.DeleteRepositoryOutput{}, 45 | }, middleware.Metadata{}, nil 46 | }, 47 | ), 48 | middleware.Before, 49 | ) 50 | }, 51 | }, 52 | want: nil, 53 | wantErr: false, 54 | }, 55 | { 56 | name: "delete repository failure", 57 | args: args{ 58 | ctx: context.Background(), 59 | repositoryName: aws.String("test"), 60 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 61 | return stack.Finalize.Add( 62 | middleware.FinalizeMiddlewareFunc( 63 | "DeleteRepositoryErrorMock", 64 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 65 | return middleware.FinalizeOutput{ 66 | Result: &ecr.DeleteRepositoryOutput{}, 67 | }, middleware.Metadata{}, fmt.Errorf("DeleteRepositoryError") 68 | }, 69 | ), 70 | middleware.Before, 71 | ) 72 | }, 73 | }, 74 | want: &ClientError{ 75 | ResourceName: aws.String("test"), 76 | Err: fmt.Errorf("operation error ECR: DeleteRepository, DeleteRepositoryError"), 77 | }, 78 | wantErr: true, 79 | }, 80 | } 81 | 82 | for _, tt := range cases { 83 | t.Run(tt.name, func(t *testing.T) { 84 | cfg, err := config.LoadDefaultConfig( 85 | tt.args.ctx, 86 | config.WithRegion("ap-northeast-1"), 87 | config.WithAPIOptions([]func(*middleware.Stack) error{tt.args.withAPIOptionsFunc}), 88 | ) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | client := ecr.NewFromConfig(cfg) 94 | ecrClient := NewEcr(client) 95 | 96 | err = ecrClient.DeleteRepository(tt.args.ctx, tt.args.repositoryName) 97 | if (err != nil) != tt.wantErr { 98 | t.Errorf("error = %#v, wantErr %#v", err, tt.wantErr) 99 | return 100 | } 101 | if tt.wantErr && err.Error() != tt.want.Error() { 102 | t.Errorf("err = %#v, want %#v", err, tt.want) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestEcr_CheckRepository(t *testing.T) { 109 | type args struct { 110 | ctx context.Context 111 | repositoryName *string 112 | withAPIOptionsFunc func(*middleware.Stack) error 113 | } 114 | 115 | type want struct { 116 | exists bool 117 | err error 118 | } 119 | 120 | cases := []struct { 121 | name string 122 | args args 123 | want want 124 | wantErr bool 125 | }{ 126 | { 127 | name: "check repository exists successfully", 128 | args: args{ 129 | ctx: context.Background(), 130 | repositoryName: aws.String("test"), 131 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 132 | return stack.Finalize.Add( 133 | middleware.FinalizeMiddlewareFunc( 134 | "DescribeRepositoriesMock", 135 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 136 | return middleware.FinalizeOutput{ 137 | Result: &ecr.DescribeRepositoriesOutput{ 138 | Repositories: []types.Repository{ 139 | { 140 | RepositoryName: aws.String("test"), 141 | }, 142 | }, 143 | }, 144 | }, middleware.Metadata{}, nil 145 | }, 146 | ), 147 | middleware.Before, 148 | ) 149 | }, 150 | }, 151 | want: want{ 152 | exists: true, 153 | err: nil, 154 | }, 155 | wantErr: false, 156 | }, 157 | { 158 | name: "check repository not exists successfully", 159 | args: args{ 160 | ctx: context.Background(), 161 | repositoryName: aws.String("test"), 162 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 163 | return stack.Finalize.Add( 164 | middleware.FinalizeMiddlewareFunc( 165 | "DescribeRepositoriesNotExistMock", 166 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 167 | return middleware.FinalizeOutput{ 168 | Result: &ecr.DescribeRepositoriesOutput{ 169 | Repositories: []types.Repository{}, 170 | }, 171 | }, middleware.Metadata{}, fmt.Errorf("does not exist") 172 | }, 173 | ), 174 | middleware.Before, 175 | ) 176 | }, 177 | }, 178 | want: want{ 179 | exists: false, 180 | err: nil, 181 | }, 182 | wantErr: false, 183 | }, 184 | { 185 | name: "check repository exists failure", 186 | args: args{ 187 | ctx: context.Background(), 188 | repositoryName: aws.String("test"), 189 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 190 | return stack.Finalize.Add( 191 | middleware.FinalizeMiddlewareFunc( 192 | "DescribeRepositoriesErrorMock", 193 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 194 | return middleware.FinalizeOutput{ 195 | Result: &ecr.DescribeRepositoriesOutput{ 196 | Repositories: []types.Repository{}, 197 | }, 198 | }, middleware.Metadata{}, fmt.Errorf("DescribeRepositoriesError") 199 | }, 200 | ), 201 | middleware.Before, 202 | ) 203 | }, 204 | }, 205 | want: want{ 206 | exists: false, 207 | err: &ClientError{ 208 | ResourceName: aws.String("test"), 209 | Err: fmt.Errorf("operation error ECR: DescribeRepositories, DescribeRepositoriesError"), 210 | }, 211 | }, 212 | wantErr: true, 213 | }, 214 | } 215 | 216 | for _, tt := range cases { 217 | t.Run(tt.name, func(t *testing.T) { 218 | cfg, err := config.LoadDefaultConfig( 219 | tt.args.ctx, 220 | config.WithRegion("ap-northeast-1"), 221 | config.WithAPIOptions([]func(*middleware.Stack) error{tt.args.withAPIOptionsFunc}), 222 | ) 223 | if err != nil { 224 | t.Fatal(err) 225 | } 226 | 227 | client := ecr.NewFromConfig(cfg) 228 | ecrClient := NewEcr(client) 229 | 230 | output, err := ecrClient.CheckEcrExists(tt.args.ctx, tt.args.repositoryName) 231 | if (err != nil) != tt.wantErr { 232 | t.Errorf("error = %#v, wantErr %#v", err.Error(), tt.wantErr) 233 | return 234 | } 235 | if tt.wantErr && err.Error() != tt.want.err.Error() { 236 | t.Errorf("err = %#v, want %#v", err.Error(), tt.want.err.Error()) 237 | return 238 | } 239 | if !reflect.DeepEqual(output, tt.want.exists) { 240 | t.Errorf("output = %#v, want %#v", output, tt.want.exists) 241 | } 242 | }) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /pkg/client/errors.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "fmt" 4 | 5 | var _ error = (*ClientError)(nil) 6 | 7 | // ClientError provides the error with a resource name 8 | type ClientError struct { 9 | ResourceName *string 10 | Err error 11 | } 12 | 13 | func (e *ClientError) Error() string { 14 | var msg string 15 | if e.ResourceName == nil { 16 | msg = fmt.Sprintf("[resource -] %v", e.Err) 17 | } else { 18 | msg = fmt.Sprintf("[resource %s] %v", *e.ResourceName, e.Err) 19 | } 20 | return msg 21 | } 22 | 23 | func (e *ClientError) Unwrap() error { 24 | return e.Err 25 | } 26 | -------------------------------------------------------------------------------- /pkg/client/iam.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=iam_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/iam" 9 | "github.com/aws/aws-sdk-go-v2/service/iam/types" 10 | ) 11 | 12 | var SleepTimeSecForIam = 5 13 | 14 | type IIam interface { 15 | DeleteGroup(ctx context.Context, groupName *string) error 16 | CheckGroupExists(ctx context.Context, groupName *string) (bool, error) 17 | GetGroupUsers(ctx context.Context, groupName *string) ([]types.User, error) 18 | RemoveUsersFromGroup(ctx context.Context, groupName *string, users []types.User) error 19 | } 20 | 21 | var _ IIam = (*Iam)(nil) 22 | 23 | type Iam struct { 24 | client *iam.Client 25 | retryer *Retryer 26 | outputForGetGroup *iam.GetGroupOutput // for caching 27 | } 28 | 29 | func NewIam(client *iam.Client) *Iam { 30 | retryable := func(err error) bool { 31 | return strings.Contains(err.Error(), "api error Throttling: Rate exceeded") 32 | } 33 | retryer := NewRetryer(retryable, SleepTimeSecForIam) 34 | 35 | return &Iam{ 36 | client, 37 | retryer, 38 | nil, 39 | } 40 | } 41 | 42 | func (i *Iam) DeleteGroup(ctx context.Context, groupName *string) error { 43 | input := &iam.DeleteGroupInput{ 44 | GroupName: groupName, 45 | } 46 | 47 | optFn := func(o *iam.Options) { 48 | o.Retryer = i.retryer 49 | } 50 | 51 | _, err := i.client.DeleteGroup(ctx, input, optFn) 52 | if err != nil { 53 | return &ClientError{ 54 | ResourceName: groupName, 55 | Err: err, 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | func (i *Iam) CheckGroupExists(ctx context.Context, groupName *string) (bool, error) { 62 | _, err := i.getGroup(ctx, groupName) 63 | if err != nil && strings.Contains(err.Error(), "NoSuchEntity") { 64 | return false, nil 65 | } 66 | if err != nil { 67 | return false, &ClientError{ 68 | ResourceName: groupName, 69 | Err: err, 70 | } 71 | } 72 | 73 | return true, nil 74 | } 75 | 76 | func (i *Iam) GetGroupUsers(ctx context.Context, groupName *string) ([]types.User, error) { 77 | output, err := i.getGroup(ctx, groupName) 78 | if err != nil { 79 | return nil, &ClientError{ 80 | ResourceName: groupName, 81 | Err: err, 82 | } 83 | } 84 | 85 | return output.Users, nil 86 | } 87 | 88 | func (i *Iam) getGroup(ctx context.Context, groupName *string) (*iam.GetGroupOutput, error) { 89 | if i.outputForGetGroup != nil { 90 | return i.outputForGetGroup, nil 91 | } 92 | 93 | input := &iam.GetGroupInput{ 94 | GroupName: groupName, 95 | } 96 | 97 | optFn := func(o *iam.Options) { 98 | o.Retryer = i.retryer 99 | } 100 | 101 | // GetGroup returns a Marker, but we don't need to paginate because we get only one group by a full name 102 | output, err := i.client.GetGroup(ctx, input, optFn) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | i.outputForGetGroup = output 108 | return output, nil 109 | } 110 | 111 | func (i *Iam) RemoveUsersFromGroup(ctx context.Context, groupName *string, users []types.User) error { 112 | for _, user := range users { 113 | if err := i.removeUserFromGroup(ctx, groupName, user.UserName); err != nil { 114 | return &ClientError{ 115 | ResourceName: groupName, 116 | Err: err, 117 | } 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (i *Iam) removeUserFromGroup(ctx context.Context, groupName *string, userName *string) error { 125 | input := &iam.RemoveUserFromGroupInput{ 126 | UserName: userName, 127 | GroupName: groupName, 128 | } 129 | 130 | optFn := func(o *iam.Options) { 131 | o.Retryer = i.retryer 132 | } 133 | 134 | _, err := i.client.RemoveUserFromGroup(ctx, input, optFn) 135 | if err != nil { 136 | return err 137 | } 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /pkg/client/iam_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: iam.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=iam.go -destination=iam_mock.go -package=client -write_package_comment=false 7 | package client 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | types "github.com/aws/aws-sdk-go-v2/service/iam/types" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockIIam is a mock of IIam interface. 18 | type MockIIam struct { 19 | ctrl *gomock.Controller 20 | recorder *MockIIamMockRecorder 21 | } 22 | 23 | // MockIIamMockRecorder is the mock recorder for MockIIam. 24 | type MockIIamMockRecorder struct { 25 | mock *MockIIam 26 | } 27 | 28 | // NewMockIIam creates a new mock instance. 29 | func NewMockIIam(ctrl *gomock.Controller) *MockIIam { 30 | mock := &MockIIam{ctrl: ctrl} 31 | mock.recorder = &MockIIamMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockIIam) EXPECT() *MockIIamMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // CheckGroupExists mocks base method. 41 | func (m *MockIIam) CheckGroupExists(ctx context.Context, groupName *string) (bool, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "CheckGroupExists", ctx, groupName) 44 | ret0, _ := ret[0].(bool) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // CheckGroupExists indicates an expected call of CheckGroupExists. 50 | func (mr *MockIIamMockRecorder) CheckGroupExists(ctx, groupName any) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckGroupExists", reflect.TypeOf((*MockIIam)(nil).CheckGroupExists), ctx, groupName) 53 | } 54 | 55 | // DeleteGroup mocks base method. 56 | func (m *MockIIam) DeleteGroup(ctx context.Context, groupName *string) error { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "DeleteGroup", ctx, groupName) 59 | ret0, _ := ret[0].(error) 60 | return ret0 61 | } 62 | 63 | // DeleteGroup indicates an expected call of DeleteGroup. 64 | func (mr *MockIIamMockRecorder) DeleteGroup(ctx, groupName any) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGroup", reflect.TypeOf((*MockIIam)(nil).DeleteGroup), ctx, groupName) 67 | } 68 | 69 | // GetGroupUsers mocks base method. 70 | func (m *MockIIam) GetGroupUsers(ctx context.Context, groupName *string) ([]types.User, error) { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "GetGroupUsers", ctx, groupName) 73 | ret0, _ := ret[0].([]types.User) 74 | ret1, _ := ret[1].(error) 75 | return ret0, ret1 76 | } 77 | 78 | // GetGroupUsers indicates an expected call of GetGroupUsers. 79 | func (mr *MockIIamMockRecorder) GetGroupUsers(ctx, groupName any) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupUsers", reflect.TypeOf((*MockIIam)(nil).GetGroupUsers), ctx, groupName) 82 | } 83 | 84 | // RemoveUsersFromGroup mocks base method. 85 | func (m *MockIIam) RemoveUsersFromGroup(ctx context.Context, groupName *string, users []types.User) error { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "RemoveUsersFromGroup", ctx, groupName, users) 88 | ret0, _ := ret[0].(error) 89 | return ret0 90 | } 91 | 92 | // RemoveUsersFromGroup indicates an expected call of RemoveUsersFromGroup. 93 | func (mr *MockIIamMockRecorder) RemoveUsersFromGroup(ctx, groupName, users any) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUsersFromGroup", reflect.TypeOf((*MockIIam)(nil).RemoveUsersFromGroup), ctx, groupName, users) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/client/main_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.uber.org/goleak" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | fmt.Println() 12 | fmt.Println("==========================================") 13 | fmt.Println("========== Start Test: client ============") 14 | fmt.Println("==========================================") 15 | goleak.VerifyTestMain(m) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/client/retryer.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "math/rand/v2" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | ) 10 | 11 | const MaxRetryCount = 10 12 | 13 | var _ aws.RetryerV2 = (*Retryer)(nil) 14 | 15 | type Retryer struct { 16 | isErrorRetryableFunc func(error) bool 17 | delayTimeSec int 18 | } 19 | 20 | func NewRetryer(isErrorRetryableFunc func(error) bool, delayTimeSec int) *Retryer { 21 | return &Retryer{ 22 | isErrorRetryableFunc: isErrorRetryableFunc, 23 | delayTimeSec: delayTimeSec, 24 | } 25 | } 26 | 27 | func (r *Retryer) IsErrorRetryable(err error) bool { 28 | return r.isErrorRetryableFunc(err) 29 | } 30 | 31 | func (r *Retryer) MaxAttempts() int { 32 | return MaxRetryCount 33 | } 34 | 35 | func (r *Retryer) RetryDelay(int, error) (time.Duration, error) { 36 | waitTime := 1 37 | if r.delayTimeSec > 1 { 38 | //nolint:gosec 39 | waitTime += rand.IntN(r.delayTimeSec) 40 | } 41 | return time.Duration(waitTime) * time.Second, nil 42 | } 43 | 44 | func (r *Retryer) GetRetryToken(context.Context, error) (func(error) error, error) { 45 | return func(error) error { return nil }, nil 46 | } 47 | 48 | func (r *Retryer) GetInitialToken() func(error) error { 49 | return func(error) error { return nil } 50 | } 51 | 52 | func (r *Retryer) GetAttemptToken(context.Context) (func(error) error, error) { 53 | return func(error) error { return nil }, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/client/s3.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=s3_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "strings" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/s3" 11 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 12 | ) 13 | 14 | var SleepTimeSecForS3 = 10 15 | 16 | type ListObjectsOrVersionsByPageOutput struct { 17 | ObjectIdentifiers []types.ObjectIdentifier 18 | NextKeyMarker *string 19 | NextVersionIdMarker *string 20 | } 21 | type listObjectVersionsByPageOutput struct { 22 | ObjectIdentifiers []types.ObjectIdentifier 23 | NextKeyMarker *string 24 | NextVersionIdMarker *string 25 | } 26 | type listObjectsByPageOutput struct { 27 | ObjectIdentifiers []types.ObjectIdentifier 28 | NextToken *string 29 | } 30 | 31 | type IS3 interface { 32 | DeleteBucket(ctx context.Context, bucketName *string) error 33 | DeleteObjects(ctx context.Context, bucketName *string, objects []types.ObjectIdentifier) ([]types.Error, error) 34 | ListObjectsOrVersionsByPage( 35 | ctx context.Context, 36 | bucketName *string, 37 | keyMarker *string, 38 | versionIdMarker *string, 39 | ) (*ListObjectsOrVersionsByPageOutput, error) 40 | CheckBucketExists(ctx context.Context, bucketName *string) (bool, error) 41 | GetDirectoryBucketsFlag() bool 42 | } 43 | 44 | var _ IS3 = (*S3)(nil) 45 | 46 | type S3 struct { 47 | client *s3.Client 48 | directoryBucketsFlag bool 49 | retryer *Retryer 50 | } 51 | 52 | func NewS3(client *s3.Client, directoryBucketsFlag bool) *S3 { 53 | retryable := func(err error) bool { 54 | // See: https://github.com/go-to-k/delstack/issues/373 55 | return strings.Contains(err.Error(), "api error SlowDown") || 56 | strings.Contains(err.Error(), "https response error StatusCode: 0") || 57 | strings.Contains(err.Error(), "EOF") || 58 | strings.Contains(err.Error(), "Please try again") || 59 | strings.Contains(err.Error(), "connection reset by peer") 60 | } 61 | retryer := NewRetryer(retryable, SleepTimeSecForS3) 62 | 63 | return &S3{ 64 | client, 65 | directoryBucketsFlag, 66 | retryer, 67 | } 68 | } 69 | 70 | func (s *S3) DeleteBucket(ctx context.Context, bucketName *string) error { 71 | input := &s3.DeleteBucketInput{ 72 | Bucket: bucketName, 73 | } 74 | 75 | optFn := func(o *s3.Options) { 76 | o.Retryer = s.retryer 77 | } 78 | _, err := s.client.DeleteBucket(ctx, input, optFn) 79 | if err != nil { 80 | return &ClientError{ 81 | ResourceName: bucketName, 82 | Err: err, 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func (s *S3) DeleteObjects( 89 | ctx context.Context, 90 | bucketName *string, 91 | objects []types.ObjectIdentifier, 92 | ) ([]types.Error, error) { 93 | errors := []types.Error{} 94 | retryCounts := 0 95 | 96 | // Assuming that the number of objects received as an argument does not 97 | // exceed 1000, so no slice splitting and validation whether exceeds 98 | // 1000 or not are good. 99 | for len(objects) > 0 { 100 | input := &s3.DeleteObjectsInput{ 101 | Bucket: bucketName, 102 | Delete: &types.Delete{ 103 | Objects: objects, 104 | Quiet: aws.Bool(true), 105 | }, 106 | } 107 | 108 | optFn := func(o *s3.Options) { 109 | o.Retryer = s.retryer 110 | } 111 | output, err := s.client.DeleteObjects(ctx, input, optFn) 112 | if err != nil { 113 | return []types.Error{}, &ClientError{ 114 | ResourceName: bucketName, 115 | Err: err, 116 | } 117 | } 118 | 119 | if len(output.Errors) == 0 { 120 | break 121 | } 122 | 123 | retryCounts++ 124 | 125 | if retryCounts > s.retryer.MaxAttempts() { 126 | errors = append(errors, output.Errors...) 127 | break 128 | } 129 | 130 | objects = []types.ObjectIdentifier{} 131 | for _, err := range output.Errors { 132 | // Error example: 133 | // Code: InternalError 134 | // Message: We encountered an internal error. Please try again. 135 | if strings.Contains(*err.Message, "Please try again") { 136 | objects = append(objects, types.ObjectIdentifier{ 137 | Key: err.Key, 138 | VersionId: err.VersionId, 139 | }) 140 | } else { 141 | errors = append(errors, err) 142 | } 143 | } 144 | // random sleep 145 | if len(objects) > 0 { 146 | sleepTime, _ := s.retryer.RetryDelay(0, nil) 147 | time.Sleep(sleepTime) 148 | } 149 | } 150 | 151 | return errors, nil 152 | } 153 | 154 | func (s *S3) ListObjectsOrVersionsByPage( 155 | ctx context.Context, 156 | bucketName *string, 157 | keyMarker *string, 158 | versionIdMarker *string, 159 | ) (*ListObjectsOrVersionsByPageOutput, error) { 160 | var objectIdentifiers []types.ObjectIdentifier 161 | var nextKeyMarker *string 162 | var nextVersionIdMarker *string 163 | 164 | if s.directoryBucketsFlag { 165 | output, err := s.listObjectsByPage(ctx, bucketName, keyMarker) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | objectIdentifiers = output.ObjectIdentifiers 171 | nextKeyMarker = output.NextToken 172 | } else { 173 | output, err := s.listObjectVersionsByPage(ctx, bucketName, keyMarker, versionIdMarker) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | objectIdentifiers = output.ObjectIdentifiers 179 | nextKeyMarker = output.NextKeyMarker 180 | nextVersionIdMarker = output.NextVersionIdMarker 181 | } 182 | 183 | return &ListObjectsOrVersionsByPageOutput{ 184 | ObjectIdentifiers: objectIdentifiers, 185 | NextKeyMarker: nextKeyMarker, 186 | NextVersionIdMarker: nextVersionIdMarker, 187 | }, nil 188 | } 189 | 190 | func (s *S3) listObjectVersionsByPage( 191 | ctx context.Context, 192 | bucketName *string, 193 | keyMarker *string, 194 | versionIdMarker *string, 195 | ) (*listObjectVersionsByPageOutput, error) { 196 | objectIdentifiers := []types.ObjectIdentifier{} 197 | input := &s3.ListObjectVersionsInput{ 198 | Bucket: bucketName, 199 | KeyMarker: keyMarker, 200 | VersionIdMarker: versionIdMarker, 201 | } 202 | 203 | optFn := func(o *s3.Options) { 204 | o.Retryer = s.retryer 205 | } 206 | output, err := s.client.ListObjectVersions(ctx, input, optFn) 207 | if err != nil { 208 | return nil, &ClientError{ 209 | ResourceName: bucketName, 210 | Err: err, 211 | } 212 | } 213 | 214 | for _, version := range output.Versions { 215 | objectIdentifier := types.ObjectIdentifier{ 216 | Key: version.Key, 217 | VersionId: version.VersionId, 218 | } 219 | objectIdentifiers = append(objectIdentifiers, objectIdentifier) 220 | } 221 | 222 | for _, deleteMarker := range output.DeleteMarkers { 223 | objectIdentifier := types.ObjectIdentifier{ 224 | Key: deleteMarker.Key, 225 | VersionId: deleteMarker.VersionId, 226 | } 227 | objectIdentifiers = append(objectIdentifiers, objectIdentifier) 228 | } 229 | 230 | return &listObjectVersionsByPageOutput{ 231 | ObjectIdentifiers: objectIdentifiers, 232 | NextKeyMarker: output.NextKeyMarker, 233 | NextVersionIdMarker: output.NextVersionIdMarker, 234 | }, nil 235 | } 236 | 237 | func (s *S3) listObjectsByPage( 238 | ctx context.Context, 239 | bucketName *string, 240 | token *string, 241 | ) (*listObjectsByPageOutput, error) { 242 | objectIdentifiers := []types.ObjectIdentifier{} 243 | input := &s3.ListObjectsV2Input{ 244 | Bucket: bucketName, 245 | ContinuationToken: token, 246 | } 247 | 248 | optFn := func(o *s3.Options) { 249 | o.Retryer = s.retryer 250 | } 251 | 252 | output, err := s.client.ListObjectsV2(ctx, input, optFn) 253 | if err != nil { 254 | return nil, &ClientError{ 255 | ResourceName: bucketName, 256 | Err: err, 257 | } 258 | } 259 | 260 | for _, object := range output.Contents { 261 | objectIdentifier := types.ObjectIdentifier{ 262 | Key: object.Key, 263 | } 264 | objectIdentifiers = append(objectIdentifiers, objectIdentifier) 265 | } 266 | return &listObjectsByPageOutput{ 267 | ObjectIdentifiers: objectIdentifiers, 268 | NextToken: output.NextContinuationToken, 269 | }, nil 270 | } 271 | 272 | func (s *S3) CheckBucketExists(ctx context.Context, bucketName *string) (bool, error) { 273 | var listBucketsFunc func(ctx context.Context) ([]types.Bucket, error) 274 | if s.directoryBucketsFlag { 275 | listBucketsFunc = s.listDirectoryBuckets 276 | } else { 277 | listBucketsFunc = s.listBuckets 278 | } 279 | 280 | buckets, err := listBucketsFunc(ctx) 281 | if err != nil { 282 | return false, &ClientError{ 283 | ResourceName: bucketName, 284 | Err: err, 285 | } 286 | } 287 | 288 | for _, bucket := range buckets { 289 | if *bucket.Name == *bucketName { 290 | return true, nil 291 | } 292 | } 293 | 294 | return false, nil 295 | } 296 | 297 | func (s *S3) listBuckets(ctx context.Context) ([]types.Bucket, error) { 298 | input := &s3.ListBucketsInput{} 299 | 300 | optFn := func(o *s3.Options) { 301 | o.Retryer = s.retryer 302 | } 303 | 304 | output, err := s.client.ListBuckets(ctx, input, optFn) 305 | if err != nil { 306 | return []types.Bucket{}, err 307 | } 308 | 309 | return output.Buckets, nil 310 | } 311 | 312 | func (s *S3) listDirectoryBuckets(ctx context.Context) ([]types.Bucket, error) { 313 | buckets := []types.Bucket{} 314 | var continuationToken *string 315 | 316 | for { 317 | select { 318 | case <-ctx.Done(): 319 | return buckets, ctx.Err() 320 | default: 321 | } 322 | 323 | input := &s3.ListDirectoryBucketsInput{ 324 | ContinuationToken: continuationToken, 325 | } 326 | 327 | optFn := func(o *s3.Options) { 328 | o.Retryer = s.retryer 329 | } 330 | 331 | output, err := s.client.ListDirectoryBuckets(ctx, input, optFn) 332 | if err != nil { 333 | return buckets, err 334 | } 335 | 336 | buckets = append(buckets, output.Buckets...) 337 | 338 | if output.ContinuationToken == nil { 339 | break 340 | } 341 | continuationToken = output.ContinuationToken 342 | } 343 | 344 | return buckets, nil 345 | } 346 | 347 | func (s *S3) GetDirectoryBucketsFlag() bool { 348 | return s.directoryBucketsFlag 349 | } 350 | -------------------------------------------------------------------------------- /pkg/client/s3_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: s3.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=s3.go -destination=s3_mock.go -package=client -write_package_comment=false 7 | package client 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | types "github.com/aws/aws-sdk-go-v2/service/s3/types" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockIS3 is a mock of IS3 interface. 18 | type MockIS3 struct { 19 | ctrl *gomock.Controller 20 | recorder *MockIS3MockRecorder 21 | } 22 | 23 | // MockIS3MockRecorder is the mock recorder for MockIS3. 24 | type MockIS3MockRecorder struct { 25 | mock *MockIS3 26 | } 27 | 28 | // NewMockIS3 creates a new mock instance. 29 | func NewMockIS3(ctrl *gomock.Controller) *MockIS3 { 30 | mock := &MockIS3{ctrl: ctrl} 31 | mock.recorder = &MockIS3MockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockIS3) EXPECT() *MockIS3MockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // CheckBucketExists mocks base method. 41 | func (m *MockIS3) CheckBucketExists(ctx context.Context, bucketName *string) (bool, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "CheckBucketExists", ctx, bucketName) 44 | ret0, _ := ret[0].(bool) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // CheckBucketExists indicates an expected call of CheckBucketExists. 50 | func (mr *MockIS3MockRecorder) CheckBucketExists(ctx, bucketName any) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckBucketExists", reflect.TypeOf((*MockIS3)(nil).CheckBucketExists), ctx, bucketName) 53 | } 54 | 55 | // DeleteBucket mocks base method. 56 | func (m *MockIS3) DeleteBucket(ctx context.Context, bucketName *string) error { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "DeleteBucket", ctx, bucketName) 59 | ret0, _ := ret[0].(error) 60 | return ret0 61 | } 62 | 63 | // DeleteBucket indicates an expected call of DeleteBucket. 64 | func (mr *MockIS3MockRecorder) DeleteBucket(ctx, bucketName any) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucket", reflect.TypeOf((*MockIS3)(nil).DeleteBucket), ctx, bucketName) 67 | } 68 | 69 | // DeleteObjects mocks base method. 70 | func (m *MockIS3) DeleteObjects(ctx context.Context, bucketName *string, objects []types.ObjectIdentifier) ([]types.Error, error) { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "DeleteObjects", ctx, bucketName, objects) 73 | ret0, _ := ret[0].([]types.Error) 74 | ret1, _ := ret[1].(error) 75 | return ret0, ret1 76 | } 77 | 78 | // DeleteObjects indicates an expected call of DeleteObjects. 79 | func (mr *MockIS3MockRecorder) DeleteObjects(ctx, bucketName, objects any) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjects", reflect.TypeOf((*MockIS3)(nil).DeleteObjects), ctx, bucketName, objects) 82 | } 83 | 84 | // GetDirectoryBucketsFlag mocks base method. 85 | func (m *MockIS3) GetDirectoryBucketsFlag() bool { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "GetDirectoryBucketsFlag") 88 | ret0, _ := ret[0].(bool) 89 | return ret0 90 | } 91 | 92 | // GetDirectoryBucketsFlag indicates an expected call of GetDirectoryBucketsFlag. 93 | func (mr *MockIS3MockRecorder) GetDirectoryBucketsFlag() *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectoryBucketsFlag", reflect.TypeOf((*MockIS3)(nil).GetDirectoryBucketsFlag)) 96 | } 97 | 98 | // ListObjectsOrVersionsByPage mocks base method. 99 | func (m *MockIS3) ListObjectsOrVersionsByPage(ctx context.Context, bucketName, keyMarker, versionIdMarker *string) (*ListObjectsOrVersionsByPageOutput, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "ListObjectsOrVersionsByPage", ctx, bucketName, keyMarker, versionIdMarker) 102 | ret0, _ := ret[0].(*ListObjectsOrVersionsByPageOutput) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // ListObjectsOrVersionsByPage indicates an expected call of ListObjectsOrVersionsByPage. 108 | func (mr *MockIS3MockRecorder) ListObjectsOrVersionsByPage(ctx, bucketName, keyMarker, versionIdMarker any) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsOrVersionsByPage", reflect.TypeOf((*MockIS3)(nil).ListObjectsOrVersionsByPage), ctx, bucketName, keyMarker, versionIdMarker) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/client/s3_tables.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=s3_tables_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/s3tables" 10 | "github.com/aws/aws-sdk-go-v2/service/s3tables/types" 11 | ) 12 | 13 | var SleepTimeSecForS3Tables = 3 // NOTE: Because S3Tables is a serial operation, a low value is OK. 14 | 15 | type ListNamespacesByPageOutput struct { 16 | Namespaces []types.NamespaceSummary 17 | ContinuationToken *string 18 | } 19 | 20 | type ListTablesByPageOutput struct { 21 | Tables []types.TableSummary 22 | ContinuationToken *string 23 | } 24 | 25 | type IS3Tables interface { 26 | DeleteTableBucket(ctx context.Context, tableBucketARN *string) error 27 | DeleteNamespace(ctx context.Context, namespace *string, tableBucketARN *string) error 28 | DeleteTable(ctx context.Context, tableName *string, namespace *string, tableBucketARN *string) error 29 | ListNamespacesByPage(ctx context.Context, tableBucketARN *string, continuationToken *string) (*ListNamespacesByPageOutput, error) 30 | ListTablesByPage(ctx context.Context, tableBucketARN *string, namespace *string, continuationToken *string) (*ListTablesByPageOutput, error) 31 | CheckTableBucketExists(ctx context.Context, tableBucketARN *string) (bool, error) 32 | } 33 | 34 | var _ IS3Tables = (*S3Tables)(nil) 35 | 36 | type S3Tables struct { 37 | client *s3tables.Client 38 | retryer *Retryer 39 | } 40 | 41 | func NewS3Tables(client *s3tables.Client) *S3Tables { 42 | retryable := func(err error) bool { 43 | isRetryable := 44 | strings.Contains(err.Error(), "api error SlowDown") || 45 | strings.Contains(err.Error(), "An internal error occurred. Try again.") || 46 | strings.Contains(err.Error(), "StatusCode: 429") || 47 | // I haven't encountered this error yet, but I got this error on S3, so I'll add it here too, just in case. 48 | strings.Contains(err.Error(), "Please try again") 49 | 50 | return isRetryable 51 | } 52 | retryer := NewRetryer(retryable, SleepTimeSecForS3Tables) 53 | 54 | return &S3Tables{ 55 | client, 56 | retryer, 57 | } 58 | } 59 | 60 | func (s *S3Tables) DeleteTableBucket(ctx context.Context, tableBucketARN *string) error { 61 | input := &s3tables.DeleteTableBucketInput{ 62 | TableBucketARN: tableBucketARN, 63 | } 64 | 65 | optFn := func(o *s3tables.Options) { 66 | o.Retryer = s.retryer 67 | } 68 | 69 | _, err := s.client.DeleteTableBucket(ctx, input, optFn) 70 | if err != nil { 71 | return &ClientError{ 72 | ResourceName: tableBucketARN, 73 | Err: err, 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func (s *S3Tables) DeleteNamespace(ctx context.Context, namespace *string, tableBucketARN *string) error { 80 | input := &s3tables.DeleteNamespaceInput{ 81 | Namespace: namespace, 82 | TableBucketARN: tableBucketARN, 83 | } 84 | 85 | optFn := func(o *s3tables.Options) { 86 | o.Retryer = s.retryer 87 | } 88 | 89 | _, err := s.client.DeleteNamespace(ctx, input, optFn) 90 | if err != nil { 91 | return &ClientError{ 92 | ResourceName: aws.String(*tableBucketARN + "/" + *namespace), 93 | Err: err, 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | func (s *S3Tables) DeleteTable(ctx context.Context, tableName *string, namespace *string, tableBucketARN *string) error { 100 | input := &s3tables.DeleteTableInput{ 101 | Name: tableName, 102 | Namespace: namespace, 103 | TableBucketARN: tableBucketARN, 104 | } 105 | 106 | optFn := func(o *s3tables.Options) { 107 | o.Retryer = s.retryer 108 | } 109 | 110 | _, err := s.client.DeleteTable(ctx, input, optFn) 111 | if err != nil { 112 | return &ClientError{ 113 | ResourceName: aws.String(*tableBucketARN + "/" + *namespace + "/" + *tableName), 114 | Err: err, 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | func (s *S3Tables) ListNamespacesByPage(ctx context.Context, tableBucketARN *string, continuationToken *string) (*ListNamespacesByPageOutput, error) { 121 | namespaces := []types.NamespaceSummary{} 122 | 123 | input := &s3tables.ListNamespacesInput{ 124 | TableBucketARN: tableBucketARN, 125 | ContinuationToken: continuationToken, 126 | } 127 | 128 | optFn := func(o *s3tables.Options) { 129 | o.Retryer = s.retryer 130 | } 131 | 132 | output, err := s.client.ListNamespaces(ctx, input, optFn) 133 | if err != nil { 134 | return nil, &ClientError{ 135 | ResourceName: tableBucketARN, 136 | Err: err, 137 | } 138 | } 139 | 140 | namespaces = append(namespaces, output.Namespaces...) 141 | 142 | return &ListNamespacesByPageOutput{ 143 | Namespaces: namespaces, 144 | ContinuationToken: output.ContinuationToken, 145 | }, nil 146 | } 147 | 148 | func (s *S3Tables) ListTablesByPage(ctx context.Context, tableBucketARN *string, namespace *string, continuationToken *string) (*ListTablesByPageOutput, error) { 149 | tables := []types.TableSummary{} 150 | 151 | input := &s3tables.ListTablesInput{ 152 | Namespace: namespace, 153 | TableBucketARN: tableBucketARN, 154 | ContinuationToken: continuationToken, 155 | } 156 | 157 | optFn := func(o *s3tables.Options) { 158 | o.Retryer = s.retryer 159 | } 160 | 161 | output, err := s.client.ListTables(ctx, input, optFn) 162 | if err != nil { 163 | return nil, &ClientError{ 164 | ResourceName: aws.String(*tableBucketARN + "/" + *namespace), 165 | Err: err, 166 | } 167 | } 168 | 169 | tables = append(tables, output.Tables...) 170 | 171 | return &ListTablesByPageOutput{ 172 | Tables: tables, 173 | ContinuationToken: output.ContinuationToken, 174 | }, nil 175 | } 176 | 177 | func (s *S3Tables) CheckTableBucketExists(ctx context.Context, tableBucketARN *string) (bool, error) { 178 | tableBuckets, err := s.listTableBuckets(ctx) 179 | if err != nil { 180 | return false, &ClientError{ 181 | ResourceName: tableBucketARN, 182 | Err: err, 183 | } 184 | } 185 | 186 | for _, tableBucket := range tableBuckets { 187 | if *tableBucket.Arn == *tableBucketARN { 188 | return true, nil 189 | } 190 | } 191 | 192 | return false, nil 193 | } 194 | 195 | func (s *S3Tables) listTableBuckets(ctx context.Context) ([]types.TableBucketSummary, error) { 196 | buckets := []types.TableBucketSummary{} 197 | var continuationToken *string 198 | 199 | for { 200 | select { 201 | case <-ctx.Done(): 202 | return buckets, ctx.Err() 203 | default: 204 | } 205 | 206 | input := &s3tables.ListTableBucketsInput{ 207 | ContinuationToken: continuationToken, 208 | } 209 | 210 | optFn := func(o *s3tables.Options) { 211 | o.Retryer = s.retryer 212 | } 213 | 214 | output, err := s.client.ListTableBuckets(ctx, input, optFn) 215 | if err != nil { 216 | return buckets, err 217 | } 218 | 219 | buckets = append(buckets, output.TableBuckets...) 220 | 221 | if output.ContinuationToken == nil { 222 | break 223 | } 224 | continuationToken = output.ContinuationToken 225 | } 226 | 227 | return buckets, nil 228 | } 229 | -------------------------------------------------------------------------------- /pkg/client/s3_tables_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: s3_tables.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=s3_tables.go -destination=s3_tables_mock.go -package=client -write_package_comment=false 7 | package client 8 | 9 | import ( 10 | context "context" 11 | reflect "reflect" 12 | 13 | gomock "go.uber.org/mock/gomock" 14 | ) 15 | 16 | // MockIS3Tables is a mock of IS3Tables interface. 17 | type MockIS3Tables struct { 18 | ctrl *gomock.Controller 19 | recorder *MockIS3TablesMockRecorder 20 | } 21 | 22 | // MockIS3TablesMockRecorder is the mock recorder for MockIS3Tables. 23 | type MockIS3TablesMockRecorder struct { 24 | mock *MockIS3Tables 25 | } 26 | 27 | // NewMockIS3Tables creates a new mock instance. 28 | func NewMockIS3Tables(ctrl *gomock.Controller) *MockIS3Tables { 29 | mock := &MockIS3Tables{ctrl: ctrl} 30 | mock.recorder = &MockIS3TablesMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockIS3Tables) EXPECT() *MockIS3TablesMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // CheckTableBucketExists mocks base method. 40 | func (m *MockIS3Tables) CheckTableBucketExists(ctx context.Context, tableBucketARN *string) (bool, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "CheckTableBucketExists", ctx, tableBucketARN) 43 | ret0, _ := ret[0].(bool) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // CheckTableBucketExists indicates an expected call of CheckTableBucketExists. 49 | func (mr *MockIS3TablesMockRecorder) CheckTableBucketExists(ctx, tableBucketARN any) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckTableBucketExists", reflect.TypeOf((*MockIS3Tables)(nil).CheckTableBucketExists), ctx, tableBucketARN) 52 | } 53 | 54 | // DeleteNamespace mocks base method. 55 | func (m *MockIS3Tables) DeleteNamespace(ctx context.Context, namespace, tableBucketARN *string) error { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "DeleteNamespace", ctx, namespace, tableBucketARN) 58 | ret0, _ := ret[0].(error) 59 | return ret0 60 | } 61 | 62 | // DeleteNamespace indicates an expected call of DeleteNamespace. 63 | func (mr *MockIS3TablesMockRecorder) DeleteNamespace(ctx, namespace, tableBucketARN any) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNamespace", reflect.TypeOf((*MockIS3Tables)(nil).DeleteNamespace), ctx, namespace, tableBucketARN) 66 | } 67 | 68 | // DeleteTable mocks base method. 69 | func (m *MockIS3Tables) DeleteTable(ctx context.Context, tableName, namespace, tableBucketARN *string) error { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "DeleteTable", ctx, tableName, namespace, tableBucketARN) 72 | ret0, _ := ret[0].(error) 73 | return ret0 74 | } 75 | 76 | // DeleteTable indicates an expected call of DeleteTable. 77 | func (mr *MockIS3TablesMockRecorder) DeleteTable(ctx, tableName, namespace, tableBucketARN any) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTable", reflect.TypeOf((*MockIS3Tables)(nil).DeleteTable), ctx, tableName, namespace, tableBucketARN) 80 | } 81 | 82 | // DeleteTableBucket mocks base method. 83 | func (m *MockIS3Tables) DeleteTableBucket(ctx context.Context, tableBucketARN *string) error { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "DeleteTableBucket", ctx, tableBucketARN) 86 | ret0, _ := ret[0].(error) 87 | return ret0 88 | } 89 | 90 | // DeleteTableBucket indicates an expected call of DeleteTableBucket. 91 | func (mr *MockIS3TablesMockRecorder) DeleteTableBucket(ctx, tableBucketARN any) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTableBucket", reflect.TypeOf((*MockIS3Tables)(nil).DeleteTableBucket), ctx, tableBucketARN) 94 | } 95 | 96 | // ListNamespacesByPage mocks base method. 97 | func (m *MockIS3Tables) ListNamespacesByPage(ctx context.Context, tableBucketARN, continuationToken *string) (*ListNamespacesByPageOutput, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "ListNamespacesByPage", ctx, tableBucketARN, continuationToken) 100 | ret0, _ := ret[0].(*ListNamespacesByPageOutput) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // ListNamespacesByPage indicates an expected call of ListNamespacesByPage. 106 | func (mr *MockIS3TablesMockRecorder) ListNamespacesByPage(ctx, tableBucketARN, continuationToken any) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNamespacesByPage", reflect.TypeOf((*MockIS3Tables)(nil).ListNamespacesByPage), ctx, tableBucketARN, continuationToken) 109 | } 110 | 111 | // ListTablesByPage mocks base method. 112 | func (m *MockIS3Tables) ListTablesByPage(ctx context.Context, tableBucketARN, namespace, continuationToken *string) (*ListTablesByPageOutput, error) { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "ListTablesByPage", ctx, tableBucketARN, namespace, continuationToken) 115 | ret0, _ := ret[0].(*ListTablesByPageOutput) 116 | ret1, _ := ret[1].(error) 117 | return ret0, ret1 118 | } 119 | 120 | // ListTablesByPage indicates an expected call of ListTablesByPage. 121 | func (mr *MockIS3TablesMockRecorder) ListTablesByPage(ctx, tableBucketARN, namespace, continuationToken any) *gomock.Call { 122 | mr.mock.ctrl.T.Helper() 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTablesByPage", reflect.TypeOf((*MockIS3Tables)(nil).ListTablesByPage), ctx, tableBucketARN, namespace, continuationToken) 124 | } 125 | -------------------------------------------------------------------------------- /testdata/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | # Delstack Test Environment 2 | 3 | This directory contains a tool for creating a test environment for `delstack`. This tool (`deploy.go`) deploys AWS CloudFormation stacks with various resources that can be used to test the stack deletion functionality of `delstack`. 4 | 5 | ## Test Stack Deployment 6 | 7 | You can deploy test CloudFormation stacks using the included `deploy.go` script **with AWS CDK for Go**. So you need to install AWS CDK. 8 | 9 | ```bash 10 | npm install -g aws-cdk@latest 11 | ``` 12 | 13 | This script creates a CloudFormation stack containing various resources that typically cause deletion issues, including: 14 | 15 | - S3 buckets with contents 16 | - S3 Express Directory buckets with contents 17 | - S3 Table buckets with namespaces and tables 18 | - IAM groups with users 19 | - ECR repositories with images 20 | - AWS Backup vaults with recovery points 21 | - And more 22 | 23 | ```bash 24 | go run testdata/deploy.go -s [-p ] 25 | ``` 26 | 27 | ### Options 28 | 29 | - `-s ` : Stage name, used as part of stack naming (optional) 30 | - `-p ` : AWS CLI profile name to use (optional) 31 | - `-r` : Make all resources RETAIN to test `-f` option for delstack (optional) 32 | 33 | ### Using the Makefile 34 | 35 | For convenience, you can also use the Makefile target: 36 | 37 | ```bash 38 | # Deploy with default stage and profile 39 | make testgen 40 | 41 | # Deploy with custom stage and profile 42 | make testgen OPT="-s my-stage -p my-profile" 43 | 44 | # Deploy the stack with all RETAIN resources 45 | make testgen OPT="-r" 46 | ``` 47 | 48 | ### Notes 49 | 50 | - Due to AWS quota limitations, only up to 5 test stacks can be created simultaneously with this script. 51 | - The script includes 2 `AWS::IAM::Group` resources only; one IAM user (`DelstackTestUser`) can only belong to 10 IAM groups, and we want to be able to make up to 5 stacks. 52 | - The script includes 2 `AWS::S3Tables::TableBucket` resources; an AWS account can have at most 10 table buckets per region. 53 | -------------------------------------------------------------------------------- /testdata/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # go.sum should be committed 15 | !go.sum 16 | 17 | # CDK asset staging directory 18 | .cdk.staging 19 | cdk.out 20 | 21 | # This directory is only used for testing purposes. 22 | cdk.context.json 23 | -------------------------------------------------------------------------------- /testdata/cdk/cdk.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | "github.com/aws/jsii-runtime-go" 9 | 10 | "cdk/lib/nest" 11 | "cdk/lib/resource" 12 | ) 13 | 14 | type TestStackProps struct { 15 | awscdk.StackProps 16 | PjPrefix string 17 | IsRetain bool 18 | } 19 | 20 | func NewTestStack(scope constructs.Construct, id string, props *TestStackProps) awscdk.Stack { 21 | var sprops awscdk.StackProps 22 | if props != nil { 23 | sprops = props.StackProps 24 | } 25 | stack := awscdk.NewStack(scope, &id, &sprops) 26 | 27 | if props.IsRetain { 28 | awscdk.RemovalPolicies_Of(scope).Retain(nil) 29 | } 30 | 31 | resource.NewEcr(stack) 32 | resource.NewS3Bucket(stack) 33 | resource.NewS3DirectoryBucket(stack, props.PjPrefix+"-Root") 34 | resource.NewS3TableBucket(stack, props.PjPrefix+"-Root") // can only contain [2 AWS::S3Tables::TableBucket] : Table bucket can only have up to 10 buckets created per AWS account (per region), and we want to be able to make up to 5 stacks 35 | resource.NewIamGroup(stack) // can only contain [2 AWS::IAM::Group] in this CDK app: 1 IAM user (DelstackTestUser) can only belong to 10 IAM groups, and we want to be able to make up to 5 stacks 36 | resource.NewCustomResources(stack) 37 | resource.NewDynamoDB(stack, props.PjPrefix+"-Root") 38 | resource.NewBackup(stack, props.PjPrefix+"-Root") 39 | 40 | nest.NewChildStack(stack, "Child", &nest.ChildStackProps{ 41 | PjPrefix: props.PjPrefix, 42 | }) 43 | nest.NewChildStack2(stack, "ChildTwo", &nest.ChildStack2Props{ 44 | PjPrefix: props.PjPrefix, 45 | }) 46 | 47 | return stack 48 | } 49 | 50 | func main() { 51 | defer jsii.Close() 52 | 53 | app := awscdk.NewApp(nil) 54 | 55 | pjPrefix := app.Node().TryGetContext(jsii.String("PJ_PREFIX")).(string) 56 | if pjPrefix == "" { 57 | pjPrefix = "delstack" 58 | } 59 | 60 | retainMode := app.Node().TryGetContext(jsii.String("RETAIN_MODE")).(string) 61 | var isRetain bool 62 | if retainMode == "true" { 63 | isRetain = true 64 | } 65 | 66 | stackName := pjPrefix + "-Test-Stack" 67 | 68 | NewTestStack(app, stackName, &TestStackProps{ 69 | StackProps: awscdk.StackProps{ 70 | Env: env(), 71 | StackName: jsii.String(stackName), 72 | }, 73 | PjPrefix: pjPrefix, 74 | IsRetain: isRetain, 75 | }) 76 | 77 | app.Synth(nil) 78 | } 79 | 80 | func env() *awscdk.Environment { 81 | account := os.Getenv("CDK_DEFAULT_ACCOUNT") 82 | region := os.Getenv("CDK_DEFAULT_REGION") 83 | 84 | if region == "" { 85 | region = "us-east-1" 86 | } 87 | 88 | return &awscdk.Environment{ 89 | Account: jsii.String(account), 90 | Region: jsii.String(region), 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /testdata/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "go mod download && go run cdk.go", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "go.mod", 11 | "go.sum", 12 | "**/*test.go" 13 | ] 14 | }, 15 | "context": { 16 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 17 | "@aws-cdk/core:checkSecretUsage": true, 18 | "@aws-cdk/core:target-partitions": [ 19 | "aws", 20 | "aws-cn" 21 | ], 22 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 23 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 24 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 25 | "@aws-cdk/aws-iam:minimizePolicies": true, 26 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 27 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 28 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 29 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 30 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 31 | "@aws-cdk/core:enablePartitionLiterals": true, 32 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 35 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 36 | "@aws-cdk/aws-route53-patters:useCertificate": true, 37 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 38 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 39 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 40 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 41 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 42 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 43 | "@aws-cdk/aws-redshift:columnId": true, 44 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 45 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 46 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 47 | "@aws-cdk/aws-kms:aliasNameRef": true, 48 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 49 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 50 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 51 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 52 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 53 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 54 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 55 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 56 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 57 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 58 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 59 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 60 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 61 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 62 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 63 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 64 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 65 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 66 | "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, 67 | "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, 68 | "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, 69 | "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, 70 | "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, 71 | "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, 72 | "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, 73 | "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, 74 | "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, 75 | "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, 76 | "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, 77 | "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, 78 | "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, 79 | "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, 80 | "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, 81 | "@aws-cdk/core:enableAdditionalMetadataCollection": true, 82 | "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true, 83 | "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, 84 | "@aws-cdk/aws-events:requireEventBusPolicySid": true 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /testdata/cdk/go.mod: -------------------------------------------------------------------------------- 1 | module cdk 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/aws/aws-cdk-go/awscdk/v2 v2.189.0 7 | github.com/aws/constructs-go/constructs/v10 v10.4.2 8 | github.com/aws/jsii-runtime-go v1.110.0 9 | ) 10 | 11 | require ( 12 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 13 | github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.229 // indirect 14 | github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.1.0 // indirect 15 | github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v41 v41.0.0 // indirect 16 | github.com/fatih/color v1.18.0 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/yuin/goldmark v1.4.13 // indirect 20 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect 21 | golang.org/x/mod v0.24.0 // indirect 22 | golang.org/x/sync v0.12.0 // indirect 23 | golang.org/x/sys v0.31.0 // indirect 24 | golang.org/x/tools v0.31.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /testdata/cdk/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 2 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 3 | github.com/aws/aws-cdk-go/awscdk/v2 v2.189.0 h1:2OvdAbsslefC/vPrIphPwF2n3UFhSkCOnWBFPPKd7oQ= 4 | github.com/aws/aws-cdk-go/awscdk/v2 v2.189.0/go.mod h1:9ENCp/SkuTkIrAxG0cEdAD1QCC+QfpN82ukwrbwzwGE= 5 | github.com/aws/constructs-go/constructs/v10 v10.4.2 h1:+hDLTsFGLJmKIn0Dg20vWpKBrVnFrEWYgTEY5UiTEG8= 6 | github.com/aws/constructs-go/constructs/v10 v10.4.2/go.mod h1:cXsNCKDV+9eR9zYYfwy6QuE4uPFp6jsq6TtH1MwBx9w= 7 | github.com/aws/jsii-runtime-go v1.110.0 h1:w3//ecQLsS1WekahBhSLEnzgf+/7yGZTLU5rx/l/jUQ= 8 | github.com/aws/jsii-runtime-go v1.110.0/go.mod h1:eLDUEd0lRYsu2WoR+EoApYPz6ibG7JOaJgbL0IlD/m8= 9 | github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.229 h1:pwQ0ejIdyj0HHdUomZzEGpzi8zTE8NMr55gwBGom8Y4= 10 | github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.229/go.mod h1:oquOkMHjv3uVsjt8ToBdJ3/i0HLD3RPEzuQlTzaieek= 11 | github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.1.0 h1:kElXjprC8wkpJu58vp+WFH6z0AJw4zitg5iSKJPKe3c= 12 | github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.1.0/go.mod h1:JY4UnvNa1YDGQ4H5wohXTHl6YVY3uCDUWl4JYUrQfb8= 13 | github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v41 v41.0.0 h1:vUMERjQ8BFG4wW6DNW+sxF5fDCnAYOukvZEiRX4kRkw= 14 | github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v41 v41.0.0/go.mod h1:JNDQuA9sW21qkalkNLfhtii9NztdzL/lscAjDIKhbV0= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 18 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 19 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 20 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 21 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 22 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 23 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 24 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 25 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 29 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 30 | github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= 31 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 32 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 33 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 34 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= 35 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 36 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 37 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 38 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 39 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 40 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 41 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 43 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 49 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 50 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 51 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 52 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 53 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 54 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 56 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 57 | -------------------------------------------------------------------------------- /testdata/cdk/lib/nest/child_stack.go: -------------------------------------------------------------------------------- 1 | package nest 2 | 3 | import ( 4 | "cdk/lib/resource" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | ) 9 | 10 | type ChildStackProps struct { 11 | awscdk.NestedStackProps 12 | PjPrefix string 13 | } 14 | 15 | func NewChildStack(scope constructs.Construct, id string, props *ChildStackProps) awscdk.NestedStack { 16 | var sprops awscdk.NestedStackProps 17 | if props != nil { 18 | sprops = props.NestedStackProps 19 | } 20 | 21 | stack := awscdk.NewNestedStack(scope, &id, &sprops) 22 | 23 | resource.NewS3Bucket(stack) 24 | resource.NewS3DirectoryBucket(stack, props.PjPrefix+"-Child") 25 | resource.NewCustomResources(stack) 26 | 27 | NewDescendStack(stack, "Descend", &DescendStackProps{ 28 | PjPrefix: props.PjPrefix, 29 | }) 30 | NewDescendStack3(stack, "DescendThree", &DescendStack3Props{ 31 | PjPrefix: props.PjPrefix, 32 | }) 33 | 34 | return stack 35 | } 36 | -------------------------------------------------------------------------------- /testdata/cdk/lib/nest/child_stack2.go: -------------------------------------------------------------------------------- 1 | package nest 2 | 3 | import ( 4 | "cdk/lib/resource" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | ) 9 | 10 | type ChildStack2Props struct { 11 | awscdk.NestedStackProps 12 | PjPrefix string 13 | } 14 | 15 | func NewChildStack2(scope constructs.Construct, id string, props *ChildStack2Props) awscdk.NestedStack { 16 | var sprops awscdk.NestedStackProps 17 | if props != nil { 18 | sprops = props.NestedStackProps 19 | } 20 | 21 | stack := awscdk.NewNestedStack(scope, &id, &sprops) 22 | 23 | resource.NewS3Bucket(stack) 24 | resource.NewCustomResources(stack) 25 | 26 | NewDescendStack2(stack, "DescendTwo", &DescendStack2Props{ 27 | PjPrefix: props.PjPrefix, 28 | }) 29 | 30 | return stack 31 | } 32 | -------------------------------------------------------------------------------- /testdata/cdk/lib/nest/descend_stack.go: -------------------------------------------------------------------------------- 1 | package nest 2 | 3 | import ( 4 | "cdk/lib/resource" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | ) 9 | 10 | type DescendStackProps struct { 11 | awscdk.NestedStackProps 12 | PjPrefix string 13 | } 14 | 15 | func NewDescendStack(scope constructs.Construct, id string, props *DescendStackProps) awscdk.NestedStack { 16 | var sprops awscdk.NestedStackProps 17 | if props != nil { 18 | sprops = props.NestedStackProps 19 | } 20 | 21 | stack := awscdk.NewNestedStack(scope, &id, &sprops) 22 | 23 | resource.NewS3DirectoryBucket(stack, props.PjPrefix+"-Descend") 24 | resource.NewS3TableBucket(stack, props.PjPrefix+"-Descend") // can only contain [2 AWS::S3Tables::TableBucket] : Table bucket can only have up to 10 buckets created per AWS account (per region), and we want to be able to make up to 5 stacks 25 | resource.NewIamGroup(stack) // can only contain [2 AWS::IAM::Group] in this CDK app: 1 IAM user (DelstackTestUser) can only belong to 10 IAM groups, and we want to be able to make up to 5 stacks 26 | 27 | return stack 28 | } 29 | -------------------------------------------------------------------------------- /testdata/cdk/lib/nest/descend_stack2.go: -------------------------------------------------------------------------------- 1 | package nest 2 | 3 | import ( 4 | "cdk/lib/resource" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | ) 9 | 10 | type DescendStack2Props struct { 11 | awscdk.NestedStackProps 12 | PjPrefix string 13 | } 14 | 15 | func NewDescendStack2(scope constructs.Construct, id string, props *DescendStack2Props) awscdk.NestedStack { 16 | var sprops awscdk.NestedStackProps 17 | if props != nil { 18 | sprops = props.NestedStackProps 19 | } 20 | 21 | stack := awscdk.NewNestedStack(scope, &id, &sprops) 22 | 23 | resource.NewEcr(stack) 24 | 25 | return stack 26 | } 27 | -------------------------------------------------------------------------------- /testdata/cdk/lib/nest/descend_stack3.go: -------------------------------------------------------------------------------- 1 | package nest 2 | 3 | import ( 4 | "cdk/lib/resource" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | ) 9 | 10 | type DescendStack3Props struct { 11 | awscdk.NestedStackProps 12 | PjPrefix string 13 | } 14 | 15 | func NewDescendStack3(scope constructs.Construct, id string, props *DescendStack3Props) awscdk.NestedStack { 16 | var sprops awscdk.NestedStackProps 17 | if props != nil { 18 | sprops = props.NestedStackProps 19 | } 20 | 21 | stack := awscdk.NewNestedStack(scope, &id, &sprops) 22 | 23 | resource.NewCustomResources(stack) 24 | resource.NewDynamoDB(stack, props.PjPrefix+"-Descend") 25 | 26 | return stack 27 | } 28 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/backup.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aws/aws-cdk-go/awscdk/v2" 5 | "github.com/aws/aws-cdk-go/awscdk/v2/awsbackup" 6 | "github.com/aws/aws-cdk-go/awscdk/v2/awsiam" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | "github.com/aws/jsii-runtime-go" 9 | ) 10 | 11 | func NewBackup(scope constructs.Construct, resourcePrefix string) { 12 | backupRole := awsiam.NewRole(scope, jsii.String("AWSBackupServiceRole"), &awsiam.RoleProps{ 13 | RoleName: jsii.String(resourcePrefix + "-AWSBackupServiceRole"), 14 | Path: jsii.String("/service-role/"), 15 | AssumedBy: awsiam.NewServicePrincipal(jsii.String("backup.amazonaws.com"), nil), 16 | ManagedPolicies: &[]awsiam.IManagedPolicy{ 17 | awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("service-role/AWSBackupServiceRolePolicyForBackup")), 18 | awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("service-role/AWSBackupServiceRolePolicyForRestores")), 19 | awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AmazonDynamoDBReadOnlyAccess")), 20 | }, 21 | }) 22 | 23 | backupVault := awsbackup.NewBackupVault(scope, jsii.String("BackupVaultWithThinBackups"), &awsbackup.BackupVaultProps{ 24 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 25 | }) 26 | 27 | backupPlan := awsbackup.NewCfnBackupPlan(scope, jsii.String("BackupPlanWithThinBackups"), &awsbackup.CfnBackupPlanProps{ 28 | BackupPlan: &awsbackup.CfnBackupPlan_BackupPlanResourceTypeProperty{ 29 | BackupPlanName: jsii.String(resourcePrefix + "-Backup-Plan"), 30 | BackupPlanRule: &[]*awsbackup.CfnBackupPlan_BackupRuleResourceTypeProperty{ 31 | { 32 | RuleName: jsii.String("RuleForDailyBackups"), 33 | TargetBackupVault: backupVault.BackupVaultName(), 34 | ScheduleExpression: jsii.String("cron(0 18 * * ? *)"), 35 | StartWindowMinutes: jsii.Number(60), 36 | Lifecycle: &awsbackup.CfnBackupPlan_LifecycleResourceTypeProperty{ 37 | DeleteAfterDays: jsii.Number(3), 38 | }, 39 | }, 40 | }, 41 | }, 42 | }) 43 | 44 | selectionName := resourcePrefix + "-Backup-Selection" 45 | awsbackup.NewCfnBackupSelection(scope, jsii.String("TagBasedBackupSelection"), &awsbackup.CfnBackupSelectionProps{ 46 | BackupPlanId: backupPlan.AttrBackupPlanId(), 47 | BackupSelection: &awsbackup.CfnBackupSelection_BackupSelectionResourceTypeProperty{ 48 | SelectionName: jsii.String(selectionName), 49 | IamRoleArn: backupRole.RoleArn(), 50 | ListOfTags: &[]*awsbackup.CfnBackupSelection_ConditionResourceTypeProperty{ 51 | { 52 | ConditionType: jsii.String("STRINGEQUALS"), 53 | ConditionKey: jsii.String("Test"), 54 | ConditionValue: jsii.String("Test"), 55 | }, 56 | }, 57 | }, 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/custom_resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aws/aws-cdk-go/awscdk/v2" 5 | "github.com/aws/aws-cdk-go/awscdk/v2/awslambda" 6 | "github.com/aws/aws-cdk-go/awscdk/v2/awslogs" 7 | "github.com/aws/aws-cdk-go/awscdk/v2/customresources" 8 | "github.com/aws/constructs-go/constructs/v10" 9 | "github.com/aws/jsii-runtime-go" 10 | ) 11 | 12 | func NewCustomResources(scope constructs.Construct) { 13 | logGroup := awslogs.NewLogGroup(scope, jsii.String("LogGroup"), &awslogs.LogGroupProps{ 14 | Retention: awslogs.RetentionDays_ONE_DAY, 15 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 16 | }) 17 | 18 | resourcePolicyLambda := awslambda.NewFunction(scope, jsii.String("ResourcePolicyLambdaForLogs"), &awslambda.FunctionProps{ 19 | Runtime: awslambda.Runtime_PYTHON_3_13(), 20 | Handler: jsii.String("index.handler"), 21 | Code: awslambda.Code_FromInline(jsii.String(getCode())), 22 | }) 23 | 24 | provider := customresources.NewProvider(scope, jsii.String("CustomResourceProvider"), &customresources.ProviderProps{ 25 | OnEventHandler: resourcePolicyLambda, 26 | }) 27 | 28 | awscdk.NewCustomResource(scope, jsii.String("AddResourcePolicy"), &awscdk.CustomResourceProps{ 29 | ServiceToken: provider.ServiceToken(), 30 | Properties: &map[string]interface{}{ 31 | "CloudWatchLogsLogGroupArn": []interface{}{logGroup.LogGroupArn()}, 32 | "PolicyName": "ResourcePolicyForDNSLog", 33 | "ServiceName": "route53.amazonaws.com", 34 | "ServiceTimeout": "5", 35 | }, 36 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 37 | }) 38 | } 39 | 40 | func getCode() string { 41 | return ` 42 | import json 43 | import cfnresponse 44 | import boto3 45 | from botocore.exceptions import ClientError 46 | 47 | client = boto3.client("logs") 48 | 49 | def PutPolicy(arns, policyname, service): 50 | arn_str = '","'.join(arns) 51 | arn = "[\"" + arn_str + "\"]" 52 | 53 | response = client.put_resource_policy( 54 | policyName=policyname, 55 | policyDocument="{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"" + service + "\"},\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\":"+ arn + "}]}", 56 | ) 57 | return 58 | 59 | def DeletePolicy(policyname): 60 | response = client.delete_resource_policy( 61 | policyName=policyname 62 | ) 63 | return 64 | 65 | def handler(event, context): 66 | 67 | CloudWatchLogsLogGroupArns = event['ResourceProperties']['CloudWatchLogsLogGroupArn'] 68 | PolicyName = event['ResourceProperties']['PolicyName'] 69 | ServiceName = event['ResourceProperties']['ServiceName'] 70 | 71 | responseData = {} 72 | 73 | try: 74 | if event['RequestType'] == "Delete": 75 | # DeletePolicy(PolicyName) 76 | responseData['Data'] = "FAILED" 77 | status=cfnresponse.FAILED 78 | if event['RequestType'] == "Create": 79 | # PutPolicy(CloudWatchLogsLogGroupArns, PolicyName, ServiceName) 80 | responseData['Data'] = "SUCCESS" 81 | status=cfnresponse.SUCCESS 82 | except ClientError as e: 83 | responseData['Data'] = "FAILED" 84 | status=cfnresponse.FAILED 85 | print("Unexpected error: %s" % e) 86 | 87 | cfnresponse.send(event, context, status, responseData, "CustomResourcePhysicalID") 88 | ` 89 | } 90 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/dynamodb.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aws/aws-cdk-go/awscdk/v2" 5 | "github.com/aws/aws-cdk-go/awscdk/v2/awsdynamodb" 6 | "github.com/aws/constructs-go/constructs/v10" 7 | "github.com/aws/jsii-runtime-go" 8 | ) 9 | 10 | func NewDynamoDB(scope constructs.Construct, resourcePrefix string) { 11 | awsdynamodb.NewTable(scope, jsii.String("TableForBackup"), &awsdynamodb.TableProps{ 12 | TableName: jsii.String(resourcePrefix + "-Table"), 13 | PartitionKey: &awsdynamodb.Attribute{ 14 | Name: jsii.String("Id"), 15 | Type: awsdynamodb.AttributeType_STRING, 16 | }, 17 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/ecr.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aws/aws-cdk-go/awscdk/v2" 5 | "github.com/aws/aws-cdk-go/awscdk/v2/awsecr" 6 | "github.com/aws/constructs-go/constructs/v10" 7 | "github.com/aws/jsii-runtime-go" 8 | ) 9 | 10 | func NewEcr(scope constructs.Construct) { 11 | ecr := awsecr.NewRepository(scope, jsii.String("Ecr"), &awsecr.RepositoryProps{ 12 | EmptyOnDelete: jsii.Bool(false), 13 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 14 | }) 15 | 16 | ecr.AddLifecycleRule(&awsecr.LifecycleRule{ 17 | MaxImageCount: jsii.Number(3), 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/iam_group.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aws/aws-cdk-go/awscdk/v2/awsiam" 5 | "github.com/aws/constructs-go/constructs/v10" 6 | "github.com/aws/jsii-runtime-go" 7 | ) 8 | 9 | func NewIamGroup(scope constructs.Construct) { 10 | awsiam.NewGroup(scope, jsii.String("IamGroup"), &awsiam.GroupProps{}) 11 | } 12 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/lambda.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aws/aws-cdk-go/awscdk/v2" 5 | "github.com/aws/aws-cdk-go/awscdk/v2/awslambda" 6 | "github.com/aws/aws-cdk-go/awscdk/v2/awslogs" 7 | "github.com/aws/aws-cdk-go/awscdk/v2/customresources" 8 | "github.com/aws/constructs-go/constructs/v10" 9 | "github.com/aws/jsii-runtime-go" 10 | ) 11 | 12 | func NewCustomResource(scope constructs.Construct) { 13 | logGroup := awslogs.NewLogGroup(scope, jsii.String("LogGroup"), &awslogs.LogGroupProps{ 14 | Retention: awslogs.RetentionDays_ONE_DAY, 15 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 16 | }) 17 | 18 | resourcePolicyLambda := awslambda.NewFunction(scope, jsii.String("ResourcePolicyLambdaForLogs"), &awslambda.FunctionProps{ 19 | Runtime: awslambda.Runtime_PYTHON_3_13(), 20 | Handler: jsii.String("index.handler"), 21 | Code: awslambda.Code_FromInline(jsii.String(getCode())), 22 | }) 23 | 24 | provider := customresources.NewProvider(scope, jsii.String("CustomResourceProvider"), &customresources.ProviderProps{ 25 | OnEventHandler: resourcePolicyLambda, 26 | }) 27 | 28 | awscdk.NewCustomResource(scope, jsii.String("AddResourcePolicy"), &awscdk.CustomResourceProps{ 29 | ServiceToken: provider.ServiceToken(), 30 | Properties: &map[string]interface{}{ 31 | "CloudWatchLogsLogGroupArn": []interface{}{logGroup.LogGroupArn()}, 32 | "PolicyName": "ResourcePolicyForDNSLog", 33 | "ServiceName": "route53.amazonaws.com", 34 | "ServiceTimeout": "5", 35 | }, 36 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 37 | }) 38 | } 39 | 40 | func getLambdaCode() string { 41 | return ` 42 | import json 43 | import cfnresponse 44 | import boto3 45 | from botocore.exceptions import ClientError 46 | 47 | client = boto3.client("logs") 48 | 49 | def PutPolicy(arns, policyname, service): 50 | arn_str = '","'.join(arns) 51 | arn = "[\"" + arn_str + "\"]" 52 | 53 | response = client.put_resource_policy( 54 | policyName=policyname, 55 | policyDocument="{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"" + service + "\"},\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\":"+ arn + "}]}", 56 | ) 57 | return 58 | 59 | def DeletePolicy(policyname): 60 | response = client.delete_resource_policy( 61 | policyName=policyname 62 | ) 63 | return 64 | 65 | def handler(event, context): 66 | 67 | CloudWatchLogsLogGroupArns = event['ResourceProperties']['CloudWatchLogsLogGroupArn'] 68 | PolicyName = event['ResourceProperties']['PolicyName'] 69 | ServiceName = event['ResourceProperties']['ServiceName'] 70 | 71 | responseData = {} 72 | 73 | try: 74 | if event['RequestType'] == "Delete": 75 | # DeletePolicy(PolicyName) 76 | responseData['Data'] = "FAILED" 77 | status=cfnresponse.FAILED 78 | if event['RequestType'] == "Create": 79 | # PutPolicy(CloudWatchLogsLogGroupArns, PolicyName, ServiceName) 80 | responseData['Data'] = "SUCCESS" 81 | status=cfnresponse.SUCCESS 82 | except ClientError as e: 83 | responseData['Data'] = "FAILED" 84 | status=cfnresponse.FAILED 85 | print("Unexpected error: %s" % e) 86 | 87 | cfnresponse.send(event, context, status, responseData, "CustomResourcePhysicalID") 88 | ` 89 | } 90 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/s3_bucket.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aws/aws-cdk-go/awscdk/v2" 5 | "github.com/aws/aws-cdk-go/awscdk/v2/awss3" 6 | "github.com/aws/constructs-go/constructs/v10" 7 | "github.com/aws/jsii-runtime-go" 8 | ) 9 | 10 | func NewS3Bucket(scope constructs.Construct) { 11 | awss3.NewBucket(scope, jsii.String("Bucket"), &awss3.BucketProps{ 12 | Versioned: jsii.Bool(true), 13 | RemovalPolicy: awscdk.RemovalPolicy_DESTROY, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/s3_directory_bucket.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2/awss3express" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | "github.com/aws/jsii-runtime-go" 9 | ) 10 | 11 | func NewS3DirectoryBucket(scope constructs.Construct, bucketNamePrefix string) { 12 | awss3express.NewCfnDirectoryBucket(scope, jsii.String("DirectoryBucket"), &awss3express.CfnDirectoryBucketProps{ 13 | BucketName: jsii.String(strings.ToLower(bucketNamePrefix) + "--use1-az4--x-s3"), 14 | DataRedundancy: jsii.String("SingleAvailabilityZone"), 15 | LocationName: jsii.String("use1-az4"), 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /testdata/cdk/lib/resource/s3_table_bucket.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aws/aws-cdk-go/awscdk/v2/awss3tables" 7 | "github.com/aws/constructs-go/constructs/v10" 8 | "github.com/aws/jsii-runtime-go" 9 | ) 10 | 11 | func NewS3TableBucket(scope constructs.Construct, bucketNamePrefix string) { 12 | // Namespaces and tables are created in the deploy.go file because they are not supported in the CDK and CFn yet 13 | awss3tables.NewCfnTableBucket(scope, jsii.String("TableBucket"), &awss3tables.CfnTableBucketProps{ 14 | TableBucketName: jsii.String(strings.ToLower(bucketNamePrefix)), 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-to-k/delstack/testdata 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.36.3 7 | github.com/aws/aws-sdk-go-v2/config v1.29.14 8 | github.com/aws/aws-sdk-go-v2/service/backup v1.41.2 9 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.59.2 10 | github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 11 | github.com/aws/aws-sdk-go-v2/service/iam v1.41.1 12 | github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 13 | github.com/aws/aws-sdk-go-v2/service/s3tables v1.2.2 14 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 15 | github.com/fatih/color v1.18.0 16 | golang.org/x/sync v0.13.0 17 | ) 18 | 19 | require ( 20 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect 21 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 22 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 33 | github.com/aws/smithy-go v1.22.3 // indirect 34 | github.com/mattn/go-colorable v0.1.14 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | golang.org/x/sys v0.32.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /testdata/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 2 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 3 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= 4 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= 5 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 6 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 7 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 9 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 10 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 11 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 12 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 13 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 14 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 15 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 16 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 17 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= 18 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= 19 | github.com/aws/aws-sdk-go-v2/service/backup v1.41.2 h1:ZUhpA6CSdSujpAnVkM9KKa/ZLZWtz9ixE/yxjYJsqFA= 20 | github.com/aws/aws-sdk-go-v2/service/backup v1.41.2/go.mod h1:m+D3BbPUewtKk/9bWmxGVg1mDeNCu5NtPoTdiLQnEM8= 21 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.59.2 h1:o9cuZdZlI9VWMqsNa2mnf2IRsFAROHnaYA1BW3lHGuY= 22 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.59.2/go.mod h1:penaZKzGmqHGZId4EUCBIW/f9l4Y7hQ5NKd45yoCYuI= 23 | github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 h1:YyH8Hk73bYzdbvf6S8NF5z/fb/1stpiMnFSfL6jSfRA= 24 | github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= 25 | github.com/aws/aws-sdk-go-v2/service/iam v1.41.1 h1:Kq3R+K49y23CGC5UQF3Vpw5oZEQk5gF/nn+MekPD0ZY= 26 | github.com/aws/aws-sdk-go-v2/service/iam v1.41.1/go.mod h1:mPJkGQzeCoPs82ElNILor2JzZgYENr4UaSKUT8K27+c= 27 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 29 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= 30 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 32 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 33 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= 34 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= 35 | github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 h1:tWUG+4wZqdMl/znThEk9tcCy8tTMxq8dW0JTgamohrY= 36 | github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= 37 | github.com/aws/aws-sdk-go-v2/service/s3tables v1.2.2 h1:AFUOzlpkHEKSXFQ0ccqTN5nPxy4lZRV7sTgINHTE+gg= 38 | github.com/aws/aws-sdk-go-v2/service/s3tables v1.2.2/go.mod h1:u8pFMlyM6roXU/RRPYKb+07R+OoyVKO1Gu1AGlDODQk= 39 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 40 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 41 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 42 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 43 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 44 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 45 | github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= 46 | github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 47 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 48 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 49 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 50 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 51 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 52 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 53 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 54 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 55 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 57 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 58 | -------------------------------------------------------------------------------- /testdata/policy_document.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "", 6 | "Effect": "Allow", 7 | "Resource": "*", 8 | "Action": "sts:AssumeRole" 9 | } 10 | ] 11 | } --------------------------------------------------------------------------------