├── .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 ├── cmd └── lamver │ └── main.go ├── go.mod ├── go.sum ├── install.sh ├── internal ├── action │ ├── action.go │ ├── action_test.go │ └── main_test.go ├── app │ └── app.go ├── io │ ├── input.go │ ├── logger.go │ ├── output.go │ └── ui.go ├── types │ └── lambda_function_data.go └── version │ ├── main_test.go │ ├── version.go │ └── version_test.go ├── pkg └── client │ ├── aws_config.go │ ├── ec2.go │ ├── ec2_mock.go │ ├── ec2_test.go │ ├── lambda.go │ ├── lambda_mock.go │ ├── lambda_test.go │ └── main_test.go └── testdata ├── README.md ├── cmd ├── create │ └── main.go └── delete │ └── main.go ├── go.mod ├── go.sum └── pkg ├── aws └── config.go ├── iam └── role.go └── lambda ├── function.go └── source.go /.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 | action 40 | io 41 | types 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 | result.csv 27 | 28 | main 29 | -------------------------------------------------------------------------------- /.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: lamver 2 | env: 3 | - GO111MODULE=on 4 | before: 5 | hooks: 6 | - go mod tidy 7 | builds: 8 | - main: ./cmd/lamver/main.go 9 | binary: lamver 10 | ldflags: 11 | - -s -w 12 | - -X github.com/go-to-k/lamver/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/lamver 43 | description: lamver 44 | test: | 45 | system "#{bin}/lamver -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 | "streetsidesoftware.code-spell-checker" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "lamver", 4 | "jessevdk", 5 | "errgroup", 6 | "zerolog", 7 | "Msgf", 8 | "tablewriter", 9 | "ldflags", 10 | "goreleaser", 11 | "Unshallow", 12 | "unshallow", 13 | "gomod", 14 | ], 15 | "gopls": { 16 | "analyses": { 17 | "shadow": true 18 | } 19 | }, 20 | "go.lintTool": "golangci-lint", 21 | "go.lintFlags": [ 22 | "--fast" 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.18.0](https://github.com/go-to-k/lamver/compare/v0.17.0...v0.18.0) - 2025-03-28 4 | - docs: add gif in README by @go-to-k in https://github.com/go-to-k/lamver/pull/256 5 | - feat: support ruby 3.4 by @go-to-k in https://github.com/go-to-k/lamver/pull/258 6 | 7 | ## [v0.17.0](https://github.com/go-to-k/lamver/compare/v0.16.0...v0.17.0) - 2024-11-21 8 | - feat: support nodejs 22 by @go-to-k in https://github.com/go-to-k/lamver/pull/254 9 | 10 | ## [v0.16.0](https://github.com/go-to-k/lamver/compare/v0.15.1...v0.16.0) - 2024-11-13 11 | - ci: change PR label names for release by @go-to-k in https://github.com/go-to-k/lamver/pull/251 12 | - docs: improve style for README by @go-to-k in https://github.com/go-to-k/lamver/pull/250 13 | - feat: support python 3.13 by @go-to-k in https://github.com/go-to-k/lamver/pull/253 14 | 15 | ## [v0.15.1](https://github.com/go-to-k/lamver/compare/v0.15.0...v0.15.1) - 2024-08-23 16 | - ci: change token for tagpr by @go-to-k in https://github.com/go-to-k/lamver/pull/248 17 | 18 | ## [v0.15.0](https://github.com/go-to-k/lamver/compare/v0.14.2...v0.15.0) - 2024-08-23 19 | - ci: tweak for pr-lint by @go-to-k in https://github.com/go-to-k/lamver/pull/237 20 | - ci: Manage labels in PR lint by @go-to-k in https://github.com/go-to-k/lamver/pull/239 21 | - ci: tweak for semantic-pull-request workflow by @go-to-k in https://github.com/go-to-k/lamver/pull/240 22 | - ci: fix bug that labels are not created by @go-to-k in https://github.com/go-to-k/lamver/pull/242 23 | - ci: ignore lint on tagpr PR by @go-to-k in https://github.com/go-to-k/lamver/pull/243 24 | - ci: add revert type in prlint by @go-to-k in https://github.com/go-to-k/lamver/pull/244 25 | - ci: change token for tagpr by @go-to-k in https://github.com/go-to-k/lamver/pull/246 26 | - ci: don't run CI in PR actions by @go-to-k in https://github.com/go-to-k/lamver/pull/247 27 | - ci: add error linters by @go-to-k in https://github.com/go-to-k/lamver/pull/245 28 | - feat(io): redesign UI implementation with a new library by @go-to-k in https://github.com/go-to-k/lamver/pull/241 29 | 30 | ## [v0.14.2](https://github.com/go-to-k/lamver/compare/v0.14.1...v0.14.2) - 2024-08-16 31 | - ci: PR-Lint for PR titles by @go-to-k in https://github.com/go-to-k/lamver/pull/235 32 | 33 | ## [v0.14.0](https://github.com/go-to-k/lamver/compare/v0.13.0...v0.14.0) - 2024-08-16 34 | - ci: use goreleaser v6 by @go-to-k in https://github.com/go-to-k/lamver/pull/231 35 | 36 | ## [v0.13.0](https://github.com/go-to-k/lamver/compare/v0.12.1...v0.13.0) - 2024-08-16 37 | - ci: release on in tagpr by @go-to-k in https://github.com/go-to-k/lamver/pull/229 38 | 39 | ## [v0.12.1](https://github.com/go-to-k/lamver/compare/v0.12.0...v0.12.1) - 2024-08-16 40 | - chore: change config of brews in .goreleaser.yaml by @go-to-k in https://github.com/go-to-k/lamver/pull/188 41 | - chore: use new gomock by @go-to-k in https://github.com/go-to-k/lamver/pull/224 42 | - ci: add linter by @go-to-k in https://github.com/go-to-k/lamver/pull/225 43 | - ci: use tagpr by @go-to-k in https://github.com/go-to-k/lamver/pull/226 44 | - ci: fix ci.yml by @go-to-k in https://github.com/go-to-k/lamver/pull/228 45 | 46 | ## [v0.12.0](https://github.com/go-to-k/lamver/compare/v0.11.0...v0.12.0) - 2024-04-04 47 | - chore(deps): bump actions/cache from 3 to 4 by @dependabot in https://github.com/go-to-k/lamver/pull/177 48 | - chore(deps): bump actions/setup-go from 4 to 5 by @dependabot in https://github.com/go-to-k/lamver/pull/164 49 | - chore(deps): bump go.uber.org/goleak from 1.2.1 to 1.3.0 by @dependabot in https://github.com/go-to-k/lamver/pull/154 50 | - chore(deps): bump actions/upload-artifact from 3 to 4 by @dependabot in https://github.com/go-to-k/lamver/pull/170 51 | - feat(client): add runtime ruby 3.3 by @go-to-k in https://github.com/go-to-k/lamver/pull/183 52 | 53 | ## [v0.11.0](https://github.com/go-to-k/lamver/compare/v0.10.0...v0.11.0) - 2024-02-21 54 | - chore: add PR template by @go-to-k in https://github.com/go-to-k/lamver/pull/173 55 | - docs: aqua install in README by @go-to-k in https://github.com/go-to-k/lamver/pull/176 56 | - feat(client): add runtime dotnet8 by @go-to-k in https://github.com/go-to-k/lamver/pull/182 57 | 58 | ## [v0.10.0](https://github.com/go-to-k/lamver/compare/v0.9.0...v0.10.0) - 2023-12-22 59 | - feat(io): keep filter for region and runtime selection active by @go-to-k in https://github.com/go-to-k/lamver/pull/172 60 | 61 | ## [v0.9.0](https://github.com/go-to-k/lamver/compare/v0.8.0...v0.9.0) - 2023-12-07 62 | - chore(deps): bump golang.org/x/sync from 0.3.0 to 0.5.0 by @dependabot in https://github.com/go-to-k/lamver/pull/155 63 | - feat(install): Use Script Install by @go-to-k in https://github.com/go-to-k/lamver/pull/163 64 | 65 | ## [v0.8.0](https://github.com/go-to-k/lamver/compare/v0.7.0...v0.8.0) - 2023-11-16 66 | - feat: add java21 runtime by @go-to-k in https://github.com/go-to-k/lamver/pull/153 67 | 68 | ## [v0.7.0](https://github.com/go-to-k/lamver/compare/v0.6.1...v0.7.0) - 2023-11-15 69 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.38.0 to 1.40.0 by @dependabot in https://github.com/go-to-k/lamver/pull/145 70 | - chore(deps): bump actions/checkout from 3 to 4 by @dependabot in https://github.com/go-to-k/lamver/pull/127 71 | - chore(deps): bump goreleaser/goreleaser-action from 4 to 5 by @dependabot in https://github.com/go-to-k/lamver/pull/132 72 | - feat: add new runtime nodejs20.x, python3.12 and provided.al2023 by @go-to-k in https://github.com/go-to-k/lamver/pull/150 73 | 74 | ## [v0.6.1](https://github.com/go-to-k/lamver/compare/v0.6.0...v0.6.1) - 2023-10-30 75 | - chore: minor improvement for keyword search by @go-to-k in https://github.com/go-to-k/lamver/pull/149 76 | 77 | ## [v0.6.0](https://github.com/go-to-k/lamver/compare/v0.5.0...v0.6.0) - 2023-10-05 78 | - docs: README for command options by @go-to-k in https://github.com/go-to-k/lamver/pull/130 79 | - chore: go version to 1.21 by @go-to-k in https://github.com/go-to-k/lamver/pull/140 80 | 81 | ## [v0.5.0](https://github.com/go-to-k/lamver/compare/v0.4.2...v0.5.0) - 2023-09-06 82 | - docs: README.md by @go-to-k in https://github.com/go-to-k/lamver/pull/111 83 | - ci: fix coverage report path by @go-to-k in https://github.com/go-to-k/lamver/pull/125 84 | - test: add goleak by @go-to-k in https://github.com/go-to-k/lamver/pull/126 85 | - feat: add function name keyword option by @go-to-k in https://github.com/go-to-k/lamver/pull/129 86 | 87 | ## [v0.4.2](https://github.com/go-to-k/lamver/compare/v0.4.1...v0.4.2) - 2023-07-31 88 | - ci: version name template by @go-to-k in https://github.com/go-to-k/lamver/pull/98 89 | 90 | ## [v0.4.1](https://github.com/go-to-k/lamver/compare/v0.4.0...v0.4.1) - 2023-07-31 91 | - ci: goreleaser template by @go-to-k in https://github.com/go-to-k/lamver/pull/95 92 | - ci: goreleaser name template by @go-to-k in https://github.com/go-to-k/lamver/pull/96 93 | - ci: release.yml by @go-to-k in https://github.com/go-to-k/lamver/pull/97 94 | 95 | ## [v0.4.0](https://github.com/go-to-k/lamver/compare/v0.3.0...v0.4.0) - 2023-07-31 96 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/ec2 from 1.98.0 to 1.105.1 by @dependabot in https://github.com/go-to-k/lamver/pull/83 97 | - chore(deps): bump golang.org/x/sync from 0.1.0 to 0.3.0 by @dependabot in https://github.com/go-to-k/lamver/pull/73 98 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.35.0 to 1.37.1 by @dependabot in https://github.com/go-to-k/lamver/pull/80 99 | - feat: CheckboxesPageSize to 50 by @go-to-k in https://github.com/go-to-k/lamver/pull/84 100 | - feat: sort by function names by @go-to-k in https://github.com/go-to-k/lamver/pull/86 101 | - feat: add runtime for python 3.11 by @go-to-k in https://github.com/go-to-k/lamver/pull/90 102 | - fix: goreleaser by @go-to-k in https://github.com/go-to-k/lamver/pull/93 103 | - fix: goreleaser version by @go-to-k in https://github.com/go-to-k/lamver/pull/94 104 | 105 | ## [v0.3.0](https://github.com/go-to-k/lamver/commits/v0.3.0) - 2023-06-06 106 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/ec2 from 1.93.2 to 1.98.0 by @dependabot in https://github.com/go-to-k/lamver/pull/68 107 | - feat: add-runtime-versions by @go-to-k in https://github.com/go-to-k/lamver/pull/69 108 | 109 | ## [v0.2.1](https://github.com/go-to-k/lamver/compare/v0.2.0...v0.2.1) - 2023-04-21 110 | - refactor: compareActualVersion by @go-to-k in https://github.com/go-to-k/lamver/pull/57 111 | 112 | ## [v0.2.0](https://github.com/go-to-k/lamver/compare/v0.1.1...v0.2.0) - 2023-04-21 113 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/ec2 from 1.90.0 to 1.91.0 by @dependabot in https://github.com/go-to-k/lamver/pull/44 114 | - test:makefile for diff tests by @go-to-k in https://github.com/go-to-k/lamver/pull/52 115 | - feat: python new ver by @go-to-k in https://github.com/go-to-k/lamver/pull/55 116 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/ec2 from 1.91.0 to 1.93.2 by @dependabot in https://github.com/go-to-k/lamver/pull/50 117 | - feat: change sort order by @go-to-k in https://github.com/go-to-k/lamver/pull/56 118 | 119 | ## [v0.1.1](https://github.com/go-to-k/lamver/compare/v0.1.0...v0.1.1) - 2023-03-22 120 | - chore: change makefile for coverage by @go-to-k in https://github.com/go-to-k/lamver/pull/21 121 | - chore: write package comment by @go-to-k in https://github.com/go-to-k/lamver/pull/42 122 | - chore(deps): bump actions/setup-go from 3 to 4 by @dependabot in https://github.com/go-to-k/lamver/pull/41 123 | - chore(deps): bump github.com/rs/zerolog from 1.28.0 to 1.29.0 by @dependabot in https://github.com/go-to-k/lamver/pull/16 124 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/ec2 from 1.77.0 to 1.90.0 by @dependabot in https://github.com/go-to-k/lamver/pull/40 125 | - chore(deps): bump github.com/urfave/cli/v2 from 2.23.7 to 2.25.0 by @dependabot in https://github.com/go-to-k/lamver/pull/35 126 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.28.0 to 1.30.2 by @dependabot in https://github.com/go-to-k/lamver/pull/43 127 | 128 | ## [v0.1.0](https://github.com/go-to-k/lamver/compare/v0.0.2...v0.1.0) - 2023-02-07 129 | - chore(deps): bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.26.2 to 1.28.0 by @dependabot in https://github.com/go-to-k/lamver/pull/4 130 | - test: unuse mocks by @go-to-k in https://github.com/go-to-k/lamver/pull/8 131 | - refactor: change constants to pascal by @go-to-k in https://github.com/go-to-k/lamver/pull/15 132 | - Chore/tool description and messages by @go-to-k in https://github.com/go-to-k/lamver/pull/3 133 | - docs: README by @go-to-k in https://github.com/go-to-k/lamver/pull/2 134 | - feat: #6 case insensitive function names search by @go-to-k in https://github.com/go-to-k/lamver/pull/17 135 | 136 | ## [v0.0.2](https://github.com/go-to-k/lamver/compare/v0.0.1...v0.0.2) - 2023-01-07 137 | 138 | ## [v0.0.1](https://github.com/go-to-k/lamver/commits/v0.0.1) - 2023-01-07 139 | -------------------------------------------------------------------------------- /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 | .PHONY: test_diff test test_view lint lint_diff mockgen deadcode shadow cognit run build install clean testgen testgen_clean testgen_help 6 | 7 | COLORIZE_PASS = sed "s/^\([- ]*\)\(PASS\)/\1$$(printf "$(GREEN)")\2$$(printf "$(RESET)")/g" 8 | COLORIZE_FAIL = sed "s/^\([- ]*\)\(FAIL\)/\1$$(printf "$(RED)")\2$$(printf "$(RESET)")/g" 9 | 10 | VERSION := $(shell git describe --tags --abbrev=0) 11 | REVISION := $(shell git rev-parse --short HEAD) 12 | LDFLAGS := -s -w \ 13 | -X 'github.com/go-to-k/lamver/internal/version.Version=$(VERSION)' \ 14 | -X 'github.com/go-to-k/lamver/internal/version.Revision=$(REVISION)' 15 | GO_FILES := $(shell find . -type f -name '*.go' -print) 16 | 17 | DIFF_FILE := "$$(git diff --name-only --diff-filter=ACMRT | grep .go$ | xargs -I{} dirname {} | sort | uniq | xargs -I{} echo ./{})" 18 | 19 | TEST_DIFF_RESULT := "$$(go test -race -cover -v $$(echo $(DIFF_FILE)) -coverpkg=./...)" 20 | TEST_FULL_RESULT := "$$(go test -race -cover -v ./... -coverpkg=./...)" 21 | TEST_COV_RESULT := "$$(go test -race -cover -v ./... -coverpkg=./... -coverprofile=cover.out.tmp)" 22 | 23 | FAIL_CHECK := "^[^\s\t]*FAIL[^\s\t]*$$" 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 | deadcode: 42 | deadcode ./... 43 | shadow: 44 | find . -type f -name '*.go' | sed -e "s/\/[^\.\/]*\.go//g" | uniq | xargs shadow 45 | cognit: 46 | gocognit -top 10 ./ | grep -v "_test.go" 47 | run: 48 | go mod tidy 49 | go run -ldflags "$(LDFLAGS)" cmd/lamver/main.go $${OPT} 50 | build: $(GO_FILES) 51 | go mod tidy 52 | go build -ldflags "$(LDFLAGS)" -o lamver cmd/lamver/main.go 53 | install: 54 | go install -ldflags "$(LDFLAGS)" github.com/go-to-k/lamver/cmd/lamver 55 | clean: 56 | go clean 57 | rm -f lamver 58 | 59 | # Test data generation commands 60 | # ================================== 61 | 62 | # Run test functions generator 63 | testgen: 64 | @echo "Running test functions generator..." 65 | @cd testdata && go mod tidy && go run cmd/create/main.go $(OPT) 66 | 67 | # Clean test functions 68 | testgen_clean: 69 | @echo "Cleaning test functions..." 70 | @cd testdata && go mod tidy && go run cmd/delete/main.go $(OPT) 71 | 72 | # Help for test functions generation 73 | testgen_help: 74 | @echo "Test functions generation targets:" 75 | @echo " testgen - Run the test functions generator" 76 | @echo " testgen_clean - Clean test functions" 77 | @echo "" 78 | @echo "Example usage:" 79 | @echo " make testgen" 80 | @echo " make testgen OPT=\"-p myprofile\"" 81 | @echo " make testgen_clean OPT=\"-p myprofile\"" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lamver 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-to-k/lamver)](https://goreportcard.com/report/github.com/go-to-k/lamver) ![GitHub](https://img.shields.io/github/license/go-to-k/lamver) ![GitHub](https://img.shields.io/github/v/release/go-to-k/lamver) [![ci](https://github.com/go-to-k/lamver/actions/workflows/ci.yml/badge.svg)](https://github.com/go-to-k/lamver/actions/workflows/ci.yml) 4 | 5 | The description in **Japanese** is available on the following blog page. -> [Blog](https://go-to-k.hatenablog.com/entry/lamver) 6 | 7 | The description in **English** is available on the following blog page. -> [Blog](https://dev.to/aws-builders/lambda-runtimeversion-search-tool-across-regions-41l0) 8 | 9 | ## What is 10 | 11 | CLI tool to search AWS Lambda runtime values and versions. 12 | 13 | By filtering by regions, runtime and versions, or part of the function name, you can find a list of functions **across regions**. 14 | 15 | This will allow you can see the following. 16 | 17 | - What Lambda functions exist in which regions 18 | - Whether there are any functions that have reached EOL 19 | - Whether there is a function in an unexpected region 20 | - Whether a function exists based on a specific naming rule 21 | 22 | Also this tool can support output results **as a CSV file.** 23 | 24 | ![lamver](https://github.com/user-attachments/assets/af374bfd-b23f-48ee-bfae-d4194f7000ae) 25 | 26 | ## Install 27 | 28 | - Homebrew 29 | 30 | ```bash 31 | brew install go-to-k/tap/lamver 32 | ``` 33 | 34 | - Linux, Darwin (macOS) and Windows 35 | 36 | ```bash 37 | curl -fsSL https://raw.githubusercontent.com/go-to-k/lamver/main/install.sh | sh 38 | lamver -h 39 | 40 | # To install a specific version of lamver 41 | # e.g. version 0.8.0 42 | curl -fsSL https://raw.githubusercontent.com/go-to-k/lamver/main/install.sh | sh -s "v0.8.0" 43 | lamver -h 44 | ``` 45 | 46 | - aqua 47 | 48 | ```bash 49 | aqua g -i go-to-k/lamver 50 | ``` 51 | 52 | - Binary 53 | - [Releases](https://github.com/go-to-k/lamver/releases) 54 | - Git Clone and install(for developers) 55 | 56 | ```bash 57 | git clone https://github.com/go-to-k/lamver.git 58 | cd lamver 59 | make install 60 | ``` 61 | 62 | ## How to use 63 | 64 | ```bash 65 | lamver [-p ] [-r ] [-o ] [-k ] 66 | ``` 67 | 68 | ### options 69 | 70 | - -p, --profile: optional 71 | - AWS profile name 72 | - -r, --region: optional 73 | - Default AWS region 74 | - The region to output is selected interactively and does not need to be specified. 75 | - -o, --output: optional 76 | - Output file path for CSV format 77 | - -k, --keyword: optional 78 | - Keyword for function name filtering (case-insensitive) 79 | 80 | ## Input flow 81 | 82 | ### Enter `lamver` 83 | 84 | ```bash 85 | ❯ lamver 86 | ``` 87 | 88 | You can specify `-k, --keyword` option. This is a keyword for **function name filtering (case-insensitive)**. 89 | 90 | ```sh 91 | ❯ lamver -k goto 92 | ``` 93 | 94 | ### Choose regions 95 | 96 | ```bash 97 | ? Select regions you want to search. 98 | [Use arrows to move, space to select, to all, to none, type to filter] 99 | [x] ap-northeast-1 100 | [ ] ap-northeast-2 101 | [ ] ap-northeast-3 102 | [ ] ap-south-1 103 | [ ] ap-southeast-1 104 | [ ] ap-southeast-2 105 | [ ] ca-central-1 106 | [ ] eu-central-1 107 | [ ] eu-north-1 108 | [ ] eu-west-1 109 | [ ] eu-west-2 110 | [ ] eu-west-3 111 | [ ] sa-east-1 112 | [x] us-east-1 113 | > [x] us-east-2 114 | [ ] us-west-1 115 | [ ] us-west-2 116 | ``` 117 | 118 | ### Choose runtime values 119 | 120 | ```bash 121 | ? Select runtime values you want to search. 122 | [Use arrows to move, space to select, to all, to none, type to filter] 123 | > [ ] dotnet6 124 | [ ] dotnet8 125 | [ ] dotnetcore1.0 126 | [ ] dotnetcore2.0 127 | [ ] dotnetcore2.1 128 | [ ] dotnetcore3.1 129 | [x] go1.x 130 | [ ] java8 131 | [ ] java8.al2 132 | [ ] java11 133 | [ ] java17 134 | [ ] java21 135 | [ ] nodejs 136 | [ ] nodejs4.3 137 | [ ] nodejs4.3-edge 138 | [ ] nodejs6.10 139 | [ ] nodejs8.10 140 | [ ] nodejs10.x 141 | [x] nodejs12.x 142 | [ ] nodejs14.x 143 | [ ] nodejs16.x 144 | [ ] nodejs18.x 145 | [ ] nodejs20.x 146 | [ ] nodejs22.x 147 | [ ] provided 148 | [x] provided.al2 149 | [ ] provided.al2023 150 | [ ] python2.7 151 | [ ] python3.6 152 | [ ] python3.7 153 | [ ] python3.8 154 | [ ] python3.9 155 | [ ] python3.10 156 | [ ] python3.11 157 | [ ] python3.12 158 | [ ] python3.13 159 | [ ] ruby2.5 160 | [ ] ruby2.7 161 | [ ] ruby3.2 162 | [ ] ruby3.3 163 | [ ] ruby3.4 164 | ``` 165 | 166 | ### Enter part of the function name 167 | 168 | You can search function names in a **case-insensitive**. 169 | 170 | **Empty** input will output **all functions**. 171 | 172 | This phase is skipped if you specify `-k` option. 173 | 174 | ```bash 175 | Filter a keyword of function names(case-insensitive): test-goto 176 | ``` 177 | 178 | ### The result will be output 179 | 180 | ```bash 181 | +--------------+----------------+----------------------+------------------------------+ 182 | | RUNTIME | REGION | FUNCTIONNAME | LASTMODIFIED | 183 | +--------------+----------------+----------------------+------------------------------+ 184 | | go1.x | ap-northeast-1 | Test-goto-function2 | 2023-01-07T14:54:23.406+0000 | 185 | +--------------+----------------+----------------------+------------------------------+ 186 | | go1.x | ap-northeast-1 | test-Goto-function10 | 2023-01-07T15:29:11.658+0000 | 187 | +--------------+----------------+----------------------+------------------------------+ 188 | | go1.x | us-east-2 | TEST-goto-function6 | 2023-01-07T15:28:08.507+0000 | 189 | +--------------+----------------+----------------------+------------------------------+ 190 | | nodejs12.x | ap-northeast-1 | test-GOTO-function1 | 2023-01-07T14:53:49.141+0000 | 191 | +--------------+----------------+----------------------+------------------------------+ 192 | | nodejs12.x | us-east-1 | TEST-GOTO-function4 | 2023-01-07T15:18:14.191+0000 | 193 | +--------------+----------------+----------------------+------------------------------+ 194 | | nodejs12.x | us-east-1 | test-goto-function7 | 2023-01-07T15:28:20.921+0000 | 195 | +--------------+----------------+----------------------+------------------------------+ 196 | | nodejs12.x | us-east-2 | test-goto-function5 | 2023-01-07T15:18:34.408+0000 | 197 | +--------------+----------------+----------------------+------------------------------+ 198 | | provided.al2 | ap-northeast-1 | test-goto-function8 | 2023-01-07T15:28:34.968+0000 | 199 | +--------------+----------------+----------------------+------------------------------+ 200 | | provided.al2 | us-east-1 | test-goto-function3 | 2023-01-07T15:17:35.965+0000 | 201 | +--------------+----------------+----------------------+------------------------------+ 202 | | provided.al2 | us-east-2 | test-goto-function9 | 2023-01-07T15:29:16.107+0000 | 203 | +--------------+----------------+----------------------+------------------------------+ 204 | INF 10 counts hit! 205 | ``` 206 | 207 | ## CSV output mode 208 | 209 | By default, results are output as table format on the screen. 210 | 211 | If you add `-o` option, then results can be output **as a CSV file**. 212 | 213 | ```bash 214 | lamver -o ./result.csv 215 | ``` 216 | -------------------------------------------------------------------------------- /cmd/lamver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/go-to-k/lamver/internal/app" 8 | "github.com/go-to-k/lamver/internal/io" 9 | "github.com/go-to-k/lamver/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/lamver 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.28.3 10 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.187.1 11 | github.com/aws/aws-sdk-go-v2/service/lambda v1.71.0 12 | github.com/aws/smithy-go v1.22.2 13 | github.com/charmbracelet/bubbletea v0.27.0 14 | github.com/fatih/color v1.17.0 15 | github.com/olekukonko/tablewriter v0.0.5 16 | github.com/rs/zerolog v1.33.0 17 | github.com/urfave/cli/v2 v2.25.0 18 | go.uber.org/goleak v1.3.0 19 | go.uber.org/mock v0.4.0 20 | golang.org/x/sync v0.8.0 21 | ) 22 | 23 | require ( 24 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect 25 | github.com/aws/aws-sdk-go-v2/credentials v1.17.44 // indirect 26 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 28 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 // indirect 35 | github.com/charmbracelet/x/ansi v0.1.4 // indirect 36 | github.com/charmbracelet/x/input v0.1.0 // indirect 37 | github.com/charmbracelet/x/term v0.1.1 // indirect 38 | github.com/charmbracelet/x/windows v0.1.0 // indirect 39 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 40 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 41 | github.com/mattn/go-colorable v0.1.13 // indirect 42 | github.com/mattn/go-isatty v0.0.20 // indirect 43 | github.com/mattn/go-localereader v0.0.1 // indirect 44 | github.com/mattn/go-runewidth v0.0.15 // indirect 45 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 46 | github.com/muesli/cancelreader v0.2.2 // indirect 47 | github.com/rivo/uniseg v0.4.7 // indirect 48 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 49 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 50 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 51 | golang.org/x/sys v0.24.0 // indirect 52 | golang.org/x/text v0.3.8 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /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.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= 6 | github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= 7 | github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= 9 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= 10 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= 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.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= 16 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= 17 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.187.1 h1:g6N2LDa3UuNR8CZvTYuXUKzfCD6S1iqRIsDFkbtwu0Y= 18 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.187.1/go.mod h1:0A17IIeys01WfjDKehspGP+Cyo/YH/eNADIbEbRS9yM= 19 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= 20 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= 21 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= 22 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= 23 | github.com/aws/aws-sdk-go-v2/service/lambda v1.71.0 h1:8PjrcaqDZKar6ivI8c6vwNADOURebrRZQms3SxggRgU= 24 | github.com/aws/aws-sdk-go-v2/service/lambda v1.71.0/go.mod h1:c27kk10S36lBYgbG1jR3opn4OAS5Y/4wjJa1GiHK/X4= 25 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= 27 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= 28 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= 29 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= 31 | github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 32 | github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 33 | github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU= 34 | github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y= 35 | github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= 36 | github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 37 | github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= 38 | github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= 39 | github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= 40 | github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= 41 | github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= 42 | github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= 43 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 44 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 45 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 46 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 47 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 49 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 50 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 51 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 52 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 53 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 54 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 55 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 56 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 57 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 58 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 59 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 60 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 61 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 62 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 63 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 64 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 65 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 66 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 67 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 68 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 69 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 70 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 71 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 74 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 75 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 76 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 77 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 78 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 79 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 80 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 81 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 82 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 83 | github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8= 84 | github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 85 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 86 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 87 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 88 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 89 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 90 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 91 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 92 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 93 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 94 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 95 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 96 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 97 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 102 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 103 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 104 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 105 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 106 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 107 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script automates the installation of the lamver 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 lamver 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 lamver 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/lamver/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 lamver binary based on the version, OS, and architecture. 61 | FILE_NAME="lamver_${VERSION}_${OS}_${ARCH}.tar.gz" 62 | URL="https://github.com/go-to-k/lamver/releases/download/v${VERSION}/${FILE_NAME}" 63 | 64 | # Download the lamver binary. 65 | echo "Downloading lamver..." 66 | if ! curl -L -o "$FILE_NAME" "$URL"; then 67 | echo "Failed to download lamver" 68 | exit 1 69 | fi 70 | 71 | # Install lamver. 72 | # This involves extracting the binary and moving it to /usr/local/bin. 73 | echo "Installing lamver..." 74 | if ! tar -xzf "$FILE_NAME"; then 75 | echo "Failed to extract lamver" 76 | exit 1 77 | fi 78 | if ! sudo mv lamver /usr/local/bin/lamver; then 79 | echo "Failed to install lamver" 80 | exit 1 81 | fi 82 | 83 | # Clean up by removing the downloaded tar file. 84 | rm "$FILE_NAME" 85 | 86 | echo "lamver installation complete." 87 | echo "Run 'lamver -h' to see how to use lamver." 88 | -------------------------------------------------------------------------------- /internal/action/action.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "sort" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/go-to-k/lamver/internal/types" 11 | "github.com/go-to-k/lamver/pkg/client" 12 | 13 | "golang.org/x/sync/errgroup" 14 | "golang.org/x/sync/semaphore" 15 | ) 16 | 17 | type GetAllRegionsAndRuntimeInput struct { 18 | Ctx context.Context 19 | EC2 client.EC2Client 20 | Lambda client.LambdaClient 21 | DefaultRegion string 22 | } 23 | 24 | func GetAllRegionsAndRuntime(input *GetAllRegionsAndRuntimeInput) (regionList []string, runtimeList []string, err error) { 25 | eg, ctx := errgroup.WithContext(input.Ctx) 26 | eg.Go(func() error { 27 | regionList, err = input.EC2.DescribeRegions(ctx) 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | }) 33 | 34 | eg.Go(func() error { 35 | runtimeList = input.Lambda.ListRuntimeValues() 36 | return nil 37 | }) 38 | 39 | if err := eg.Wait(); err != nil { 40 | return regionList, runtimeList, err 41 | } 42 | 43 | return regionList, runtimeList, nil 44 | } 45 | 46 | type CreateFunctionListInput struct { 47 | Ctx context.Context 48 | TargetRegions []string 49 | TargetRuntime []string 50 | Keyword string 51 | Lambda client.LambdaClient 52 | } 53 | 54 | func CreateFunctionList(input *CreateFunctionListInput) ([][]string, error) { 55 | functionMap := make(map[string]map[string][][]string, len(input.TargetRuntime)) 56 | 57 | eg, ctx := errgroup.WithContext(input.Ctx) 58 | functionCh := make(chan *types.LambdaFunctionData) 59 | sem := semaphore.NewWeighted(int64(runtime.NumCPU())) 60 | wg := sync.WaitGroup{} 61 | 62 | wg.Add(1) 63 | go func() { 64 | defer wg.Done() 65 | for f := range functionCh { 66 | if _, exist := functionMap[f.Runtime]; !exist { 67 | functionMap[f.Runtime] = make(map[string][][]string, len(input.TargetRegions)) 68 | } 69 | functionMap[f.Runtime][f.Region] = append(functionMap[f.Runtime][f.Region], []string{f.FunctionName, f.LastModified}) 70 | } 71 | }() 72 | 73 | for _, region := range input.TargetRegions { 74 | region := region 75 | if err := sem.Acquire(ctx, 1); err != nil { 76 | return [][]string{}, err 77 | } 78 | eg.Go(func() error { 79 | defer sem.Release(1) 80 | return putToFunctionChannelByRegion( 81 | ctx, 82 | region, 83 | input.TargetRuntime, 84 | input.Keyword, 85 | functionCh, 86 | input.Lambda, 87 | ) 88 | }) 89 | } 90 | 91 | go func() { 92 | //nolint:errcheck 93 | eg.Wait() 94 | close(functionCh) 95 | }() 96 | 97 | if err := eg.Wait(); err != nil { 98 | return [][]string{}, err 99 | } 100 | 101 | wg.Wait() // for functionMap race 102 | 103 | sortedFunctionList := sortAndSetFunctionList(input.TargetRegions, input.TargetRuntime, functionMap) 104 | 105 | return sortedFunctionList, nil 106 | } 107 | 108 | func putToFunctionChannelByRegion( 109 | ctx context.Context, 110 | region string, 111 | targetRuntime []string, 112 | keyword string, 113 | functionCh chan *types.LambdaFunctionData, 114 | lambda client.LambdaClient, 115 | ) error { 116 | functions, err := lambda.ListFunctionsWithRegion(ctx, region) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | lowerKeyword := strings.ToLower(keyword) 122 | 123 | for _, function := range functions { 124 | for _, runtime := range targetRuntime { 125 | select { 126 | case <-ctx.Done(): 127 | return ctx.Err() 128 | default: 129 | } 130 | if string(function.Runtime) != runtime { 131 | continue 132 | } 133 | // for case-insensitive 134 | lowerFunctionName := strings.ToLower(*function.FunctionName) 135 | if strings.Contains(lowerFunctionName, lowerKeyword) { 136 | functionCh <- &types.LambdaFunctionData{ 137 | Runtime: runtime, 138 | Region: region, 139 | FunctionName: *function.FunctionName, 140 | LastModified: *function.LastModified, 141 | } 142 | } 143 | break 144 | } 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func sortAndSetFunctionList(regionList []string, runtimeList []string, functionMap map[string]map[string][][]string) [][]string { 151 | var functionList [][]string 152 | 153 | for _, runtime := range runtimeList { 154 | if _, exist := functionMap[runtime]; !exist { 155 | continue 156 | } 157 | for _, region := range regionList { 158 | if _, exist := functionMap[runtime][region]; !exist { 159 | continue 160 | } 161 | 162 | sort.Slice(functionMap[runtime][region], func(i, j int) bool { 163 | return functionMap[runtime][region][i][0] < functionMap[runtime][region][j][0] 164 | }) 165 | 166 | for _, f := range functionMap[runtime][region] { 167 | var data []string 168 | data = append(data, runtime) 169 | data = append(data, region) 170 | data = append(data, f...) 171 | functionList = append(functionList, data) 172 | } 173 | } 174 | } 175 | 176 | return functionList 177 | } 178 | -------------------------------------------------------------------------------- /internal/action/action_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/go-to-k/lamver/internal/types" 11 | "github.com/go-to-k/lamver/pkg/client" 12 | 13 | "github.com/aws/aws-sdk-go-v2/aws" 14 | lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types" 15 | "go.uber.org/mock/gomock" 16 | ) 17 | 18 | func TestGetAllRegionsAndRuntime(t *testing.T) { 19 | type args struct { 20 | ctx context.Context 21 | region string 22 | } 23 | 24 | tests := []struct { 25 | name string 26 | args args 27 | prepareMockEC2ClientFn func(m *client.MockEC2Client) 28 | prepareMockLambdaClientFn func(m *client.MockLambdaClient) 29 | wantRegionList []string 30 | wantRuntimeList []string 31 | wantErr bool 32 | }{ 33 | { 34 | name: "GetAllRegionsAndRuntime success", 35 | args: args{ 36 | ctx: context.Background(), 37 | region: "us-east-1", 38 | }, 39 | prepareMockEC2ClientFn: func(m *client.MockEC2Client) { 40 | m.EXPECT().DescribeRegions(gomock.Any()).Return( 41 | []string{ 42 | "ap-northeast-1", 43 | "us-east-1", 44 | }, nil, 45 | ) 46 | }, 47 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 48 | m.EXPECT().ListRuntimeValues().Return( 49 | []string{ 50 | "go1.x", 51 | "nodejs18.x", 52 | }, 53 | ) 54 | }, 55 | wantRegionList: []string{ 56 | "ap-northeast-1", 57 | "us-east-1", 58 | }, 59 | wantRuntimeList: []string{ 60 | "go1.x", 61 | "nodejs18.x", 62 | }, 63 | wantErr: false, 64 | }, 65 | { 66 | name: "GetAllRegionsAndRuntime fail by DescribeRegions Error", 67 | args: args{ 68 | ctx: context.Background(), 69 | region: "us-east-1", 70 | }, 71 | prepareMockEC2ClientFn: func(m *client.MockEC2Client) { 72 | m.EXPECT().DescribeRegions(gomock.Any()).Return( 73 | []string{}, fmt.Errorf("DescribeRegionsError"), 74 | ) 75 | }, 76 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 77 | m.EXPECT().ListRuntimeValues().Return( 78 | []string{ 79 | "go1.x", 80 | "nodejs18.x", 81 | }, 82 | ) 83 | }, 84 | wantRegionList: []string{}, 85 | wantRuntimeList: []string{ 86 | "go1.x", 87 | "nodejs18.x", 88 | }, 89 | wantErr: true, 90 | }, 91 | } 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | ctrl := gomock.NewController(t) 95 | ec2ClientMock := client.NewMockEC2Client(ctrl) 96 | lambdaClientMock := client.NewMockLambdaClient(ctrl) 97 | 98 | tt.prepareMockEC2ClientFn(ec2ClientMock) 99 | tt.prepareMockLambdaClientFn(lambdaClientMock) 100 | 101 | input := &GetAllRegionsAndRuntimeInput{ 102 | Ctx: tt.args.ctx, 103 | EC2: ec2ClientMock, 104 | Lambda: lambdaClientMock, 105 | DefaultRegion: tt.args.region, 106 | } 107 | 108 | gotRegionList, gotRuntimeList, err := GetAllRegionsAndRuntime(input) 109 | if (err != nil) != tt.wantErr { 110 | t.Errorf("GetAllRegionsAndRuntime() error = %v, wantErr %v", err, tt.wantErr) 111 | return 112 | } 113 | if !reflect.DeepEqual(gotRegionList, tt.wantRegionList) { 114 | t.Errorf("GetAllRegionsAndRuntime() gotRegionList = %v, want %v", gotRegionList, tt.wantRegionList) 115 | } 116 | if !reflect.DeepEqual(gotRuntimeList, tt.wantRuntimeList) { 117 | t.Errorf("GetAllRegionsAndRuntime() gotRuntimeList = %v, want %v", gotRuntimeList, tt.wantRuntimeList) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func TestCreateFunctionList(t *testing.T) { 124 | type args struct { 125 | ctx context.Context 126 | targetRegions []string 127 | targetRuntime []string 128 | keyword string 129 | } 130 | 131 | tests := []struct { 132 | name string 133 | args args 134 | prepareMockLambdaClientFn func(m *client.MockLambdaClient) 135 | want [][]string 136 | wantErr bool 137 | }{ 138 | { 139 | name: "CreateFunctionList success", 140 | args: args{ 141 | ctx: context.Background(), 142 | targetRegions: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 143 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 144 | keyword: "", 145 | }, 146 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 147 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "ap-northeast-1").Return( 148 | []lambdaTypes.FunctionConfiguration{ 149 | { 150 | FunctionName: aws.String("Function1"), 151 | Runtime: lambdaTypes.RuntimeNodejs, 152 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 153 | }, 154 | { 155 | FunctionName: aws.String("Function2"), 156 | Runtime: lambdaTypes.RuntimeGo1x, 157 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 158 | }, 159 | }, nil, 160 | ) 161 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 162 | []lambdaTypes.FunctionConfiguration{ 163 | { 164 | FunctionName: aws.String("Function3"), 165 | Runtime: lambdaTypes.RuntimeNodejs, 166 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 167 | }, 168 | { 169 | FunctionName: aws.String("Function4"), 170 | Runtime: lambdaTypes.RuntimeGo1x, 171 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 172 | }, 173 | }, nil, 174 | ) 175 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-2").Return( 176 | []lambdaTypes.FunctionConfiguration{ 177 | { 178 | FunctionName: aws.String("Function5"), 179 | Runtime: lambdaTypes.RuntimeNodejs, 180 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 181 | }, 182 | { 183 | FunctionName: aws.String("Function6"), 184 | Runtime: lambdaTypes.RuntimeNodejs18x, 185 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 186 | }, 187 | }, nil, 188 | ) 189 | }, 190 | want: [][]string{ 191 | {"nodejs18.x", "us-east-2", "Function6", "2022-12-22T09:47:43.728+0000"}, 192 | {"nodejs", "ap-northeast-1", "Function1", "2022-12-21T09:47:43.728+0000"}, 193 | {"nodejs", "us-east-1", "Function3", "2022-12-21T09:47:43.728+0000"}, 194 | {"nodejs", "us-east-2", "Function5", "2022-12-21T09:47:43.728+0000"}, 195 | }, 196 | wantErr: false, 197 | }, 198 | { 199 | name: "CreateFunctionList success but there is no function", 200 | args: args{ 201 | ctx: context.Background(), 202 | targetRegions: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 203 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 204 | keyword: "", 205 | }, 206 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 207 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "ap-northeast-1").Return( 208 | []lambdaTypes.FunctionConfiguration{}, nil, 209 | ) 210 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 211 | []lambdaTypes.FunctionConfiguration{}, nil, 212 | ) 213 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-2").Return( 214 | []lambdaTypes.FunctionConfiguration{}, nil, 215 | ) 216 | }, 217 | want: [][]string{}, 218 | wantErr: false, 219 | }, 220 | { 221 | name: "CreateFunctionList success if a keyword is not empty", 222 | args: args{ 223 | ctx: context.Background(), 224 | targetRegions: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 225 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 226 | keyword: "3", 227 | }, 228 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 229 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "ap-northeast-1").Return( 230 | []lambdaTypes.FunctionConfiguration{ 231 | { 232 | FunctionName: aws.String("Function1"), 233 | Runtime: lambdaTypes.RuntimeNodejs, 234 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 235 | }, 236 | { 237 | FunctionName: aws.String("Function2"), 238 | Runtime: lambdaTypes.RuntimeGo1x, 239 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 240 | }, 241 | }, nil, 242 | ) 243 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 244 | []lambdaTypes.FunctionConfiguration{ 245 | { 246 | FunctionName: aws.String("Function3"), 247 | Runtime: lambdaTypes.RuntimeNodejs, 248 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 249 | }, 250 | { 251 | FunctionName: aws.String("Function4"), 252 | Runtime: lambdaTypes.RuntimeGo1x, 253 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 254 | }, 255 | }, nil, 256 | ) 257 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-2").Return( 258 | []lambdaTypes.FunctionConfiguration{ 259 | { 260 | FunctionName: aws.String("Function5"), 261 | Runtime: lambdaTypes.RuntimeNodejs, 262 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 263 | }, 264 | { 265 | FunctionName: aws.String("Function6"), 266 | Runtime: lambdaTypes.RuntimeNodejs18x, 267 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 268 | }, 269 | }, nil, 270 | ) 271 | }, 272 | want: [][]string{ 273 | {"nodejs", "us-east-1", "Function3", "2022-12-21T09:47:43.728+0000"}, 274 | }, 275 | wantErr: false, 276 | }, 277 | { 278 | name: "CreateFunctionList success if a keyword is not empty and lower", 279 | args: args{ 280 | ctx: context.Background(), 281 | targetRegions: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 282 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 283 | keyword: "function3", 284 | }, 285 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 286 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "ap-northeast-1").Return( 287 | []lambdaTypes.FunctionConfiguration{ 288 | { 289 | FunctionName: aws.String("Function1"), 290 | Runtime: lambdaTypes.RuntimeNodejs, 291 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 292 | }, 293 | { 294 | FunctionName: aws.String("Function2"), 295 | Runtime: lambdaTypes.RuntimeGo1x, 296 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 297 | }, 298 | }, nil, 299 | ) 300 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 301 | []lambdaTypes.FunctionConfiguration{ 302 | { 303 | FunctionName: aws.String("Function3"), 304 | Runtime: lambdaTypes.RuntimeNodejs, 305 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 306 | }, 307 | { 308 | FunctionName: aws.String("Function4"), 309 | Runtime: lambdaTypes.RuntimeGo1x, 310 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 311 | }, 312 | }, nil, 313 | ) 314 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-2").Return( 315 | []lambdaTypes.FunctionConfiguration{ 316 | { 317 | FunctionName: aws.String("Function5"), 318 | Runtime: lambdaTypes.RuntimeNodejs, 319 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 320 | }, 321 | { 322 | FunctionName: aws.String("Function6"), 323 | Runtime: lambdaTypes.RuntimeNodejs18x, 324 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 325 | }, 326 | }, nil, 327 | ) 328 | }, 329 | want: [][]string{ 330 | {"nodejs", "us-east-1", "Function3", "2022-12-21T09:47:43.728+0000"}, 331 | }, 332 | wantErr: false, 333 | }, 334 | { 335 | name: "CreateFunctionList success if a keyword is not empty and upper", 336 | args: args{ 337 | ctx: context.Background(), 338 | targetRegions: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 339 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 340 | keyword: "FUNCTION3", 341 | }, 342 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 343 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "ap-northeast-1").Return( 344 | []lambdaTypes.FunctionConfiguration{ 345 | { 346 | FunctionName: aws.String("Function1"), 347 | Runtime: lambdaTypes.RuntimeNodejs, 348 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 349 | }, 350 | { 351 | FunctionName: aws.String("Function2"), 352 | Runtime: lambdaTypes.RuntimeGo1x, 353 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 354 | }, 355 | }, nil, 356 | ) 357 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 358 | []lambdaTypes.FunctionConfiguration{ 359 | { 360 | FunctionName: aws.String("Function3"), 361 | Runtime: lambdaTypes.RuntimeNodejs, 362 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 363 | }, 364 | { 365 | FunctionName: aws.String("Function4"), 366 | Runtime: lambdaTypes.RuntimeGo1x, 367 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 368 | }, 369 | }, nil, 370 | ) 371 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-2").Return( 372 | []lambdaTypes.FunctionConfiguration{ 373 | { 374 | FunctionName: aws.String("Function5"), 375 | Runtime: lambdaTypes.RuntimeNodejs, 376 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 377 | }, 378 | { 379 | FunctionName: aws.String("Function6"), 380 | Runtime: lambdaTypes.RuntimeNodejs18x, 381 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 382 | }, 383 | }, nil, 384 | ) 385 | }, 386 | want: [][]string{ 387 | {"nodejs", "us-east-1", "Function3", "2022-12-21T09:47:43.728+0000"}, 388 | }, 389 | wantErr: false, 390 | }, 391 | { 392 | name: "CreateFunctionList success if a keyword is not empty when except runtime", 393 | args: args{ 394 | ctx: context.Background(), 395 | targetRegions: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 396 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 397 | keyword: "2", 398 | }, 399 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 400 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "ap-northeast-1").Return( 401 | []lambdaTypes.FunctionConfiguration{ 402 | { 403 | FunctionName: aws.String("Function1"), 404 | Runtime: lambdaTypes.RuntimeNodejs, 405 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 406 | }, 407 | { 408 | FunctionName: aws.String("Function2"), 409 | Runtime: lambdaTypes.RuntimeGo1x, 410 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 411 | }, 412 | }, nil, 413 | ) 414 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 415 | []lambdaTypes.FunctionConfiguration{ 416 | { 417 | FunctionName: aws.String("Function3"), 418 | Runtime: lambdaTypes.RuntimeNodejs, 419 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 420 | }, 421 | { 422 | FunctionName: aws.String("Function4"), 423 | Runtime: lambdaTypes.RuntimeGo1x, 424 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 425 | }, 426 | }, nil, 427 | ) 428 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-2").Return( 429 | []lambdaTypes.FunctionConfiguration{ 430 | { 431 | FunctionName: aws.String("Function5"), 432 | Runtime: lambdaTypes.RuntimeNodejs, 433 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 434 | }, 435 | { 436 | FunctionName: aws.String("Function6"), 437 | Runtime: lambdaTypes.RuntimeNodejs18x, 438 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 439 | }, 440 | }, nil, 441 | ) 442 | }, 443 | want: [][]string{}, 444 | wantErr: false, 445 | }, 446 | { 447 | name: "CreateFunctionList success if any regions have empty function list", 448 | args: args{ 449 | ctx: context.Background(), 450 | targetRegions: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 451 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 452 | keyword: "", 453 | }, 454 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 455 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "ap-northeast-1").Return( 456 | []lambdaTypes.FunctionConfiguration{}, nil, 457 | ) 458 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 459 | []lambdaTypes.FunctionConfiguration{ 460 | { 461 | FunctionName: aws.String("Function3"), 462 | Runtime: lambdaTypes.RuntimeNodejs, 463 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 464 | }, 465 | { 466 | FunctionName: aws.String("Function4"), 467 | Runtime: lambdaTypes.RuntimeGo1x, 468 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 469 | }, 470 | }, nil, 471 | ) 472 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-2").Return( 473 | []lambdaTypes.FunctionConfiguration{ 474 | { 475 | FunctionName: aws.String("Function5"), 476 | Runtime: lambdaTypes.RuntimeNodejs, 477 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 478 | }, 479 | { 480 | FunctionName: aws.String("Function6"), 481 | Runtime: lambdaTypes.RuntimeNodejs18x, 482 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 483 | }, 484 | }, nil, 485 | ) 486 | }, 487 | want: [][]string{ 488 | {"nodejs18.x", "us-east-2", "Function6", "2022-12-22T09:47:43.728+0000"}, 489 | {"nodejs", "us-east-1", "Function3", "2022-12-21T09:47:43.728+0000"}, 490 | {"nodejs", "us-east-2", "Function5", "2022-12-21T09:47:43.728+0000"}, 491 | }, 492 | wantErr: false, 493 | }, 494 | } 495 | for _, tt := range tests { 496 | t.Run(tt.name, func(t *testing.T) { 497 | ctrl := gomock.NewController(t) 498 | lambdaClientMock := client.NewMockLambdaClient(ctrl) 499 | 500 | tt.prepareMockLambdaClientFn(lambdaClientMock) 501 | 502 | input := &CreateFunctionListInput{ 503 | Ctx: tt.args.ctx, 504 | TargetRegions: tt.args.targetRegions, 505 | TargetRuntime: tt.args.targetRuntime, 506 | Keyword: tt.args.keyword, 507 | Lambda: lambdaClientMock, 508 | } 509 | 510 | got, err := CreateFunctionList(input) 511 | if (err != nil) != tt.wantErr { 512 | t.Errorf("CreateFunctionList() error = %v, wantErr %v", err, tt.wantErr) 513 | return 514 | } 515 | if !reflect.DeepEqual(got, tt.want) && (len(got) != 0 || len(tt.want) != 0) { 516 | t.Errorf("CreateFunctionList() = %v, want %v", got, tt.want) 517 | } 518 | }) 519 | } 520 | } 521 | 522 | func Test_putToFunctionChannelByRegion(t *testing.T) { 523 | type args struct { 524 | ctx context.Context 525 | region string 526 | targetRuntime []string 527 | keyword string 528 | functionCh chan *types.LambdaFunctionData 529 | } 530 | 531 | tests := []struct { 532 | name string 533 | args args 534 | prepareMockLambdaClientFn func(m *client.MockLambdaClient) 535 | putCount int 536 | wantErr bool 537 | }{ 538 | { 539 | name: "putToFunctionChannelByRegion success", 540 | args: args{ 541 | ctx: context.Background(), 542 | region: "us-east-1", 543 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 544 | keyword: "", 545 | functionCh: make(chan *types.LambdaFunctionData), 546 | }, 547 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 548 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 549 | []lambdaTypes.FunctionConfiguration{ 550 | { 551 | FunctionName: aws.String("Function3"), 552 | Runtime: lambdaTypes.RuntimeNodejs, 553 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 554 | }, 555 | { 556 | FunctionName: aws.String("Function4"), 557 | Runtime: lambdaTypes.RuntimeGo1x, 558 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 559 | }, 560 | }, nil, 561 | ) 562 | }, 563 | putCount: 1, 564 | wantErr: false, 565 | }, 566 | { 567 | name: "putToFunctionChannelByRegion success if there is no corresponding runtime", 568 | args: args{ 569 | ctx: context.Background(), 570 | region: "us-east-1", 571 | targetRuntime: []string{"nodejs18.x"}, 572 | keyword: "", 573 | functionCh: make(chan *types.LambdaFunctionData), 574 | }, 575 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 576 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 577 | []lambdaTypes.FunctionConfiguration{ 578 | { 579 | FunctionName: aws.String("Function3"), 580 | Runtime: lambdaTypes.RuntimeNodejs, 581 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 582 | }, 583 | { 584 | FunctionName: aws.String("Function4"), 585 | Runtime: lambdaTypes.RuntimeGo1x, 586 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 587 | }, 588 | }, nil, 589 | ) 590 | }, 591 | putCount: 0, 592 | wantErr: false, 593 | }, 594 | { 595 | name: "putToFunctionChannelByRegion success if lower keywords given", 596 | args: args{ 597 | ctx: context.Background(), 598 | region: "us-east-1", 599 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 600 | keyword: "function3", 601 | functionCh: make(chan *types.LambdaFunctionData), 602 | }, 603 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 604 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 605 | []lambdaTypes.FunctionConfiguration{ 606 | { 607 | FunctionName: aws.String("Function3"), 608 | Runtime: lambdaTypes.RuntimeNodejs, 609 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 610 | }, 611 | { 612 | FunctionName: aws.String("Function4"), 613 | Runtime: lambdaTypes.RuntimeGo1x, 614 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 615 | }, 616 | }, nil, 617 | ) 618 | }, 619 | putCount: 1, 620 | wantErr: false, 621 | }, 622 | { 623 | name: "putToFunctionChannelByRegion success if upper keywords given", 624 | args: args{ 625 | ctx: context.Background(), 626 | region: "us-east-1", 627 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 628 | keyword: "FUNCTION3", 629 | functionCh: make(chan *types.LambdaFunctionData), 630 | }, 631 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 632 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 633 | []lambdaTypes.FunctionConfiguration{ 634 | { 635 | FunctionName: aws.String("Function3"), 636 | Runtime: lambdaTypes.RuntimeNodejs, 637 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 638 | }, 639 | { 640 | FunctionName: aws.String("Function4"), 641 | Runtime: lambdaTypes.RuntimeGo1x, 642 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 643 | }, 644 | }, nil, 645 | ) 646 | }, 647 | putCount: 1, 648 | wantErr: false, 649 | }, 650 | { 651 | name: "putToFunctionChannelByRegion success if there is no corresponding function matching the given region keywords", 652 | args: args{ 653 | ctx: context.Background(), 654 | region: "us-east-1", 655 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 656 | keyword: "any", 657 | functionCh: make(chan *types.LambdaFunctionData), 658 | }, 659 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 660 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 661 | []lambdaTypes.FunctionConfiguration{ 662 | { 663 | FunctionName: aws.String("Function3"), 664 | Runtime: lambdaTypes.RuntimeNodejs, 665 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 666 | }, 667 | { 668 | FunctionName: aws.String("Function4"), 669 | Runtime: lambdaTypes.RuntimeGo1x, 670 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 671 | }, 672 | }, nil, 673 | ) 674 | }, 675 | putCount: 0, 676 | wantErr: false, 677 | }, 678 | { 679 | name: "putToFunctionChannelByRegion success but there is no function", 680 | args: args{ 681 | ctx: context.Background(), 682 | region: "us-east-1", 683 | targetRuntime: []string{"nodejs18.x", "nodejs"}, 684 | keyword: "", 685 | functionCh: make(chan *types.LambdaFunctionData), 686 | }, 687 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 688 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 689 | []lambdaTypes.FunctionConfiguration{}, nil, 690 | ) 691 | }, 692 | putCount: 0, 693 | wantErr: false, 694 | }, 695 | { 696 | name: "putToFunctionChannelByRegion success but there is no targetRuntime", 697 | args: args{ 698 | ctx: context.Background(), 699 | region: "us-east-1", 700 | targetRuntime: []string{}, 701 | keyword: "", 702 | functionCh: make(chan *types.LambdaFunctionData), 703 | }, 704 | prepareMockLambdaClientFn: func(m *client.MockLambdaClient) { 705 | m.EXPECT().ListFunctionsWithRegion(gomock.Any(), "us-east-1").Return( 706 | []lambdaTypes.FunctionConfiguration{ 707 | { 708 | FunctionName: aws.String("Function3"), 709 | Runtime: lambdaTypes.RuntimeNodejs, 710 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 711 | }, 712 | { 713 | FunctionName: aws.String("Function4"), 714 | Runtime: lambdaTypes.RuntimeGo1x, 715 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 716 | }, 717 | }, nil, 718 | ) 719 | }, 720 | putCount: 0, 721 | wantErr: false, 722 | }, 723 | } 724 | for _, tt := range tests { 725 | t.Run(tt.name, func(t *testing.T) { 726 | ctrl := gomock.NewController(t) 727 | lambdaClientMock := client.NewMockLambdaClient(ctrl) 728 | 729 | tt.prepareMockLambdaClientFn(lambdaClientMock) 730 | 731 | putCount := 0 732 | ctx, cancel := context.WithCancel(tt.args.ctx) 733 | ch := tt.args.functionCh 734 | wg := sync.WaitGroup{} 735 | 736 | wg.Add(1) 737 | go func() { 738 | defer wg.Done() 739 | for { 740 | select { 741 | case <-ctx.Done(): 742 | return 743 | case <-ch: 744 | putCount++ 745 | } 746 | } 747 | }() 748 | 749 | if err := putToFunctionChannelByRegion(ctx, tt.args.region, tt.args.targetRuntime, tt.args.keyword, ch, lambdaClientMock); (err != nil) != tt.wantErr { 750 | t.Errorf("putToFunctionChannelByRegion() error = %v, wantErr %v", err, tt.wantErr) 751 | cancel() 752 | return 753 | } 754 | cancel() 755 | wg.Wait() 756 | if !tt.wantErr && putCount != tt.putCount { 757 | t.Errorf("putCount = %v, tt.putCount %v", putCount, tt.putCount) 758 | } 759 | }) 760 | } 761 | } 762 | 763 | func Test_sortAndSetFunctionList(t *testing.T) { 764 | type args struct { 765 | regionList []string 766 | runtimeList []string 767 | functionMap map[string]map[string][][]string 768 | } 769 | tests := []struct { 770 | name string 771 | args args 772 | want [][]string 773 | }{ 774 | { 775 | name: "sortAndSetFunctionList success", 776 | args: args{ 777 | regionList: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 778 | runtimeList: []string{"nodejs", "nodejs18.x"}, 779 | functionMap: map[string]map[string][][]string{ 780 | "nodejs": { 781 | "ap-northeast-1": { 782 | []string{"Function1", "2022-12-21T09:47:43.728+0000"}, 783 | }, 784 | "us-east-1": { 785 | []string{"Function1", "2022-12-21T09:47:43.728+0000"}, 786 | }, 787 | }, 788 | "nodejs18.x": { 789 | "ap-northeast-1": { 790 | []string{"Function3", "2022-12-22T09:47:43.728+0000"}, 791 | }, 792 | "us-east-2": { 793 | []string{"Function3", "2022-12-22T09:47:43.728+0000"}, 794 | }, 795 | }, 796 | }, 797 | }, 798 | want: [][]string{ 799 | {"nodejs", "ap-northeast-1", "Function1", "2022-12-21T09:47:43.728+0000"}, 800 | {"nodejs", "us-east-1", "Function1", "2022-12-21T09:47:43.728+0000"}, 801 | {"nodejs18.x", "ap-northeast-1", "Function3", "2022-12-22T09:47:43.728+0000"}, 802 | {"nodejs18.x", "us-east-2", "Function3", "2022-12-22T09:47:43.728+0000"}, 803 | }, 804 | }, 805 | { 806 | name: "sortAndSetFunctionList success with sorted by function names per regions and runtime", 807 | args: args{ 808 | regionList: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 809 | runtimeList: []string{"nodejs", "nodejs18.x"}, 810 | functionMap: map[string]map[string][][]string{ 811 | "nodejs": { 812 | "ap-northeast-1": { 813 | []string{"Function-A", "2022-12-21T09:47:43.728+0000"}, 814 | []string{"Function", "2022-12-21T09:47:43.728+0000"}, 815 | []string{"Function-c", "2022-12-21T09:47:43.728+0000"}, 816 | []string{"Function-b", "2022-12-21T09:47:43.728+0000"}, 817 | []string{"Function-B", "2022-12-21T09:47:43.728+0000"}, 818 | []string{"Function-a", "2022-12-21T09:47:43.728+0000"}, 819 | []string{"Function-1", "2022-12-21T09:47:43.728+0000"}, 820 | }, 821 | "us-east-1": { 822 | []string{"Function-b-1", "2022-12-21T09:47:43.728+0000"}, 823 | []string{"Function-a-2", "2022-12-21T09:47:43.728+0000"}, 824 | }, 825 | }, 826 | "nodejs18.x": { 827 | "ap-northeast-1": { 828 | []string{"Function-a-3", "2022-12-21T09:47:43.728+0000"}, 829 | []string{"Function-a-0", "2022-12-21T09:47:43.728+0000"}, 830 | }, 831 | }, 832 | }, 833 | }, 834 | want: [][]string{ 835 | {"nodejs", "ap-northeast-1", "Function", "2022-12-21T09:47:43.728+0000"}, 836 | {"nodejs", "ap-northeast-1", "Function-1", "2022-12-21T09:47:43.728+0000"}, 837 | {"nodejs", "ap-northeast-1", "Function-A", "2022-12-21T09:47:43.728+0000"}, 838 | {"nodejs", "ap-northeast-1", "Function-B", "2022-12-21T09:47:43.728+0000"}, 839 | {"nodejs", "ap-northeast-1", "Function-a", "2022-12-21T09:47:43.728+0000"}, 840 | {"nodejs", "ap-northeast-1", "Function-b", "2022-12-21T09:47:43.728+0000"}, 841 | {"nodejs", "ap-northeast-1", "Function-c", "2022-12-21T09:47:43.728+0000"}, 842 | {"nodejs", "us-east-1", "Function-a-2", "2022-12-21T09:47:43.728+0000"}, 843 | {"nodejs", "us-east-1", "Function-b-1", "2022-12-21T09:47:43.728+0000"}, 844 | {"nodejs18.x", "ap-northeast-1", "Function-a-0", "2022-12-21T09:47:43.728+0000"}, 845 | {"nodejs18.x", "ap-northeast-1", "Function-a-3", "2022-12-21T09:47:43.728+0000"}, 846 | }, 847 | }, 848 | { 849 | name: "sortAndSetFunctionList success if runtimeList is empty", 850 | args: args{ 851 | regionList: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 852 | runtimeList: []string{}, 853 | functionMap: map[string]map[string][][]string{ 854 | "nodejs": { 855 | "ap-northeast-1": { 856 | []string{"Function1", "2022-12-21T09:47:43.728+0000"}, 857 | }, 858 | "us-east-1": { 859 | []string{"Function1", "2022-12-21T09:47:43.728+0000"}, 860 | }, 861 | }, 862 | "nodejs18.x": { 863 | "ap-northeast-1": { 864 | []string{"Function3", "2022-12-22T09:47:43.728+0000"}, 865 | }, 866 | "us-east-2": { 867 | []string{"Function3", "2022-12-22T09:47:43.728+0000"}, 868 | }, 869 | }, 870 | }, 871 | }, 872 | want: [][]string{}, 873 | }, 874 | { 875 | name: "sortAndSetFunctionList success if regionList is empty", 876 | args: args{ 877 | regionList: []string{}, 878 | runtimeList: []string{"nodejs", "nodejs18.x"}, 879 | functionMap: map[string]map[string][][]string{ 880 | "nodejs": { 881 | "ap-northeast-1": { 882 | []string{"Function1", "2022-12-21T09:47:43.728+0000"}, 883 | }, 884 | "us-east-1": { 885 | []string{"Function1", "2022-12-21T09:47:43.728+0000"}, 886 | }, 887 | }, 888 | "nodejs18.x": { 889 | "ap-northeast-1": { 890 | []string{"Function3", "2022-12-22T09:47:43.728+0000"}, 891 | }, 892 | "us-east-2": { 893 | []string{"Function3", "2022-12-22T09:47:43.728+0000"}, 894 | }, 895 | }, 896 | }, 897 | }, 898 | want: [][]string{}, 899 | }, 900 | { 901 | name: "sortAndSetFunctionList success if functionMap is empty", 902 | args: args{ 903 | regionList: []string{"ap-northeast-1", "us-east-1", "us-east-2"}, 904 | runtimeList: []string{"nodejs", "nodejs18.x"}, 905 | functionMap: map[string]map[string][][]string{}, 906 | }, 907 | want: [][]string{}, 908 | }, 909 | } 910 | for _, tt := range tests { 911 | t.Run(tt.name, func(t *testing.T) { 912 | got := sortAndSetFunctionList(tt.args.regionList, tt.args.runtimeList, tt.args.functionMap) 913 | if !reflect.DeepEqual(got, tt.want) && (len(got) != 0 || len(tt.want) != 0) { 914 | t.Errorf("sortAndSetFunctionList() = %v, want %v", got, tt.want) 915 | } 916 | }) 917 | } 918 | } 919 | -------------------------------------------------------------------------------- /internal/action/main_test.go: -------------------------------------------------------------------------------- 1 | package action 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: action ============") 14 | fmt.Println("==========================================") 15 | goleak.VerifyTestMain(m) 16 | } 17 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/go-to-k/lamver/internal/action" 8 | "github.com/go-to-k/lamver/internal/io" 9 | "github.com/go-to-k/lamver/internal/types" 10 | "github.com/go-to-k/lamver/pkg/client" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/service/ec2" 14 | "github.com/aws/aws-sdk-go-v2/service/lambda" 15 | "github.com/urfave/cli/v2" 16 | ) 17 | 18 | const SDKRetryMaxAttempts = 3 19 | 20 | type App struct { 21 | Cli *cli.App 22 | Profile string 23 | DefaultRegion string 24 | CSVOutputFilePath string 25 | FunctionNameKeyword string 26 | } 27 | 28 | func NewApp(version string) *App { 29 | app := App{} 30 | 31 | app.Cli = &cli.App{ 32 | Name: "lamver", 33 | Usage: "CLI tool to search Lambda runtime and versions.", 34 | Flags: []cli.Flag{ 35 | &cli.StringFlag{ 36 | Name: "profile", 37 | Aliases: []string{"p"}, 38 | Usage: "AWS profile name", 39 | Destination: &app.Profile, 40 | }, 41 | &cli.StringFlag{ 42 | Name: "region", 43 | Aliases: []string{"r"}, 44 | Usage: "AWS default region", 45 | Destination: &app.DefaultRegion, 46 | }, 47 | &cli.StringFlag{ 48 | Name: "output", 49 | Aliases: []string{"o"}, 50 | Usage: "Output file path for CSV format", 51 | Destination: &app.CSVOutputFilePath, 52 | }, 53 | &cli.StringFlag{ 54 | Name: "keyword", 55 | Aliases: []string{"k"}, 56 | Usage: "Keyword for function name filtering (case-insensitive)", 57 | Destination: &app.FunctionNameKeyword, 58 | }, 59 | }, 60 | } 61 | 62 | app.Cli.Version = version 63 | app.Cli.Action = app.getAction() 64 | app.Cli.HideHelpCommand = true 65 | 66 | return &app 67 | } 68 | 69 | func (a *App) Run(ctx context.Context) error { 70 | return a.Cli.RunContext(ctx, os.Args) 71 | } 72 | 73 | func (a *App) getAction() func(c *cli.Context) error { 74 | return func(c *cli.Context) error { 75 | cfg, err := client.LoadAWSConfig(c.Context, a.DefaultRegion, a.Profile) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | lambdaClient := client.NewLambda( 81 | lambda.NewFromConfig(cfg, func(o *lambda.Options) { 82 | o.RetryMaxAttempts = SDKRetryMaxAttempts 83 | o.RetryMode = aws.RetryModeStandard 84 | }), 85 | ) 86 | 87 | ec2Client := client.NewEC2( 88 | ec2.NewFromConfig(cfg, func(o *ec2.Options) { 89 | o.RetryMaxAttempts = SDKRetryMaxAttempts 90 | o.RetryMode = aws.RetryModeStandard 91 | }), 92 | ) 93 | 94 | getAllRegionsAndRuntimeInput := &action.GetAllRegionsAndRuntimeInput{ 95 | Ctx: c.Context, 96 | EC2: ec2Client, 97 | Lambda: lambdaClient, 98 | DefaultRegion: a.DefaultRegion, 99 | } 100 | allRegions, allRuntime, err := action.GetAllRegionsAndRuntime(getAllRegionsAndRuntimeInput) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | regionsLabel := []string{"Select regions you want to search."} 106 | targetRegions, continuation, err := io.GetCheckboxes(regionsLabel, allRegions) 107 | if err != nil { 108 | return err 109 | } 110 | if !continuation { 111 | return nil 112 | } 113 | 114 | runtimeLabel := []string{"Select runtime values you want to search."} 115 | targetRuntime, continuation, err := io.GetCheckboxes(runtimeLabel, allRuntime) 116 | if err != nil { 117 | return err 118 | } 119 | if !continuation { 120 | return nil 121 | } 122 | 123 | var keyword string 124 | if a.FunctionNameKeyword != "" { 125 | keyword = a.FunctionNameKeyword 126 | } else { 127 | keywordLabel := "Filter a keyword of function names(case-insensitive): " 128 | keyword = io.InputKeywordForFilter(keywordLabel) 129 | } 130 | 131 | createFunctionListInput := &action.CreateFunctionListInput{ 132 | Ctx: c.Context, 133 | TargetRegions: targetRegions, 134 | TargetRuntime: targetRuntime, 135 | Keyword: keyword, 136 | Lambda: lambdaClient, 137 | } 138 | functionList, err := action.CreateFunctionList(createFunctionListInput) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | functionHeader := types.GetLambdaFunctionDataKeys() 144 | if err := io.OutputResult(functionHeader, functionList, a.CSVOutputFilePath); err != nil { 145 | return err 146 | } 147 | 148 | return nil 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /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) ([]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: 32 | Logger.Warn().Msg("Not selected!") 33 | } 34 | if len(checkboxes) == 0 || ui.IsCanceled { 35 | ok := GetYesNo("Do you want to finish?") 36 | if ok { 37 | Logger.Info().Msg("Finished...") 38 | return checkboxes, false, nil 39 | } 40 | continue 41 | } 42 | 43 | fmt.Fprintf(os.Stderr, " %s\n", color.CyanString(strings.Join(checkboxes, ", "))) 44 | 45 | ok := GetYesNo("OK?") 46 | if ok { 47 | return checkboxes, true, nil 48 | } 49 | } 50 | } 51 | 52 | func InputKeywordForFilter(label string) string { 53 | reader := bufio.NewReader(os.Stdin) 54 | 55 | fmt.Fprintf(os.Stderr, "%s", label) 56 | s, _ := reader.ReadString('\n') 57 | fmt.Fprintln(os.Stderr) 58 | 59 | s = strings.TrimSpace(s) 60 | 61 | return s 62 | } 63 | 64 | func GetYesNo(label string) bool { 65 | choices := "Y/n" 66 | r := bufio.NewReader(os.Stdin) 67 | var s string 68 | 69 | for { 70 | fmt.Fprintf(os.Stderr, "%s (%s) ", label, choices) 71 | s, _ = r.ReadString('\n') 72 | fmt.Fprintln(os.Stderr) 73 | 74 | s = strings.TrimSpace(s) 75 | if s == "" { 76 | return true 77 | } 78 | s = strings.ToLower(s) 79 | if s == "y" || s == "yes" { 80 | return true 81 | } 82 | if s == "n" || s == "no" { 83 | return false 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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/output.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/olekukonko/tablewriter" 10 | ) 11 | 12 | func OutputResult(functionHeader []string, functionData [][]string, csvOutputFilePath string) error { 13 | if csvOutputFilePath != "" { 14 | if err := outputAsCSV(functionHeader, functionData, csvOutputFilePath); err != nil { 15 | return err 16 | } 17 | } else { 18 | if err := outputAsTable(functionHeader, functionData); err != nil { 19 | return err 20 | } 21 | } 22 | 23 | Logger.Info().Msgf("%d counts hit! ", len(functionData)) 24 | 25 | return nil 26 | } 27 | 28 | func outputAsTable(header []string, data [][]string) error { 29 | tableString := &strings.Builder{} 30 | table := tablewriter.NewWriter(tableString) 31 | table.SetHeader(header) 32 | table.SetRowLine(true) 33 | table.SetAutoWrapText(false) 34 | table.AppendBulk(data) 35 | table.Render() 36 | 37 | stringAsTableFormat := tableString.String() 38 | 39 | fmt.Fprintf(os.Stderr, "%s", stringAsTableFormat) 40 | 41 | return nil 42 | } 43 | 44 | func outputAsCSV(header []string, data [][]string, csvOutputFilePath string) error { 45 | file, err := os.Create(csvOutputFilePath) 46 | if err != nil { 47 | return err 48 | } 49 | defer file.Close() 50 | 51 | w := csv.NewWriter(file) 52 | var outputData [][]string 53 | 54 | outputData = append(outputData, header) 55 | outputData = append(outputData, data...) 56 | 57 | if err := w.WriteAll(outputData); err != nil { 58 | return err 59 | } 60 | 61 | if err := w.Error(); err != nil { 62 | return err 63 | } 64 | 65 | Logger.Info().Msg("Finished writing output!") 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /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/types/lambda_function_data.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type LambdaFunctionData struct { 4 | Runtime string 5 | Region string 6 | FunctionName string 7 | LastModified string 8 | } 9 | 10 | func GetLambdaFunctionDataKeys() []string { 11 | return []string{"Runtime", "Region", "FunctionName", "LastModified"} 12 | } 13 | -------------------------------------------------------------------------------- /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 | func Test_IsDebug(t *testing.T) { 8 | type args struct { 9 | Version string 10 | Revision string 11 | } 12 | 13 | cases := []struct { 14 | name string 15 | args args 16 | want bool 17 | wantErr bool 18 | }{ 19 | { 20 | name: "true if both Version and Revision are empty", 21 | args: args{ 22 | Version: "", 23 | Revision: "", 24 | }, 25 | want: true, 26 | wantErr: false, 27 | }, 28 | { 29 | name: "true if Version is empty and Revision is not empty", 30 | args: args{ 31 | Version: "", 32 | Revision: "abcde", 33 | }, 34 | want: true, 35 | wantErr: false, 36 | }, 37 | { 38 | name: "false if Revision is not empty and Revision is empty", 39 | args: args{ 40 | Version: "v1.0.0", 41 | Revision: "", 42 | }, 43 | want: false, 44 | wantErr: false, 45 | }, 46 | { 47 | name: "true if both Version and Revision are not empty", 48 | args: args{ 49 | Version: "v1.0.0", 50 | Revision: "abcde", 51 | }, 52 | want: true, 53 | wantErr: false, 54 | }, 55 | } 56 | 57 | for _, tt := range cases { 58 | t.Run(tt.name, func(t *testing.T) { 59 | Version = tt.args.Version 60 | Revision = tt.args.Revision 61 | got := IsDebug() 62 | 63 | if got != tt.want { 64 | t.Errorf("got = %#v, want %#v", got, tt.want) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func Test_GetVersion(t *testing.T) { 71 | type args struct { 72 | Version string 73 | Revision string 74 | } 75 | 76 | cases := []struct { 77 | name string 78 | args args 79 | want string 80 | wantErr bool 81 | }{ 82 | { 83 | name: "Both Version and Revision are empty", 84 | args: args{ 85 | Version: "", 86 | Revision: "", 87 | }, 88 | want: "", 89 | wantErr: false, 90 | }, 91 | { 92 | name: "Version is empty and Revision is not empty", 93 | args: args{ 94 | Version: "", 95 | Revision: "abcde", 96 | }, 97 | want: "", 98 | wantErr: false, 99 | }, 100 | { 101 | name: "Revision is not empty and Revision is empty", 102 | args: args{ 103 | Version: "v1.0.0", 104 | Revision: "", 105 | }, 106 | want: "v1.0.0", 107 | wantErr: false, 108 | }, 109 | { 110 | name: "Both Version and Revision are not empty", 111 | args: args{ 112 | Version: "v1.0.0", 113 | Revision: "abcde", 114 | }, 115 | want: "v1.0.0-abcde", 116 | wantErr: false, 117 | }, 118 | } 119 | 120 | for _, tt := range cases { 121 | t.Run(tt.name, func(t *testing.T) { 122 | Version = tt.args.Version 123 | Revision = tt.args.Revision 124 | got := GetVersion() 125 | 126 | if got != tt.want { 127 | t.Errorf("got = %#v, want %#v", got, tt.want) 128 | } 129 | }) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /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/ec2.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=ec2_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "sort" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/ec2" 9 | ) 10 | 11 | type EC2Client interface { 12 | DescribeRegions(ctx context.Context) ([]string, error) 13 | } 14 | 15 | type EC2 struct { 16 | client *ec2.Client 17 | } 18 | 19 | var _ EC2Client = (*EC2)(nil) 20 | 21 | func NewEC2(client *ec2.Client) *EC2 { 22 | return &EC2{ 23 | client: client, 24 | } 25 | } 26 | func (c *EC2) DescribeRegions(ctx context.Context) ([]string, error) { 27 | outputRegions := []string{} 28 | input := &ec2.DescribeRegionsInput{} 29 | 30 | output, err := c.client.DescribeRegions(ctx, input) 31 | if err != nil { 32 | return outputRegions, err 33 | } 34 | 35 | for _, region := range output.Regions { 36 | outputRegions = append(outputRegions, *region.RegionName) 37 | } 38 | 39 | sort.Strings(outputRegions) 40 | return outputRegions, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/client/ec2_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ec2.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=ec2.go -destination=ec2_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 | // MockEC2Client is a mock of EC2Client interface. 17 | type MockEC2Client struct { 18 | ctrl *gomock.Controller 19 | recorder *MockEC2ClientMockRecorder 20 | } 21 | 22 | // MockEC2ClientMockRecorder is the mock recorder for MockEC2Client. 23 | type MockEC2ClientMockRecorder struct { 24 | mock *MockEC2Client 25 | } 26 | 27 | // NewMockEC2Client creates a new mock instance. 28 | func NewMockEC2Client(ctrl *gomock.Controller) *MockEC2Client { 29 | mock := &MockEC2Client{ctrl: ctrl} 30 | mock.recorder = &MockEC2ClientMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockEC2Client) EXPECT() *MockEC2ClientMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // DescribeRegions mocks base method. 40 | func (m *MockEC2Client) DescribeRegions(ctx context.Context) ([]string, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "DescribeRegions", ctx) 43 | ret0, _ := ret[0].([]string) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // DescribeRegions indicates an expected call of DescribeRegions. 49 | func (mr *MockEC2ClientMockRecorder) DescribeRegions(ctx any) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRegions", reflect.TypeOf((*MockEC2Client)(nil).DescribeRegions), ctx) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/client/ec2_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/ec2" 12 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 13 | "github.com/aws/smithy-go/middleware" 14 | ) 15 | 16 | func TestEC2_DescribeRegions(t *testing.T) { 17 | type args struct { 18 | ctx context.Context 19 | withAPIOptionsFunc func(*middleware.Stack) error 20 | } 21 | tests := []struct { 22 | name string 23 | args args 24 | want []string 25 | wantErr bool 26 | }{ 27 | { 28 | name: "DescribeRegions success", 29 | args: args{ 30 | ctx: context.Background(), 31 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 32 | return stack.Finalize.Add( 33 | middleware.FinalizeMiddlewareFunc( 34 | "DescribeRegionsMock", 35 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 36 | return middleware.FinalizeOutput{ 37 | Result: &ec2.DescribeRegionsOutput{ 38 | Regions: []types.Region{ 39 | { 40 | RegionName: aws.String("ap-northeast-1"), 41 | }, 42 | { 43 | RegionName: aws.String("us-east-1"), 44 | }, 45 | }, 46 | }, 47 | }, middleware.Metadata{}, nil 48 | }, 49 | ), 50 | middleware.Before, 51 | ) 52 | }, 53 | }, 54 | want: []string{ 55 | "ap-northeast-1", 56 | "us-east-1", 57 | }, 58 | wantErr: false, 59 | }, 60 | { 61 | name: "DescribeRegions sorted success", 62 | args: args{ 63 | ctx: context.Background(), 64 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 65 | return stack.Finalize.Add( 66 | middleware.FinalizeMiddlewareFunc( 67 | "DescribeRegionsUnSortedMock", 68 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 69 | return middleware.FinalizeOutput{ 70 | Result: &ec2.DescribeRegionsOutput{ 71 | Regions: []types.Region{ 72 | { 73 | RegionName: aws.String("us-east-1"), 74 | }, 75 | { 76 | RegionName: aws.String("ap-northeast-1"), 77 | }, 78 | }, 79 | }, 80 | }, middleware.Metadata{}, nil 81 | }, 82 | ), 83 | middleware.Before, 84 | ) 85 | }, 86 | }, 87 | want: []string{ // sort by region name 88 | "ap-northeast-1", 89 | "us-east-1", 90 | }, 91 | wantErr: false, 92 | }, 93 | { 94 | name: "DescribeRegions fail", 95 | args: args{ 96 | ctx: context.Background(), 97 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 98 | return stack.Finalize.Add( 99 | middleware.FinalizeMiddlewareFunc( 100 | "DescribeRegionsErrorMock", 101 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 102 | return middleware.FinalizeOutput{ 103 | Result: &ec2.DescribeRegionsOutput{}, 104 | }, middleware.Metadata{}, fmt.Errorf("DescribeRegionsError") 105 | }, 106 | ), 107 | middleware.Before, 108 | ) 109 | }, 110 | }, 111 | want: []string{}, 112 | wantErr: true, 113 | }, 114 | } 115 | for _, tt := range tests { 116 | t.Run(tt.name, func(t *testing.T) { 117 | cfg, err := config.LoadDefaultConfig( 118 | tt.args.ctx, 119 | config.WithRegion("ap-northeast-1"), 120 | config.WithAPIOptions([]func(*middleware.Stack) error{tt.args.withAPIOptionsFunc}), 121 | ) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | client := ec2.NewFromConfig(cfg) 127 | ec2Client := NewEC2(client) 128 | 129 | got, err := ec2Client.DescribeRegions(tt.args.ctx) 130 | if (err != nil) != tt.wantErr { 131 | t.Errorf("EC2.DescribeRegions() error = %v, wantErr %v", err, tt.wantErr) 132 | return 133 | } 134 | if !reflect.DeepEqual(got, tt.want) { 135 | t.Errorf("EC2.DescribeRegions() = %v, want %v", got, tt.want) 136 | } 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pkg/client/lambda.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=$GOFILE -destination=lambda_mock.go -package=$GOPACKAGE -write_package_comment=false 2 | package client 3 | 4 | import ( 5 | "context" 6 | "regexp" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/aws/aws-sdk-go-v2/service/lambda" 12 | "github.com/aws/aws-sdk-go-v2/service/lambda/types" 13 | ) 14 | 15 | type LambdaClient interface { 16 | ListFunctions(ctx context.Context) ([]types.FunctionConfiguration, error) 17 | ListFunctionsWithRegion(ctx context.Context, region string) ([]types.FunctionConfiguration, error) 18 | ListRuntimeValues() []string 19 | } 20 | 21 | type Lambda struct { 22 | client *lambda.Client 23 | } 24 | 25 | var _ LambdaClient = (*Lambda)(nil) 26 | 27 | func NewLambda(client *lambda.Client) *Lambda { 28 | return &Lambda{ 29 | client: client, 30 | } 31 | } 32 | 33 | func (c *Lambda) ListFunctions(ctx context.Context) ([]types.FunctionConfiguration, error) { 34 | return c.ListFunctionsWithRegion(ctx, "") 35 | } 36 | 37 | func (c *Lambda) ListFunctionsWithRegion(ctx context.Context, region string) ([]types.FunctionConfiguration, error) { 38 | var nextMarker *string 39 | outputs := []types.FunctionConfiguration{} 40 | 41 | var optFns func(*lambda.Options) 42 | if region != "" { 43 | optFns = func(o *lambda.Options) { 44 | o.Region = region 45 | } 46 | } 47 | 48 | for { 49 | input := &lambda.ListFunctionsInput{ 50 | Marker: nextMarker, 51 | } 52 | 53 | var ( 54 | output *lambda.ListFunctionsOutput 55 | err error 56 | ) 57 | 58 | if region == "" { 59 | output, err = c.client.ListFunctions(ctx, input) 60 | } else { 61 | output, err = c.client.ListFunctions(ctx, input, optFns) 62 | } 63 | if err != nil { 64 | return outputs, err 65 | } 66 | 67 | outputs = append(outputs, output.Functions...) 68 | 69 | nextMarker = output.NextMarker 70 | 71 | if nextMarker == nil { 72 | break 73 | } 74 | } 75 | 76 | return outputs, nil 77 | } 78 | 79 | func (c *Lambda) ListRuntimeValues() []string { 80 | var r types.Runtime 81 | runtimeStrList := []string{} 82 | runtimeList := r.Values() 83 | 84 | sort.Slice(runtimeList, func(i, j int) bool { 85 | first := string(runtimeList[i]) 86 | second := string(runtimeList[j]) 87 | 88 | firstRuntime, firstVersion, firstRest := c.splitVersion(first) 89 | secondRuntime, secondVersion, secondRest := c.splitVersion(second) 90 | 91 | if firstRuntime != secondRuntime { 92 | return firstRuntime < secondRuntime 93 | } 94 | 95 | if hasFinished, shouldSorted := c.compareActualVersion(firstVersion, secondVersion); hasFinished { 96 | return shouldSorted 97 | } 98 | 99 | if firstRest == "" { 100 | return true 101 | } 102 | if secondRest == "" { 103 | return false 104 | } 105 | 106 | return firstRest < secondRest 107 | }) 108 | 109 | for _, runtime := range runtimeList { 110 | runtimeStrList = append(runtimeStrList, string(runtime)) 111 | } 112 | 113 | return runtimeStrList 114 | } 115 | 116 | func (c *Lambda) splitVersion(runtimeStr string) (string, string, string) { 117 | r := regexp.MustCompile(`^(\D+)([\d\.]+)?(.*)?$`) 118 | matches := r.FindStringSubmatch(runtimeStr) 119 | 120 | runtime := "" 121 | version := "" 122 | rest := "" 123 | 124 | if len(matches) > 1 { 125 | runtime = matches[1] 126 | } 127 | if len(matches) > 2 { 128 | version = matches[2] 129 | } 130 | if len(matches) > 3 { 131 | rest = matches[3] 132 | } 133 | 134 | return runtime, version, rest 135 | } 136 | 137 | func (c *Lambda) compareActualVersion(first string, second string) (hasFinished bool, shouldSorted bool) { 138 | if first == "" { 139 | return true, true 140 | } 141 | if second == "" { 142 | return true, false 143 | } 144 | 145 | if first[:len(first)-1] == "." { 146 | first = first[len(first)-1:] 147 | } 148 | if second[:len(second)-1] == "." { 149 | second = second[len(second)-1:] 150 | } 151 | 152 | firstIntegers := first 153 | firstDecimals := "" 154 | secondIntegers := second 155 | secondDecimals := "" 156 | 157 | if before, after, ok := strings.Cut(first, "."); ok { 158 | firstIntegers = before 159 | firstDecimals = after 160 | } 161 | if before, after, ok := strings.Cut(second, "."); ok { 162 | secondIntegers = before 163 | secondDecimals = after 164 | } 165 | 166 | if firstIntegers != secondIntegers { 167 | fInt, _ := strconv.Atoi(firstIntegers) 168 | sInt, _ := strconv.Atoi(secondIntegers) 169 | return true, fInt < sInt 170 | } 171 | 172 | if firstDecimals == "" && secondDecimals != "" { 173 | return true, true 174 | } 175 | if firstDecimals != "" && secondDecimals == "" { 176 | return true, false 177 | } 178 | if firstDecimals != secondDecimals { 179 | fDec, _ := strconv.Atoi(firstDecimals) 180 | sDec, _ := strconv.Atoi(secondDecimals) 181 | return true, fDec < sDec 182 | } 183 | 184 | return false, false 185 | } 186 | -------------------------------------------------------------------------------- /pkg/client/lambda_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: lambda.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=lambda.go -destination=lambda_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/lambda/types" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockLambdaClient is a mock of LambdaClient interface. 18 | type MockLambdaClient struct { 19 | ctrl *gomock.Controller 20 | recorder *MockLambdaClientMockRecorder 21 | } 22 | 23 | // MockLambdaClientMockRecorder is the mock recorder for MockLambdaClient. 24 | type MockLambdaClientMockRecorder struct { 25 | mock *MockLambdaClient 26 | } 27 | 28 | // NewMockLambdaClient creates a new mock instance. 29 | func NewMockLambdaClient(ctrl *gomock.Controller) *MockLambdaClient { 30 | mock := &MockLambdaClient{ctrl: ctrl} 31 | mock.recorder = &MockLambdaClientMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockLambdaClient) EXPECT() *MockLambdaClientMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // ListFunctions mocks base method. 41 | func (m *MockLambdaClient) ListFunctions(ctx context.Context) ([]types.FunctionConfiguration, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "ListFunctions", ctx) 44 | ret0, _ := ret[0].([]types.FunctionConfiguration) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // ListFunctions indicates an expected call of ListFunctions. 50 | func (mr *MockLambdaClientMockRecorder) ListFunctions(ctx any) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFunctions", reflect.TypeOf((*MockLambdaClient)(nil).ListFunctions), ctx) 53 | } 54 | 55 | // ListFunctionsWithRegion mocks base method. 56 | func (m *MockLambdaClient) ListFunctionsWithRegion(ctx context.Context, region string) ([]types.FunctionConfiguration, error) { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "ListFunctionsWithRegion", ctx, region) 59 | ret0, _ := ret[0].([]types.FunctionConfiguration) 60 | ret1, _ := ret[1].(error) 61 | return ret0, ret1 62 | } 63 | 64 | // ListFunctionsWithRegion indicates an expected call of ListFunctionsWithRegion. 65 | func (mr *MockLambdaClientMockRecorder) ListFunctionsWithRegion(ctx, region any) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFunctionsWithRegion", reflect.TypeOf((*MockLambdaClient)(nil).ListFunctionsWithRegion), ctx, region) 68 | } 69 | 70 | // ListRuntimeValues mocks base method. 71 | func (m *MockLambdaClient) ListRuntimeValues() []string { 72 | m.ctrl.T.Helper() 73 | ret := m.ctrl.Call(m, "ListRuntimeValues") 74 | ret0, _ := ret[0].([]string) 75 | return ret0 76 | } 77 | 78 | // ListRuntimeValues indicates an expected call of ListRuntimeValues. 79 | func (mr *MockLambdaClientMockRecorder) ListRuntimeValues() *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRuntimeValues", reflect.TypeOf((*MockLambdaClient)(nil).ListRuntimeValues)) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/client/lambda_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/lambda" 12 | "github.com/aws/aws-sdk-go-v2/service/lambda/types" 13 | "github.com/aws/smithy-go/middleware" 14 | ) 15 | 16 | type markerKey struct{} 17 | 18 | func getNextMarkerForInitialize( 19 | ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler, 20 | ) ( 21 | out middleware.InitializeOutput, metadata middleware.Metadata, err error, 22 | ) { 23 | //nolint:gocritic 24 | switch v := in.Parameters.(type) { 25 | case *lambda.ListFunctionsInput: 26 | ctx = middleware.WithStackValue(ctx, markerKey{}, v.Marker) 27 | } 28 | return next.HandleInitialize(ctx, in) 29 | } 30 | 31 | func TestLambda_ListFunctionsWithRegion(t *testing.T) { 32 | type args struct { 33 | ctx context.Context 34 | region string 35 | withAPIOptionsFunc func(*middleware.Stack) error 36 | } 37 | tests := []struct { 38 | name string 39 | args args 40 | want []types.FunctionConfiguration 41 | wantErr bool 42 | }{ 43 | { 44 | name: "ListFunctionsWithRegion success", 45 | args: args{ 46 | ctx: context.Background(), 47 | region: "us-east-1", 48 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 49 | return stack.Finalize.Add( 50 | middleware.FinalizeMiddlewareFunc( 51 | "ListFunctionsMock", 52 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 53 | return middleware.FinalizeOutput{ 54 | Result: &lambda.ListFunctionsOutput{ 55 | NextMarker: nil, 56 | Functions: []types.FunctionConfiguration{ 57 | { 58 | FunctionName: aws.String("Function1"), 59 | Runtime: types.RuntimeNodejs, 60 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 61 | }, 62 | { 63 | FunctionName: aws.String("Function2"), 64 | Runtime: types.RuntimeNodejs18x, 65 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 66 | }, 67 | }, 68 | }, 69 | }, middleware.Metadata{}, nil 70 | }, 71 | ), 72 | middleware.Before, 73 | ) 74 | }, 75 | }, 76 | want: []types.FunctionConfiguration{ 77 | { 78 | FunctionName: aws.String("Function1"), 79 | Runtime: types.RuntimeNodejs, 80 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 81 | }, 82 | { 83 | FunctionName: aws.String("Function2"), 84 | Runtime: types.RuntimeNodejs18x, 85 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 86 | }, 87 | }, 88 | wantErr: false, 89 | }, 90 | { 91 | name: "ListFunctionsWithRegion with no functions success", 92 | args: args{ 93 | ctx: context.Background(), 94 | region: "us-east-1", 95 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 96 | return stack.Finalize.Add( 97 | middleware.FinalizeMiddlewareFunc( 98 | "ListFunctionsWithNoFunctionsMock", 99 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 100 | return middleware.FinalizeOutput{ 101 | Result: &lambda.ListFunctionsOutput{ 102 | NextMarker: nil, 103 | Functions: []types.FunctionConfiguration{}, 104 | }, 105 | }, middleware.Metadata{}, nil 106 | }, 107 | ), 108 | middleware.Before, 109 | ) 110 | }, 111 | }, 112 | want: []types.FunctionConfiguration{}, 113 | wantErr: false, 114 | }, 115 | { 116 | name: "ListFunctionsWithRegion with NextMarker success", 117 | args: args{ 118 | ctx: context.Background(), 119 | region: "us-east-1", 120 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 121 | err := stack.Initialize.Add( 122 | middleware.InitializeMiddlewareFunc( 123 | "GetNextMarkerFromListFunctionsInput", 124 | getNextMarkerForInitialize, 125 | ), middleware.Before, 126 | ) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | err = stack.Finalize.Add( 132 | middleware.FinalizeMiddlewareFunc( 133 | "ListFunctionsWithNextMarkerMock", 134 | func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 135 | marker := middleware.GetStackValue(ctx, markerKey{}).(*string) 136 | 137 | var nextMarker *string 138 | var functions []types.FunctionConfiguration 139 | if marker == nil { 140 | nextMarker = aws.String("NextMarker") 141 | functions = []types.FunctionConfiguration{ 142 | { 143 | FunctionName: aws.String("Function1"), 144 | Runtime: types.RuntimeNodejs, 145 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 146 | }, 147 | { 148 | FunctionName: aws.String("Function2"), 149 | Runtime: types.RuntimeNodejs18x, 150 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 151 | }, 152 | } 153 | return middleware.FinalizeOutput{ 154 | Result: &lambda.ListFunctionsOutput{ 155 | NextMarker: nextMarker, 156 | Functions: functions, 157 | }, 158 | }, middleware.Metadata{}, nil 159 | } else { 160 | functions = []types.FunctionConfiguration{ 161 | { 162 | FunctionName: aws.String("Function3"), 163 | Runtime: types.RuntimeGo1x, 164 | LastModified: aws.String("2022-12-21T10:47:43.728+0000"), 165 | }, 166 | { 167 | FunctionName: aws.String("Function4"), 168 | Runtime: types.RuntimeProvidedal2, 169 | LastModified: aws.String("2022-12-22T11:47:43.728+0000"), 170 | }, 171 | } 172 | return middleware.FinalizeOutput{ 173 | Result: &lambda.ListFunctionsOutput{ 174 | NextMarker: nextMarker, 175 | Functions: functions, 176 | }, 177 | }, middleware.Metadata{}, nil 178 | } 179 | }, 180 | ), 181 | middleware.Before, 182 | ) 183 | return err 184 | }, 185 | }, 186 | want: []types.FunctionConfiguration{ 187 | { 188 | FunctionName: aws.String("Function1"), 189 | Runtime: types.RuntimeNodejs, 190 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 191 | }, 192 | { 193 | FunctionName: aws.String("Function2"), 194 | Runtime: types.RuntimeNodejs18x, 195 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 196 | }, 197 | { 198 | FunctionName: aws.String("Function3"), 199 | Runtime: types.RuntimeGo1x, 200 | LastModified: aws.String("2022-12-21T10:47:43.728+0000"), 201 | }, 202 | { 203 | FunctionName: aws.String("Function4"), 204 | Runtime: types.RuntimeProvidedal2, 205 | LastModified: aws.String("2022-12-22T11:47:43.728+0000"), 206 | }, 207 | }, 208 | wantErr: false, 209 | }, 210 | { 211 | name: "ListFunctionsWithRegion fail", 212 | args: args{ 213 | ctx: context.Background(), 214 | region: "us-east-1", 215 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 216 | return stack.Finalize.Add( 217 | middleware.FinalizeMiddlewareFunc( 218 | "ListFunctionsErrorMock", 219 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 220 | return middleware.FinalizeOutput{ 221 | Result: &lambda.ListFunctionsOutput{}, 222 | }, middleware.Metadata{}, fmt.Errorf("ListFunctionsError") 223 | }, 224 | ), 225 | middleware.Before, 226 | ) 227 | }, 228 | }, 229 | want: []types.FunctionConfiguration{}, 230 | wantErr: true, 231 | }, 232 | { 233 | name: "ListFunctionsWithRegion with NextMarker fail", 234 | args: args{ 235 | ctx: context.Background(), 236 | region: "us-east-1", 237 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 238 | err := stack.Initialize.Add( 239 | middleware.InitializeMiddlewareFunc( 240 | "GetNextMarkerFromListFunctionsInput", 241 | getNextMarkerForInitialize, 242 | ), middleware.Before, 243 | ) 244 | if err != nil { 245 | return err 246 | } 247 | 248 | err = stack.Finalize.Add( 249 | middleware.FinalizeMiddlewareFunc( 250 | "ListFunctionsWithNextMarkerErrorMock", 251 | func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 252 | marker := middleware.GetStackValue(ctx, markerKey{}).(*string) 253 | 254 | var nextMarker *string 255 | var functions []types.FunctionConfiguration 256 | if marker == nil { 257 | nextMarker = aws.String("NextMarker") 258 | functions = []types.FunctionConfiguration{ 259 | { 260 | FunctionName: aws.String("Function1"), 261 | Runtime: types.RuntimeNodejs, 262 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 263 | }, 264 | { 265 | FunctionName: aws.String("Function2"), 266 | Runtime: types.RuntimeNodejs18x, 267 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 268 | }, 269 | } 270 | return middleware.FinalizeOutput{ 271 | Result: &lambda.ListFunctionsOutput{ 272 | NextMarker: nextMarker, 273 | Functions: functions, 274 | }, 275 | }, middleware.Metadata{}, nil 276 | } else { 277 | return middleware.FinalizeOutput{ 278 | Result: &lambda.ListFunctionsOutput{}, 279 | }, middleware.Metadata{}, fmt.Errorf("ListFunctionsError") 280 | } 281 | }, 282 | ), 283 | middleware.Before, 284 | ) 285 | return err 286 | }, 287 | }, 288 | want: []types.FunctionConfiguration{ 289 | { 290 | FunctionName: aws.String("Function1"), 291 | Runtime: types.RuntimeNodejs, 292 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 293 | }, 294 | { 295 | FunctionName: aws.String("Function2"), 296 | Runtime: types.RuntimeNodejs18x, 297 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 298 | }, 299 | }, 300 | wantErr: true, 301 | }, 302 | { 303 | name: "ListFunctionsWithRegion with empty region success", 304 | args: args{ 305 | ctx: context.Background(), 306 | region: "", 307 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 308 | return stack.Finalize.Add( 309 | middleware.FinalizeMiddlewareFunc( 310 | "ListFunctionsWithEmptyRegionMock", 311 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 312 | return middleware.FinalizeOutput{ 313 | Result: &lambda.ListFunctionsOutput{ 314 | NextMarker: nil, 315 | Functions: []types.FunctionConfiguration{ 316 | { 317 | FunctionName: aws.String("Function1"), 318 | Runtime: types.RuntimeNodejs, 319 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 320 | }, 321 | { 322 | FunctionName: aws.String("Function2"), 323 | Runtime: types.RuntimeNodejs18x, 324 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 325 | }, 326 | }, 327 | }, 328 | }, middleware.Metadata{}, nil 329 | }, 330 | ), 331 | middleware.Before, 332 | ) 333 | }, 334 | }, 335 | want: []types.FunctionConfiguration{ 336 | { 337 | FunctionName: aws.String("Function1"), 338 | Runtime: types.RuntimeNodejs, 339 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 340 | }, 341 | { 342 | FunctionName: aws.String("Function2"), 343 | Runtime: types.RuntimeNodejs18x, 344 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 345 | }, 346 | }, 347 | wantErr: false, 348 | }, 349 | { 350 | name: "ListFunctionsWithRegion with no functions and empty region success", 351 | args: args{ 352 | ctx: context.Background(), 353 | region: "", 354 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 355 | return stack.Finalize.Add( 356 | middleware.FinalizeMiddlewareFunc( 357 | "ListFunctionsWithNoFunctionsAndEmptyRegionMock", 358 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 359 | return middleware.FinalizeOutput{ 360 | Result: &lambda.ListFunctionsOutput{ 361 | NextMarker: nil, 362 | Functions: []types.FunctionConfiguration{}, 363 | }, 364 | }, middleware.Metadata{}, nil 365 | }, 366 | ), 367 | middleware.Before, 368 | ) 369 | }, 370 | }, 371 | want: []types.FunctionConfiguration{}, 372 | wantErr: false, 373 | }, 374 | { 375 | name: "ListFunctionsWithRegion with NextMarker and empty region success", 376 | args: args{ 377 | ctx: context.Background(), 378 | region: "", 379 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 380 | err := stack.Initialize.Add( 381 | middleware.InitializeMiddlewareFunc( 382 | "GetNextMarkerFromListFunctionsInput", 383 | getNextMarkerForInitialize, 384 | ), middleware.Before, 385 | ) 386 | if err != nil { 387 | return err 388 | } 389 | 390 | err = stack.Finalize.Add( 391 | middleware.FinalizeMiddlewareFunc( 392 | "ListFunctionsWithNextMarkerAndEmptyRegionMock", 393 | func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 394 | marker := middleware.GetStackValue(ctx, markerKey{}).(*string) 395 | 396 | var nextMarker *string 397 | var functions []types.FunctionConfiguration 398 | if marker == nil { 399 | nextMarker = aws.String("NextMarker") 400 | functions = []types.FunctionConfiguration{ 401 | { 402 | FunctionName: aws.String("Function1"), 403 | Runtime: types.RuntimeNodejs, 404 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 405 | }, 406 | { 407 | FunctionName: aws.String("Function2"), 408 | Runtime: types.RuntimeNodejs18x, 409 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 410 | }, 411 | } 412 | return middleware.FinalizeOutput{ 413 | Result: &lambda.ListFunctionsOutput{ 414 | NextMarker: nextMarker, 415 | Functions: functions, 416 | }, 417 | }, middleware.Metadata{}, nil 418 | } else { 419 | functions = []types.FunctionConfiguration{ 420 | { 421 | FunctionName: aws.String("Function3"), 422 | Runtime: types.RuntimeGo1x, 423 | LastModified: aws.String("2022-12-21T10:47:43.728+0000"), 424 | }, 425 | { 426 | FunctionName: aws.String("Function4"), 427 | Runtime: types.RuntimeProvidedal2, 428 | LastModified: aws.String("2022-12-22T11:47:43.728+0000"), 429 | }, 430 | } 431 | return middleware.FinalizeOutput{ 432 | Result: &lambda.ListFunctionsOutput{ 433 | NextMarker: nextMarker, 434 | Functions: functions, 435 | }, 436 | }, middleware.Metadata{}, nil 437 | } 438 | }, 439 | ), 440 | middleware.Before, 441 | ) 442 | return err 443 | }, 444 | }, 445 | want: []types.FunctionConfiguration{ 446 | { 447 | FunctionName: aws.String("Function1"), 448 | Runtime: types.RuntimeNodejs, 449 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 450 | }, 451 | { 452 | FunctionName: aws.String("Function2"), 453 | Runtime: types.RuntimeNodejs18x, 454 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 455 | }, 456 | { 457 | FunctionName: aws.String("Function3"), 458 | Runtime: types.RuntimeGo1x, 459 | LastModified: aws.String("2022-12-21T10:47:43.728+0000"), 460 | }, 461 | { 462 | FunctionName: aws.String("Function4"), 463 | Runtime: types.RuntimeProvidedal2, 464 | LastModified: aws.String("2022-12-22T11:47:43.728+0000"), 465 | }, 466 | }, 467 | wantErr: false, 468 | }, 469 | { 470 | name: "ListFunctionsWithRegion with empty region fail", 471 | args: args{ 472 | ctx: context.Background(), 473 | region: "", 474 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 475 | return stack.Finalize.Add( 476 | middleware.FinalizeMiddlewareFunc( 477 | "ListFunctionsWithEmptyRegionErrorMock", 478 | func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 479 | return middleware.FinalizeOutput{ 480 | Result: &lambda.ListFunctionsOutput{ 481 | NextMarker: nil, 482 | Functions: []types.FunctionConfiguration{}, 483 | }, 484 | }, middleware.Metadata{}, fmt.Errorf("ListFunctionsError") 485 | }, 486 | ), 487 | middleware.Before, 488 | ) 489 | }, 490 | }, 491 | want: []types.FunctionConfiguration{}, 492 | wantErr: true, 493 | }, 494 | { 495 | name: "ListFunctionsWithRegion with NextMarker and empty region fail", 496 | args: args{ 497 | ctx: context.Background(), 498 | region: "", 499 | withAPIOptionsFunc: func(stack *middleware.Stack) error { 500 | err := stack.Initialize.Add( 501 | middleware.InitializeMiddlewareFunc( 502 | "GetNextMarkerFromListFunctionsInput", 503 | getNextMarkerForInitialize, 504 | ), middleware.Before, 505 | ) 506 | if err != nil { 507 | return err 508 | } 509 | 510 | err = stack.Finalize.Add( 511 | middleware.FinalizeMiddlewareFunc( 512 | "ListFunctionsWithNextMarkerAndEmptyRegionErrorMock", 513 | func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { 514 | marker := middleware.GetStackValue(ctx, markerKey{}).(*string) 515 | 516 | var nextMarker *string 517 | var functions []types.FunctionConfiguration 518 | if marker == nil { 519 | nextMarker = aws.String("NextMarker") 520 | functions = []types.FunctionConfiguration{ 521 | { 522 | FunctionName: aws.String("Function1"), 523 | Runtime: types.RuntimeNodejs, 524 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 525 | }, 526 | { 527 | FunctionName: aws.String("Function2"), 528 | Runtime: types.RuntimeNodejs18x, 529 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 530 | }, 531 | } 532 | return middleware.FinalizeOutput{ 533 | Result: &lambda.ListFunctionsOutput{ 534 | NextMarker: nextMarker, 535 | Functions: functions, 536 | }, 537 | }, middleware.Metadata{}, nil 538 | } else { 539 | return middleware.FinalizeOutput{ 540 | Result: &lambda.ListFunctionsOutput{}, 541 | }, middleware.Metadata{}, fmt.Errorf("ListFunctionsError") 542 | } 543 | }, 544 | ), 545 | middleware.Before, 546 | ) 547 | return err 548 | }, 549 | }, 550 | want: []types.FunctionConfiguration{ 551 | { 552 | FunctionName: aws.String("Function1"), 553 | Runtime: types.RuntimeNodejs, 554 | LastModified: aws.String("2022-12-21T09:47:43.728+0000"), 555 | }, 556 | { 557 | FunctionName: aws.String("Function2"), 558 | Runtime: types.RuntimeNodejs18x, 559 | LastModified: aws.String("2022-12-22T09:47:43.728+0000"), 560 | }, 561 | }, 562 | wantErr: true, 563 | }, 564 | } 565 | for _, tt := range tests { 566 | t.Run(tt.name, func(t *testing.T) { 567 | cfg, err := config.LoadDefaultConfig( 568 | tt.args.ctx, 569 | config.WithRegion("ap-northeast-1"), 570 | config.WithAPIOptions([]func(*middleware.Stack) error{tt.args.withAPIOptionsFunc}), 571 | ) 572 | if err != nil { 573 | t.Fatal(err) 574 | } 575 | 576 | client := lambda.NewFromConfig(cfg) 577 | lambdaClient := NewLambda(client) 578 | 579 | got, err := lambdaClient.ListFunctionsWithRegion(tt.args.ctx, tt.args.region) 580 | if (err != nil) != tt.wantErr { 581 | t.Errorf("Lambda.ListFunctionsWithRegion() error = %v, wantErr %v", err, tt.wantErr) 582 | return 583 | } 584 | if !reflect.DeepEqual(got, tt.want) { 585 | t.Errorf("Lambda.ListFunctionsWithRegion() = %v, want %v", got, tt.want) 586 | } 587 | }) 588 | } 589 | } 590 | 591 | func TestLambda_ListRuntimeValues(t *testing.T) { 592 | tests := []struct { 593 | name string 594 | want []string 595 | }{ 596 | { 597 | name: "ListRuntimeValues sorted success", 598 | want: []string{ 599 | "dotnet6", 600 | "dotnet8", 601 | "dotnetcore1.0", 602 | "dotnetcore2.0", 603 | "dotnetcore2.1", 604 | "dotnetcore3.1", 605 | "go1.x", 606 | "java8", 607 | "java8.al2", 608 | "java11", 609 | "java17", 610 | "java21", 611 | "nodejs", 612 | "nodejs4.3", 613 | "nodejs4.3-edge", 614 | "nodejs6.10", 615 | "nodejs8.10", 616 | "nodejs10.x", 617 | "nodejs12.x", 618 | "nodejs14.x", 619 | "nodejs16.x", 620 | "nodejs18.x", 621 | "nodejs20.x", 622 | "nodejs22.x", 623 | "provided", 624 | "provided.al2", 625 | "provided.al2023", 626 | "python2.7", 627 | "python3.6", 628 | "python3.7", 629 | "python3.8", 630 | "python3.9", 631 | "python3.10", 632 | "python3.11", 633 | "python3.12", 634 | "python3.13", 635 | "ruby2.5", 636 | "ruby2.7", 637 | "ruby3.2", 638 | "ruby3.3", 639 | "ruby3.4", 640 | }, 641 | }, 642 | } 643 | for _, tt := range tests { 644 | t.Run(tt.name, func(t *testing.T) { 645 | cfg, err := config.LoadDefaultConfig( 646 | context.Background(), 647 | config.WithRegion("ap-northeast-1"), 648 | ) 649 | if err != nil { 650 | t.Fatal(err) 651 | } 652 | 653 | client := lambda.NewFromConfig(cfg) 654 | lambdaClient := NewLambda(client) 655 | 656 | if got := lambdaClient.ListRuntimeValues(); !reflect.DeepEqual(got, tt.want) { 657 | t.Errorf("Lambda.ListRuntimeValues() = %v, want %v", got, tt.want) 658 | } 659 | }) 660 | } 661 | } 662 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | # Lambda Function Test Scripts 2 | 3 | This directory contains test scripts for creating and deleting AWS Lambda functions. 4 | These scripts are used for testing the `lamver` tool. 5 | 6 | ## Features 7 | 8 | - Creates Lambda functions in multiple regions (us-east-1, ap-northeast-1) 9 | - Supports multiple runtimes (Go, Node.js, Python) 10 | - Automatically creates and deletes IAM roles and policies 11 | - Allows specifying AWS profiles 12 | 13 | ## Usage 14 | 15 | ### Prerequisites 16 | 17 | - AWS credentials configured (with appropriate IAM permissions) 18 | - Go runtime environment 19 | 20 | ### Via Makefile 21 | 22 | From the project root directory, you can use these commands: 23 | 24 | ```bash 25 | # Default profile 26 | make testgen 27 | 28 | # Generate test Lambda functions with a specific profile 29 | make testgen OPT="-p myprofile" 30 | 31 | # Delete test Lambda functions 32 | make testgen_clean 33 | 34 | # Delete test Lambda functions with a specific profile 35 | make testgen_clean OPT="-p myprofile" 36 | 37 | # View help for test data generation 38 | make testgen_help 39 | ``` 40 | 41 | ### Direct Command Execution 42 | 43 | You can also run the commands directly: 44 | 45 | ```bash 46 | # Creating Lambda functions 47 | cd testdata 48 | go run cmd/create/main.go -p 49 | 50 | # Deleting Lambda functions 51 | cd testdata 52 | go run cmd/delete/main.go -p 53 | ``` 54 | 55 | ## Resource Naming 56 | 57 | The test scripts create resources with the following naming patterns: 58 | 59 | - IAM Role: `lamver-test-role` 60 | - IAM Policy: `lamver-test-policy` 61 | - Lambda Functions: `lamver-test-function-{region}-{runtime}` 62 | - Example: `lamver-test-function-us-east-1-nodejs` 63 | -------------------------------------------------------------------------------- /testdata/cmd/create/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "github.com/go-to-k/lamver/testdata/pkg/aws" 11 | "github.com/go-to-k/lamver/testdata/pkg/iam" 12 | "github.com/go-to-k/lamver/testdata/pkg/lambda" 13 | ) 14 | 15 | func main() { 16 | // Parse command line arguments 17 | profile := flag.String("p", "", "AWS profile name") 18 | flag.Parse() 19 | 20 | // Store created function info 21 | var createdFunctions []string 22 | 23 | // Create IAM role in us-east-1 region first 24 | usEast1Cfg, err := aws.LoadConfig(context.TODO(), *profile, "us-east-1") 25 | if err != nil { 26 | log.Fatalf("Failed to load AWS configuration for us-east-1: %v", err) 27 | } 28 | 29 | roleARN, err := iam.CreateRole(context.TODO(), usEast1Cfg) 30 | if err != nil { 31 | log.Fatalf("Failed to create IAM role: %v", err) 32 | } 33 | 34 | fmt.Printf("Created/Reused IAM Role: %s\n", roleARN) 35 | 36 | // Wait for role propagation 37 | fmt.Println("Waiting for IAM role propagation (10 seconds)...") 38 | time.Sleep(10 * time.Second) 39 | 40 | // Create Lambda functions for each region and runtime combination 41 | for _, region := range aws.Regions { 42 | // Load AWS configuration for this region 43 | cfg, err := aws.LoadConfig(context.TODO(), *profile, region) 44 | if err != nil { 45 | log.Fatalf("Failed to load AWS configuration for %s: %v", region, err) 46 | } 47 | 48 | for runtimeName, runtimeInfo := range lambda.Runtimes { 49 | // Generate Lambda function name (including region and runtime) 50 | funcName := fmt.Sprintf("%s-%s-%s", lambda.FunctionName, region, runtimeName) 51 | 52 | // Create Lambda function 53 | isNew, err := lambda.CreateFunction(context.TODO(), cfg, funcName, roleARN, runtimeInfo) 54 | if err != nil { 55 | log.Fatalf("Failed to create Lambda function (%s, %s): %v", region, runtimeName, err) 56 | } 57 | 58 | // Different message based on whether the function already existed or was newly created 59 | if isNew { 60 | fmt.Printf("Lambda function '%s' successfully created in %s region (runtime: %s)\n", 61 | funcName, region, runtimeInfo.Runtime) 62 | } else { 63 | fmt.Printf("Lambda function '%s' already exists in %s region (runtime: %s) - reusing\n", 64 | funcName, region, runtimeInfo.Runtime) 65 | } 66 | 67 | // Store function info for display 68 | createdFunctions = append(createdFunctions, fmt.Sprintf("%s (Region: %s, Runtime: %s)", 69 | funcName, region, runtimeInfo.Runtime)) 70 | } 71 | } 72 | 73 | // Display created functions summary 74 | fmt.Println("\nCreated Resources Summary:") 75 | fmt.Println("===========================") 76 | 77 | fmt.Println("\nIAM Resources:") 78 | fmt.Printf("- Role ARN: %s\n", roleARN) 79 | fmt.Printf("- Role Name: %s\n", iam.RoleName) 80 | fmt.Printf("- Policy Name: %s\n", iam.PolicyName) 81 | 82 | fmt.Println("\nLambda Functions:") 83 | for _, fn := range createdFunctions { 84 | fmt.Println("- " + fn) 85 | } 86 | fmt.Printf("\nTotal functions created: %d\n", len(createdFunctions)) 87 | } 88 | -------------------------------------------------------------------------------- /testdata/cmd/delete/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/go-to-k/lamver/testdata/pkg/aws" 10 | "github.com/go-to-k/lamver/testdata/pkg/iam" 11 | "github.com/go-to-k/lamver/testdata/pkg/lambda" 12 | ) 13 | 14 | func main() { 15 | // Parse command line arguments 16 | profile := flag.String("p", "", "AWS profile name") 17 | flag.Parse() 18 | 19 | // Store deleted function info 20 | var deletedFunctions []string 21 | var failedFunctions []string 22 | 23 | // Delete Lambda functions for each region and runtime combination 24 | for _, region := range aws.Regions { 25 | // Load AWS configuration 26 | cfg, err := aws.LoadConfig(context.TODO(), *profile, region) 27 | if err != nil { 28 | log.Fatalf("Failed to load AWS configuration: %v", err) 29 | } 30 | 31 | // Delete Lambda functions for each runtime 32 | for runtimeName, runtimeInfo := range lambda.Runtimes { 33 | // Generate Lambda function name 34 | funcName := fmt.Sprintf("%s-%s-%s", lambda.FunctionName, region, runtimeName) 35 | 36 | // Delete Lambda function 37 | err = lambda.DeleteFunction(context.TODO(), cfg, funcName) 38 | if err != nil { 39 | log.Printf("Warning: Failed to delete Lambda function '%s': %v", funcName, err) 40 | failedFunctions = append(failedFunctions, fmt.Sprintf("%s (Region: %s, Runtime: %s) - Error: %v", 41 | funcName, region, runtimeInfo.Runtime, err)) 42 | continue 43 | } 44 | 45 | fmt.Printf("Lambda function '%s' successfully deleted from %s region\n", 46 | funcName, region) 47 | 48 | // Store function info for display 49 | deletedFunctions = append(deletedFunctions, fmt.Sprintf("%s (Region: %s, Runtime: %s)", 50 | funcName, region, runtimeInfo.Runtime)) 51 | } 52 | } 53 | 54 | // Delete IAM roles and policies (only in us-east-1) 55 | if err := iam.DeleteResources(context.TODO(), *profile); err != nil { 56 | log.Fatalf("Failed to delete IAM resources: %v", err) 57 | } 58 | 59 | // Display deleted resources summary 60 | fmt.Println("\nDeleted Resources Summary:") 61 | fmt.Println("===========================") 62 | 63 | fmt.Println("\nIAM Resources:") 64 | fmt.Printf("- Role Name: %s\n", iam.RoleName) 65 | fmt.Printf("- Policy Name: %s\n", iam.PolicyName) 66 | 67 | fmt.Println("\nLambda Functions:") 68 | for _, fn := range deletedFunctions { 69 | fmt.Println("- " + fn) 70 | } 71 | 72 | if len(failedFunctions) > 0 { 73 | fmt.Println("\nFailed to Delete:") 74 | for _, fn := range failedFunctions { 75 | fmt.Println("- " + fn) 76 | } 77 | } 78 | 79 | fmt.Printf("\nTotal functions deleted: %d\n", len(deletedFunctions)) 80 | if len(failedFunctions) > 0 { 81 | fmt.Printf("Total failures: %d\n", len(failedFunctions)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /testdata/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-to-k/lamver/testdata 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.28.3 10 | github.com/aws/aws-sdk-go-v2/service/iam v1.28.5 11 | github.com/aws/aws-sdk-go-v2/service/lambda v1.71.0 12 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 13 | ) 14 | 15 | require ( 16 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect 17 | github.com/aws/aws-sdk-go-v2/credentials v1.17.44 // indirect 18 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 20 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 21 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect 26 | github.com/aws/smithy-go v1.22.2 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /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.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= 6 | github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= 7 | github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= 9 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= 10 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= 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.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= 16 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= 17 | github.com/aws/aws-sdk-go-v2/service/iam v1.28.5 h1:Ts2eDDuMLrrmd0ARlg5zSoBQUvhdthgiNnPdiykTJs0= 18 | github.com/aws/aws-sdk-go-v2/service/iam v1.28.5/go.mod h1:kKI0gdVsf+Ev9knh/3lBJbchtX5LLNH25lAzx3KDj3Q= 19 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= 20 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= 21 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= 22 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= 23 | github.com/aws/aws-sdk-go-v2/service/lambda v1.71.0 h1:8PjrcaqDZKar6ivI8c6vwNADOURebrRZQms3SxggRgU= 24 | github.com/aws/aws-sdk-go-v2/service/lambda v1.71.0/go.mod h1:c27kk10S36lBYgbG1jR3opn4OAS5Y/4wjJa1GiHK/X4= 25 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= 27 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= 28 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= 29 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= 31 | github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 32 | github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 33 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 34 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | -------------------------------------------------------------------------------- /testdata/pkg/aws/config.go: -------------------------------------------------------------------------------- 1 | package aws 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 | // List of regions 11 | var Regions = []string{"us-east-1", "ap-northeast-1"} 12 | 13 | // Load AWS configuration 14 | func LoadConfig(ctx context.Context, profile, region string) (aws.Config, error) { 15 | var opts []func(*config.LoadOptions) error 16 | 17 | if profile != "" { 18 | opts = append(opts, config.WithSharedConfigProfile(profile)) 19 | } 20 | 21 | if region != "" { 22 | opts = append(opts, config.WithRegion(region)) 23 | } 24 | 25 | return config.LoadDefaultConfig(ctx, opts...) 26 | } 27 | -------------------------------------------------------------------------------- /testdata/pkg/iam/role.go: -------------------------------------------------------------------------------- 1 | package iam 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/iam" 10 | "github.com/aws/aws-sdk-go-v2/service/sts" 11 | awsConfig "github.com/go-to-k/lamver/testdata/pkg/aws" 12 | ) 13 | 14 | const ( 15 | RoleName = "lamver-test-role" 16 | PolicyName = "lamver-test-policy" 17 | ManagedPolicyARN = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 18 | ) 19 | 20 | // IAM role policy document 21 | const assumeRolePolicyDocument = `{ 22 | "Version": "2012-10-17", 23 | "Statement": [ 24 | { 25 | "Effect": "Allow", 26 | "Principal": { 27 | "Service": "lambda.amazonaws.com" 28 | }, 29 | "Action": "sts:AssumeRole" 30 | } 31 | ] 32 | }` 33 | 34 | // Lambda execution policy document 35 | const lambdaExecutionPolicyDocument = `{ 36 | "Version": "2012-10-17", 37 | "Statement": [ 38 | { 39 | "Effect": "Allow", 40 | "Action": [ 41 | "logs:CreateLogGroup", 42 | "logs:CreateLogStream", 43 | "logs:PutLogEvents" 44 | ], 45 | "Resource": "arn:aws:logs:*:*:*" 46 | } 47 | ] 48 | }` 49 | 50 | // Create IAM role 51 | func CreateRole(ctx context.Context, cfg aws.Config) (string, error) { 52 | client := iam.NewFromConfig(cfg) 53 | var roleArn string 54 | 55 | // Check if role already exists 56 | existingRole, err := client.GetRole(ctx, &iam.GetRoleInput{ 57 | RoleName: aws.String(RoleName), 58 | }) 59 | 60 | // Role exists 61 | if err == nil && existingRole.Role != nil && existingRole.Role.Arn != nil { 62 | log.Printf("Role %s already exists, reusing it", RoleName) 63 | roleArn = *existingRole.Role.Arn 64 | } else { 65 | // Create role if it doesn't exist 66 | roleOutput, createErr := client.CreateRole(ctx, &iam.CreateRoleInput{ 67 | RoleName: aws.String(RoleName), 68 | AssumeRolePolicyDocument: aws.String(assumeRolePolicyDocument), 69 | Description: aws.String("Role for lamver testing"), 70 | }) 71 | if createErr != nil { 72 | return "", fmt.Errorf("failed to create role: %w", createErr) 73 | } 74 | roleArn = *roleOutput.Role.Arn 75 | } 76 | 77 | // Try to find existing policy 78 | accountID := getAccountID(ctx, cfg) 79 | customPolicyArn := fmt.Sprintf("arn:aws:iam::%s:policy/%s", accountID, PolicyName) 80 | 81 | // Try to get the policy 82 | _, policyErr := client.GetPolicy(ctx, &iam.GetPolicyInput{ 83 | PolicyArn: aws.String(customPolicyArn), 84 | }) 85 | 86 | // Create policy if it doesn't exist 87 | if policyErr != nil { 88 | policyOutput, createErr := client.CreatePolicy(ctx, &iam.CreatePolicyInput{ 89 | PolicyName: aws.String(PolicyName), 90 | PolicyDocument: aws.String(lambdaExecutionPolicyDocument), 91 | Description: aws.String("Policy for lamver testing"), 92 | }) 93 | if createErr != nil { 94 | return "", fmt.Errorf("failed to create policy: %w", createErr) 95 | } 96 | customPolicyArn = *policyOutput.Policy.Arn 97 | } else { 98 | log.Printf("Policy %s already exists, reusing it", PolicyName) 99 | } 100 | 101 | // Check if policies are already attached 102 | attachedPolicies, err := client.ListAttachedRolePolicies(ctx, &iam.ListAttachedRolePoliciesInput{ 103 | RoleName: aws.String(RoleName), 104 | }) 105 | 106 | var customPolicyAttached, awsManagedPolicyAttached bool 107 | if err == nil { 108 | for _, policy := range attachedPolicies.AttachedPolicies { 109 | if *policy.PolicyArn == customPolicyArn { 110 | customPolicyAttached = true 111 | } else if *policy.PolicyArn == ManagedPolicyARN { 112 | awsManagedPolicyAttached = true 113 | } 114 | } 115 | } 116 | 117 | // Attach custom policy if not already attached 118 | if !customPolicyAttached { 119 | _, err = client.AttachRolePolicy(ctx, &iam.AttachRolePolicyInput{ 120 | RoleName: aws.String(RoleName), 121 | PolicyArn: aws.String(customPolicyArn), 122 | }) 123 | if err != nil { 124 | return "", fmt.Errorf("failed to attach policy: %w", err) 125 | } 126 | } 127 | 128 | // Attach AWS managed policy if not already attached 129 | if !awsManagedPolicyAttached { 130 | _, err = client.AttachRolePolicy(ctx, &iam.AttachRolePolicyInput{ 131 | RoleName: aws.String(RoleName), 132 | PolicyArn: aws.String(ManagedPolicyARN), 133 | }) 134 | if err != nil { 135 | return "", fmt.Errorf("failed to attach AWS managed policy: %w", err) 136 | } 137 | } 138 | 139 | return roleArn, nil 140 | } 141 | 142 | // Get IAM role ARN 143 | func GetRoleARN(ctx context.Context, cfg aws.Config) (string, error) { 144 | client := iam.NewFromConfig(cfg) 145 | 146 | output, err := client.GetRole(ctx, &iam.GetRoleInput{ 147 | RoleName: aws.String(RoleName), 148 | }) 149 | if err != nil { 150 | return "", err 151 | } 152 | 153 | return *output.Role.Arn, nil 154 | } 155 | 156 | // Delete IAM roles and policies 157 | func DeleteResources(ctx context.Context, profile string) error { 158 | // Delete IAM resources in us-east-1 region 159 | cfg, err := awsConfig.LoadConfig(ctx, profile, "us-east-1") 160 | if err != nil { 161 | return fmt.Errorf("failed to load AWS configuration: %w", err) 162 | } 163 | 164 | client := iam.NewFromConfig(cfg) 165 | accountID := getAccountID(ctx, cfg) 166 | 167 | // Detach custom policy 168 | _, err = client.DetachRolePolicy(ctx, &iam.DetachRolePolicyInput{ 169 | RoleName: aws.String(RoleName), 170 | PolicyArn: aws.String(fmt.Sprintf("arn:aws:iam::%s:policy/%s", accountID, PolicyName)), 171 | }) 172 | if err != nil { 173 | log.Printf("Warning: Failed to detach custom policy: %v", err) 174 | } else { 175 | fmt.Printf("Successfully detached policy '%s' from role '%s'\n", PolicyName, RoleName) 176 | } 177 | 178 | // Detach AWS managed policy 179 | _, err = client.DetachRolePolicy(ctx, &iam.DetachRolePolicyInput{ 180 | RoleName: aws.String(RoleName), 181 | PolicyArn: aws.String(ManagedPolicyARN), 182 | }) 183 | if err != nil { 184 | log.Printf("Warning: Failed to detach AWS managed policy: %v", err) 185 | } else { 186 | fmt.Printf("Successfully detached AWS managed policy from role '%s'\n", RoleName) 187 | } 188 | 189 | // Delete role 190 | _, err = client.DeleteRole(ctx, &iam.DeleteRoleInput{ 191 | RoleName: aws.String(RoleName), 192 | }) 193 | if err != nil { 194 | log.Printf("Warning: Failed to delete role: %v", err) 195 | } else { 196 | fmt.Printf("Successfully deleted IAM role '%s'\n", RoleName) 197 | } 198 | 199 | // Delete policy 200 | _, err = client.DeletePolicy(ctx, &iam.DeletePolicyInput{ 201 | PolicyArn: aws.String(fmt.Sprintf("arn:aws:iam::%s:policy/%s", accountID, PolicyName)), 202 | }) 203 | if err != nil { 204 | log.Printf("Warning: Failed to delete policy: %v", err) 205 | } else { 206 | fmt.Printf("Successfully deleted IAM policy '%s'\n", PolicyName) 207 | } 208 | 209 | return nil 210 | } 211 | 212 | // Get AWS account ID 213 | func getAccountID(ctx context.Context, cfg aws.Config) string { 214 | stsClient := sts.NewFromConfig(cfg) 215 | identity, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) 216 | if err == nil && identity.Account != nil { 217 | return *identity.Account 218 | } 219 | 220 | log.Printf("Warning: Failed to get account ID: %v", err) 221 | return "*" 222 | } 223 | -------------------------------------------------------------------------------- /testdata/pkg/lambda/function.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/lambda" 9 | "github.com/aws/aws-sdk-go-v2/service/lambda/types" 10 | ) 11 | 12 | const ( 13 | FunctionName = "lamver-test-function" 14 | ) 15 | 16 | // Helper function to return an int32 pointer 17 | func Int32(v int32) *int32 { 18 | return &v 19 | } 20 | 21 | // Create Lambda function 22 | func CreateFunction(ctx context.Context, cfg aws.Config, funcName, roleARN string, runtimeInfo RuntimeInfo) (bool, error) { 23 | // Create Lambda client 24 | client := lambda.NewFromConfig(cfg) 25 | 26 | // Check if function already exists 27 | _, err := client.GetFunction(ctx, &lambda.GetFunctionInput{ 28 | FunctionName: aws.String(funcName), 29 | }) 30 | 31 | // If function exists, return without error 32 | if err == nil { 33 | // Return false to indicate the function already existed 34 | return false, nil 35 | } 36 | 37 | // Compress Lambda function source code into a ZIP file 38 | zipBytes, err := createZip(runtimeInfo) 39 | if err != nil { 40 | return false, fmt.Errorf("failed to create ZIP file: %w", err) 41 | } 42 | 43 | // Create Lambda function 44 | _, err = client.CreateFunction(ctx, &lambda.CreateFunctionInput{ 45 | Code: &types.FunctionCode{ 46 | ZipFile: zipBytes, 47 | }, 48 | FunctionName: aws.String(funcName), 49 | Handler: aws.String(runtimeInfo.Handler), 50 | Role: aws.String(roleARN), 51 | Runtime: runtimeInfo.Runtime, 52 | Timeout: Int32(30), 53 | MemorySize: Int32(128), 54 | }) 55 | 56 | if err != nil { 57 | return false, err 58 | } 59 | 60 | // Return true to indicate the function was newly created 61 | return true, nil 62 | } 63 | 64 | // Delete Lambda function 65 | func DeleteFunction(ctx context.Context, cfg aws.Config, funcName string) error { 66 | // Create Lambda client 67 | client := lambda.NewFromConfig(cfg) 68 | 69 | // Delete Lambda function 70 | _, err := client.DeleteFunction(ctx, &lambda.DeleteFunctionInput{ 71 | FunctionName: aws.String(funcName), 72 | }) 73 | 74 | return err 75 | } 76 | -------------------------------------------------------------------------------- /testdata/pkg/lambda/source.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "io" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/lambda/types" 9 | ) 10 | 11 | // Runtime information 12 | type RuntimeInfo struct { 13 | Runtime types.Runtime 14 | Source string 15 | Handler string 16 | FileExt string 17 | FileName string 18 | } 19 | 20 | // Lambda function source code (Go) 21 | const lambdaSourceGo = ` 22 | package main 23 | 24 | import ( 25 | "context" 26 | "fmt" 27 | 28 | "github.com/aws/aws-lambda-go/lambda" 29 | ) 30 | 31 | type Event struct { 32 | Name string ` + "`json:\"name\"`" + ` 33 | } 34 | 35 | type Response struct { 36 | Message string ` + "`json:\"message\"`" + ` 37 | } 38 | 39 | func HandleRequest(ctx context.Context, event Event) (Response, error) { 40 | return Response{ 41 | Message: fmt.Sprintf("Hello, %s!", event.Name), 42 | }, nil 43 | } 44 | 45 | func main() { 46 | lambda.Start(HandleRequest) 47 | } 48 | ` 49 | 50 | // Lambda function source code (Node.js) 51 | const lambdaSourceNode = ` 52 | exports.handler = async (event) => { 53 | const name = event.name || 'World'; 54 | return { 55 | message: "Hello, " + name + "!" 56 | }; 57 | }; 58 | ` 59 | 60 | // Lambda function source code (Python) 61 | const lambdaSourcePython = ` 62 | def lambda_handler(event, context): 63 | name = event.get('name', 'World') 64 | return { 65 | 'message': f'Hello, {name}!' 66 | } 67 | ` 68 | 69 | // List of runtimes 70 | var Runtimes = map[string]RuntimeInfo{ 71 | "go1": { 72 | Runtime: types.RuntimeProvidedal2023, 73 | Source: lambdaSourceGo, 74 | Handler: "main", 75 | FileExt: ".go", 76 | FileName: "main.go", 77 | }, 78 | "go2": { 79 | Runtime: types.RuntimeProvidedal2, 80 | Source: lambdaSourceGo, 81 | Handler: "main", 82 | FileExt: ".go", 83 | FileName: "main.go", 84 | }, 85 | "go3": { 86 | Runtime: types.RuntimeProvidedal2023, 87 | Source: lambdaSourceGo, 88 | Handler: "main", 89 | FileExt: ".go", 90 | FileName: "main.go", 91 | }, 92 | "nodejs1": { 93 | Runtime: types.RuntimeNodejs22x, 94 | Source: lambdaSourceNode, 95 | Handler: "index.handler", 96 | FileExt: ".js", 97 | FileName: "index.js", 98 | }, 99 | "nodejs2": { 100 | Runtime: types.RuntimeNodejs20x, 101 | Source: lambdaSourceNode, 102 | Handler: "index.handler", 103 | FileExt: ".js", 104 | FileName: "index.js", 105 | }, 106 | "nodejs3": { 107 | Runtime: types.RuntimeNodejs22x, 108 | Source: lambdaSourceNode, 109 | Handler: "index.handler", 110 | FileExt: ".js", 111 | FileName: "index.js", 112 | }, 113 | "python1": { 114 | Runtime: types.RuntimePython313, 115 | Source: lambdaSourcePython, 116 | Handler: "lambda_function.lambda_handler", 117 | FileExt: ".py", 118 | FileName: "lambda_function.py", 119 | }, 120 | "python2": { 121 | Runtime: types.RuntimePython312, 122 | Source: lambdaSourcePython, 123 | Handler: "lambda_function.lambda_handler", 124 | FileExt: ".py", 125 | FileName: "lambda_function.py", 126 | }, 127 | "python3": { 128 | Runtime: types.RuntimePython313, 129 | Source: lambdaSourcePython, 130 | Handler: "lambda_function.lambda_handler", 131 | FileExt: ".py", 132 | FileName: "lambda_function.py", 133 | }, 134 | } 135 | 136 | // Compress Lambda function source code into a ZIP file 137 | func createZip(runtimeInfo RuntimeInfo) ([]byte, error) { 138 | // Create ZIP file in memory 139 | buf := new(bytes.Buffer) 140 | zipWriter := zip.NewWriter(buf) 141 | 142 | // Add source file to ZIP 143 | fileWriter, err := zipWriter.Create(runtimeInfo.FileName) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | _, err = io.WriteString(fileWriter, runtimeInfo.Source) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | // Close ZIP file 154 | err = zipWriter.Close() 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | return buf.Bytes(), nil 160 | } 161 | --------------------------------------------------------------------------------