├── .dockerignore ├── .github └── workflows │ ├── docker.yaml │ ├── lint.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .go-version ├── .golangci.yml ├── .goreleaser.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── autoscaling.go ├── completion.go ├── ec2.go ├── ec2ri.go ├── ecr.go ├── ecs.go ├── elb.go ├── elbv2.go ├── iam.go ├── rds.go ├── root.go ├── ssm.go ├── sts.go └── version.go ├── go.mod ├── go.sum ├── main.go └── myaws ├── autoscaling.go ├── autoscaling_attach.go ├── autoscaling_detach.go ├── autoscaling_ls.go ├── autoscaling_set_instance_protection.go ├── autoscaling_update.go ├── autoscaling_waiter.go ├── client.go ├── config.go ├── ec2.go ├── ec2_ls.go ├── ec2_ssh.go ├── ec2_start.go ├── ec2_stop.go ├── ec2ri.go ├── ec2ri_ls.go ├── ecr_get_login.go ├── ecs.go ├── ecs_node_drain.go ├── ecs_node_ls.go ├── ecs_node_renew.go ├── ecs_node_update.go ├── ecs_service_ls.go ├── ecs_service_update.go ├── ecs_status.go ├── ecs_waiter.go ├── elb_ls.go ├── elb_ps.go ├── elbv2_ls.go ├── elbv2_ps.go ├── iam_user_ls.go ├── iam_user_reset_password.go ├── rds_ls.go ├── ssm_parameter.go ├── ssm_parameter_del.go ├── ssm_parameter_env.go ├── ssm_parameter_get.go ├── ssm_parameter_ls.go ├── ssm_parameter_put.go ├── sts_id.go └── time.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | .git 3 | bin/* 4 | dist/* 5 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: docker 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - "docker-build-*" 10 | tags: 11 | - "v[0-9]+.*" 12 | 13 | jobs: 14 | docker: 15 | timeout-minutes: 10 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 22 | - name: Docker meta 23 | id: meta 24 | uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0 25 | with: 26 | images: ${{ github.repository }} 27 | tags: | 28 | type=ref,event=branch 29 | type=ref,event=pr 30 | type=semver,pattern={{version}} 31 | type=semver,pattern={{major}}.{{minor}} 32 | - name: Login to DockerHub 33 | uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 34 | with: 35 | username: ${{ secrets.DOCKERHUB_USERNAME }} 36 | password: ${{ secrets.DOCKERHUB_TOKEN }} 37 | - name: Build and push 38 | id: docker_build 39 | uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1 40 | with: 41 | push: ${{ github.event_name != 'pull_request' }} 42 | tags: ${{ steps.meta.outputs.tags }} 43 | labels: ${{ steps.meta.outputs.labels }} 44 | platforms: linux/amd64,linux/arm64 45 | - name: Image digest 46 | run: echo ${{ steps.docker_build.outputs.digest }} 47 | - name: docker run 48 | if: ${{ github.event_name != 'pull_request' }} 49 | run: docker run --rm ${{ github.repository }}:${{ steps.meta.outputs.version }} myaws version 50 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 20 | with: 21 | go-version-file: '.go-version' 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2 24 | with: 25 | version: v1.59.1 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | permissions: 3 | contents: write 4 | 5 | on: 6 | push: 7 | tags: 8 | - "v[0-9]+.*" 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 21 | with: 22 | go-version-file: '.go-version' 23 | - name: Generate github app token 24 | uses: actions/create-github-app-token@31c86eb3b33c9b601a1f60f98dcbfd1d70f379b4 # v1.10.3 25 | id: app-token 26 | with: 27 | app-id: ${{ secrets.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | owner: ${{ github.repository_owner }} 30 | repositories: homebrew-myaws 31 | - name: Run GoReleaser 32 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 33 | with: 34 | version: "~> v2" 35 | args: release --clean 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | test: 15 | runs-on: ${{ matrix.os }} 16 | timeout-minutes: 5 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macOS-latest] 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 23 | with: 24 | go-version-file: '.go-version' 25 | - name: test 26 | run: make test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | bin/* 3 | dist/* 4 | 5 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.22 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # https://golangci-lint.run/usage/configuration/ 2 | linters: 3 | disable-all: true 4 | enable: 5 | - errcheck 6 | - goimports 7 | - gosec 8 | - gosimple 9 | - govet 10 | - ineffassign 11 | - revive 12 | - staticcheck 13 | 14 | issues: 15 | exclude-rules: 16 | - linters: 17 | - revive 18 | text: "error-strings:" 19 | - linters: 20 | - revive 21 | text: "var-naming:" 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - binary: myaws 4 | goos: 5 | - darwin 6 | - linux 7 | goarch: 8 | - amd64 9 | - arm64 10 | env: 11 | - CGO_ENABLED=0 12 | archives: 13 | - name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}" 14 | files: 15 | - none* 16 | release: 17 | prerelease: auto 18 | changelog: 19 | filters: 20 | exclude: 21 | - Merge pull request 22 | - Merge branch 23 | - Update README 24 | - Update CHANGELOG 25 | brews: 26 | - repository: 27 | owner: minamijoyo 28 | name: homebrew-myaws 29 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 30 | commit_author: 31 | name: "Masayuki Morita" 32 | email: minamijoyo@gmail.com 33 | homepage: https://github.com/minamijoyo/myaws 34 | description: "A human friendly AWS CLI written in Go" 35 | skip_upload: auto 36 | test: | 37 | system "#{bin}/myaws version" 38 | install: | 39 | bin.install "myaws" 40 | output = Utils.popen_read("#{bin}/myaws completion bash") 41 | (bash_completion/"myaws").write output 42 | output = Utils.popen_read("#{bin}/myaws completion zsh") 43 | (zsh_completion/"_myaws").write output 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## master (Unreleased) 2 | 3 | ## 0.4.9 (2024/08/18) 4 | 5 | ENHANCEMENTS: 6 | 7 | * Update actions/checkout to v4 ([#70](https://github.com/minamijoyo/myaws/pull/70)) 8 | * Update setup-go to v5 ([#71](https://github.com/minamijoyo/myaws/pull/71)) 9 | * Update golangci lint to v1.59.1 ([#72](https://github.com/minamijoyo/myaws/pull/72)) 10 | * Update Go to v1.22 ([#73](https://github.com/minamijoyo/myaws/pull/73)) 11 | * Update docker/build-push-action to v4 ([#74](https://github.com/minamijoyo/myaws/pull/74)) 12 | * Update goreleaser to v2 ([#75](https://github.com/minamijoyo/myaws/pull/75)) 13 | * Switch to the official action for creating GitHub App token ([#76](https://github.com/minamijoyo/myaws/pull/76)) 14 | 15 | ## 0.4.8 (2022/08/24) 16 | 17 | ENHANCEMENTS: 18 | 19 | * Update Go to 1.19 ([#67](https://github.com/minamijoyo/myaws/pull/67)) 20 | 21 | ## 0.4.7 (2022/07/28) 22 | 23 | ENHANCEMENTS: 24 | 25 | * Add support for linux/arm64 Docker image ([#66](https://github.com/minamijoyo/myaws/pull/66)) 26 | 27 | ## 0.4.6 (2022/07/21) 28 | 29 | ENHANCEMENTS: 30 | 31 | * Restrict repository of token for release ([#65](https://github.com/minamijoyo/myaws/pull/65)) 32 | 33 | ## 0.4.5 (2022/07/14) 34 | 35 | ENHANCEMENTS: 36 | 37 | * Use GitHub App token for updating brew formula on release ([#64](https://github.com/minamijoyo/myaws/pull/64)) 38 | 39 | ## 0.4.4 (2022/05/10) 40 | 41 | ENHANCEMENTS: 42 | 43 | * bump golang.org/x/crypto to v0.0.0-20220507011949-2cf3adece122 ([#63](https://github.com/minamijoyo/myaws/pull/63)) 44 | 45 | ## 0.4.3 (2022/04/12) 46 | 47 | NEW FEATURES: 48 | 49 | * [ssm parameter env] add quote parameter ([#59](https://github.com/minamijoyo/myaws/pull/59)) 50 | 51 | ENHANCEMENTS: 52 | 53 | * Use golangci-lint instead of golint ([#57](https://github.com/minamijoyo/myaws/pull/57)) 54 | * Update golangci-lint-action to v3 ([#60](https://github.com/minamijoyo/myaws/pull/60)) 55 | * [github actions] fix checkout step ([#61](https://github.com/minamijoyo/myaws/pull/61)) 56 | 57 | ## 0.4.2 (2021/11/18) 58 | 59 | ENHANCEMENTS: 60 | 61 | * Add release target to arm64 architecture ([#54](https://github.com/minamijoyo/myaws/pull/54)) 62 | * Update Go to v1.17.3 and Alpine to 3.14 ([#55](https://github.com/minamijoyo/myaws/pull/55)) 63 | * Remove docker build on pull request ([#56](https://github.com/minamijoyo/myaws/pull/56)) 64 | 65 | ## 0.4.1 (2021/10/28) 66 | 67 | * Restrict permissions for GitHub Actions ([#51](https://github.com/minamijoyo/myaws/pull/51)) 68 | * Set timeout for GitHub Actions ([#52](https://github.com/minamijoyo/myaws/pull/52)) 69 | 70 | ## 0.4.0 (2021/07/19) 71 | 72 | This releases contains small breaking changes to improve CI/CD workflow. AWS related functionalities didn't change. 73 | 74 | BREAKING CHANGES: 75 | 76 | * Build & push docker images on GitHub Actions ([#50](https://github.com/minamijoyo/myaws/pull/50)) 77 | 78 | The `latest` tag of docker image now points at the latest release. Previously the `latest` tag pointed at the master branch, if you want to use the master branch, use the `master` tag instead. 79 | 80 | * Set a version number explicitly in source ([#43](https://github.com/minamijoyo/myaws/pull/43)) 81 | 82 | The `version` command now contains only a version number, not a revision (commit SHA1). 83 | 84 | ENHANCEMENTS: 85 | 86 | * Fix release archives in goreleaser.yml ([#49](https://github.com/minamijoyo/myaws/pull/49)) 87 | * Fix go mod tidy ([#48](https://github.com/minamijoyo/myaws/pull/48)) 88 | * Drop goreleaser dependencies ([#47](https://github.com/minamijoyo/myaws/pull/47)) 89 | * Move CI to GitHub Actions ([#46](https://github.com/minamijoyo/myaws/pull/46)) 90 | * Ignore updating README and CHANGELOG in release notes ([#45](https://github.com/minamijoyo/myaws/pull/45)) 91 | * Cache go modules in docker build ([#44](https://github.com/minamijoyo/myaws/pull/44)) 92 | 93 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine3.20 AS build-env 2 | RUN apk --no-cache add make git 3 | 4 | # A workaround for a permission issue of git. 5 | # Since UIDs are different between host and container, 6 | # the .git directory is untrusted by default. 7 | # We need to allow it explicitly. 8 | # https://github.com/actions/checkout/issues/760 9 | RUN git config --global --add safe.directory /work 10 | 11 | WORKDIR /work 12 | 13 | COPY go.mod go.sum ./ 14 | RUN go mod download 15 | 16 | COPY . . 17 | RUN make build 18 | 19 | FROM alpine:3.20 20 | RUN apk --no-cache add ca-certificates && update-ca-certificates 21 | COPY --from=build-env /work/bin/myaws /usr/local/bin/myaws 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 Masayuki Morita 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := myaws 2 | 3 | .DEFAULT_GOAL := build 4 | 5 | .PHONY: deps 6 | deps: 7 | go mod download 8 | 9 | .PHONY: build 10 | build: deps 11 | go build -o bin/$(NAME) 12 | 13 | .PHONY: lint 14 | lint: 15 | golangci-lint run ./... 16 | 17 | .PHONY: test 18 | test: build 19 | go test ./... 20 | 21 | .PHONY: check 22 | check: lint test 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MyAWS 2 | 3 | A human friendly AWS CLI written in Go. 4 | 5 | The official aws-cli is useful but too generic. It has many arguments and options and generates huge JSON outputs. But, in most cases, my interesting resources are the same. By setting my favorite default values, MyAWS provides a simple command line interface. 6 | 7 | Note that MyAWS is under development and its interface is unstable. 8 | 9 | # Installation 10 | 11 | If you are Mac OSX user: 12 | 13 | ```bash 14 | $ brew install minamijoyo/myaws/myaws 15 | ``` 16 | 17 | or 18 | 19 | If you have Go 1.22+ development environment: 20 | 21 | ```bash 22 | $ go install github.com/minamijoyo/myaws@latest 23 | ``` 24 | 25 | or 26 | 27 | Download the latest compiled binaries and put it anywhere in your executable path. 28 | 29 | https://github.com/minamijoyo/myaws/releases 30 | 31 | # Configuration 32 | ## Required 33 | MyAWS invokes AWS API call via aws-sdk-go. 34 | Export environment variables for your AWS credentials: 35 | 36 | ```bash 37 | $ export AWS_ACCESS_KEY_ID=XXXXXX 38 | $ export AWS_SECRET_ACCESS_KEY=XXXXXX 39 | $ export AWS_DEFAULT_REGION=XXXXXX 40 | ``` 41 | 42 | or set your credentials in `$HOME/.aws/credentials` : 43 | 44 | ``` 45 | [default] 46 | aws_access_key_id = XXXXXX 47 | aws_secret_access_key = XXXXXX 48 | ``` 49 | 50 | or IAM Task Role (ECS) or IAM Role are also available. 51 | 52 | AWS credentials are checked in the order of 53 | profile, environment variables, IAM Task Role (ECS), IAM Role. 54 | Unlike the aws default, load profile before environment variables 55 | because we want to prioritize explicit arguments over the environment. 56 | 57 | AWS region can be set in Environment variable ( `AWS_DEFAULT_REGION` ), configuration file ( `$HOME/.myaws.yaml` ) , or command argument ( `--region` ). 58 | 59 | ## Optional 60 | 61 | Configuration file is optional. 62 | 63 | MyAWS read default configuration from `$HOME/.myaws.yml` 64 | 65 | A sample configuration looks like the following: 66 | 67 | ```yaml 68 | profile: default 69 | region: ap-northeast-1 70 | ec2: 71 | ls: 72 | all: false 73 | fields: 74 | - InstanceId 75 | - InstanceType 76 | - PublicIpAddress 77 | - PrivateIpAddress 78 | - StateName 79 | - LaunchTime 80 | - Tag:Name 81 | - Tag:attached_asg 82 | ``` 83 | 84 | # Example 85 | 86 | ```bash 87 | $ myaws ec2 ls 88 | i-0f48fxxxxxxxxxxxx t2.micro 52.197.xxx.xxx 10.193.xxx.xxx running 1 minute ago proxy 89 | i-0e267xxxxxxxxxxxx t2.medium 52.198.xxx.xxx 10.193.xxx.xxx running 2 days ago app 90 | i-0fdaaxxxxxxxxxxxx t2.large 52.197.xxx.xxx 10.193.xxx.xxx running 1 month ago batch 91 | ``` 92 | 93 | # Usage 94 | 95 | ```bash 96 | $ myaws --help 97 | A human friendly AWS CLI written in Go. 98 | 99 | Usage: 100 | myaws [command] 101 | 102 | Available Commands: 103 | autoscaling Manage autoscaling resources 104 | completion Generates shell completion scripts 105 | ec2 Manage EC2 resources 106 | ec2ri Manage EC2 Reserved Instance resources 107 | ecr Manage ECR resources 108 | ecs Manage ECS resources 109 | elb Manage ELB resources 110 | elbv2 Manage ELBV2 resources 111 | help Help about any command 112 | iam Manage IAM resources 113 | rds Manage RDS resources 114 | ssm Manage SSM resources 115 | sts Manage STS resources 116 | version Print version 117 | 118 | Flags: 119 | --config string config file (default $HOME/.myaws.yml) 120 | --debug Enable debug mode 121 | -h, --help help for myaws 122 | --humanize Use Human friendly format for time (default true) 123 | --profile string AWS profile (default none and used AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY environment variables.) 124 | --region string AWS region (default none and used AWS_DEFAULT_REGION environment variable. 125 | --timezone string Time zone, such as UTC, Asia/Tokyo (default "Local") 126 | 127 | Use "myaws [command] --help" for more information about a command. 128 | ``` 129 | 130 | ```bash 131 | $ myaws ec2 ls --help 132 | List EC2 instances 133 | 134 | Usage: 135 | myaws ec2 ls [flags] 136 | 137 | Flags: 138 | -a, --all List all instances (by default, list running instances only) 139 | -F, --fields string Output fields list separated by space (default "InstanceId InstanceType PublicIpAddress PrivateIpAddress AvailabilityZone StateName LaunchTime Tag:Name") 140 | -t, --filter-tag string Filter instances by tag, such as "Name:app-production". The value of tag is assumed to be a partial match 141 | -q, --quiet Only display InstanceIDs 142 | 143 | Global Flags: 144 | --config string config file (default $HOME/.myaws.yml) 145 | --humanize Use Human friendly format for time (default true) 146 | --profile string AWS profile (default none and used AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY environment variables.) 147 | --region string AWS region (default none and used AWS_DEFAULT_REGION environment variable. 148 | --timezone string Time zone, such as UTC, Asia/Tokyo (default "Local") 149 | ``` 150 | 151 | # LICENCE 152 | 153 | MIT 154 | 155 | -------------------------------------------------------------------------------- /cmd/autoscaling.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | 9 | "github.com/minamijoyo/myaws/myaws" 10 | ) 11 | 12 | func init() { 13 | RootCmd.AddCommand(newAutoscalingCmd()) 14 | } 15 | 16 | func newAutoscalingCmd() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "autoscaling", 19 | Aliases: []string{"as"}, 20 | Short: "Manage autoscaling resources", 21 | Run: func(cmd *cobra.Command, _ []string) { 22 | cmd.Help() // nolint: errcheck 23 | }, 24 | } 25 | 26 | cmd.AddCommand( 27 | newAutoscalingLsCmd(), 28 | newAutoscalingAttachCmd(), 29 | newAutoscalingDetachCmd(), 30 | newAutoscalingUpdateCmd(), 31 | ) 32 | 33 | return cmd 34 | } 35 | 36 | func newAutoscalingLsCmd() *cobra.Command { 37 | cmd := &cobra.Command{ 38 | Use: "ls", 39 | Short: "List autoscaling groups", 40 | RunE: runAutoscalingLsCmd, 41 | } 42 | 43 | flags := cmd.Flags() 44 | flags.BoolP("all", "a", false, "List all autoscaling groups (by default, list autoscaling groups only having at least 1 attached instance)") 45 | 46 | viper.BindPFlag("autoscaling.ls.all", flags.Lookup("all")) // nolint: errcheck 47 | 48 | return cmd 49 | } 50 | 51 | func runAutoscalingLsCmd(_ *cobra.Command, _ []string) error { 52 | client, err := newClient() 53 | if err != nil { 54 | return errors.Wrap(err, "newClient failed:") 55 | } 56 | 57 | options := myaws.AutoscalingLsOptions{ 58 | All: viper.GetBool("autoscaling.ls.all"), 59 | } 60 | 61 | return client.AutoscalingLs(options) 62 | } 63 | 64 | func newAutoscalingAttachCmd() *cobra.Command { 65 | cmd := &cobra.Command{ 66 | Use: "attach AUTO_SCALING_GROUP_NAME", 67 | Short: "Attach instances/loadbalancers to autoscaling group", 68 | RunE: runAutoscalingAttachCmd, 69 | } 70 | 71 | flags := cmd.Flags() 72 | flags.StringP("instance-ids", "i", "", "One or more instance IDs") 73 | flags.StringP("load-balancer-names", "l", "", "One or more load balancer names") 74 | flags.BoolP("wait", "w", false, "Wait until desired capacity instances are InService") 75 | 76 | viper.BindPFlag("autoscaling.attach.instance-ids", flags.Lookup("instance-ids")) // nolint: errcheck 77 | viper.BindPFlag("autoscaling.attach.load-balancer-names", flags.Lookup("load-balancer-names")) // nolint: errcheck 78 | viper.BindPFlag("autoscaling.attach.wait", flags.Lookup("wait")) // nolint: errcheck 79 | 80 | return cmd 81 | } 82 | 83 | func runAutoscalingAttachCmd(_ *cobra.Command, args []string) error { 84 | client, err := newClient() 85 | if err != nil { 86 | return errors.Wrap(err, "newClient failed:") 87 | } 88 | 89 | if len(args) == 0 { 90 | return errors.New("AUTO_SCALING_GROUP_NAME is required") 91 | } 92 | 93 | instanceIds := aws.StringSlice(viper.GetStringSlice("autoscaling.attach.instance-ids")) 94 | loadBalancerNames := aws.StringSlice(viper.GetStringSlice("autoscaling.attach.load-balancer-names")) 95 | options := myaws.AutoscalingAttachOptions{ 96 | AsgName: args[0], 97 | InstanceIds: instanceIds, 98 | LoadBalancerNames: loadBalancerNames, 99 | Wait: viper.GetBool("autoscaling.attach.wait"), 100 | } 101 | 102 | return client.AutoscalingAttach(options) 103 | } 104 | 105 | func newAutoscalingDetachCmd() *cobra.Command { 106 | cmd := &cobra.Command{ 107 | Use: "detach AUTO_SCALING_GROUP_NAME", 108 | Short: "Detach instances/loadbalancers from autoscaling group", 109 | RunE: runAutoscalingDetachCmd, 110 | } 111 | 112 | flags := cmd.Flags() 113 | flags.StringP("instance-ids", "i", "", "One or more instance IDs") 114 | flags.StringP("load-balancer-names", "l", "", "One or more load balancer names") 115 | flags.BoolP("wait", "w", false, "Wait until desired capacity instances are InService") 116 | 117 | viper.BindPFlag("autoscaling.detach.instance-ids", flags.Lookup("instance-ids")) // nolint: errcheck 118 | viper.BindPFlag("autoscaling.detach.load-balancer-names", flags.Lookup("load-balancer-names")) // nolint: errcheck 119 | viper.BindPFlag("autoscaling.detach.wait", flags.Lookup("wait")) // nolint: errcheck 120 | 121 | return cmd 122 | } 123 | 124 | func runAutoscalingDetachCmd(_ *cobra.Command, args []string) error { 125 | client, err := newClient() 126 | if err != nil { 127 | return errors.Wrap(err, "newClient failed:") 128 | } 129 | 130 | if len(args) == 0 { 131 | return errors.New("AUTO_SCALING_GROUP_NAME is required") 132 | } 133 | 134 | instanceIds := aws.StringSlice(viper.GetStringSlice("autoscaling.detach.instance-ids")) 135 | loadBalancerNames := aws.StringSlice(viper.GetStringSlice("autoscaling.detach.load-balancer-names")) 136 | options := myaws.AutoscalingDetachOptions{ 137 | AsgName: args[0], 138 | InstanceIds: instanceIds, 139 | LoadBalancerNames: loadBalancerNames, 140 | Wait: viper.GetBool("autoscaling.detach.wait"), 141 | } 142 | 143 | return client.AutoscalingDetach(options) 144 | } 145 | 146 | func newAutoscalingUpdateCmd() *cobra.Command { 147 | cmd := &cobra.Command{ 148 | Use: "update AUTO_SCALING_GROUP_NAME", 149 | Short: "Update autoscaling group", 150 | RunE: runAutoscalingUpdateCmd, 151 | } 152 | 153 | flags := cmd.Flags() 154 | flags.Int64P("desired-capacity", "c", -1, "The number of EC2 instances that should be running in the Auto Scaling group.") 155 | flags.BoolP("wait", "w", false, "Wait until desired capacity instances are InService") 156 | 157 | viper.BindPFlag("autoscaling.update.desired-capacity", flags.Lookup("desired-capacity")) // nolint: errcheck 158 | viper.BindPFlag("autoscaling.update.wait", flags.Lookup("wait")) // nolint: errcheck 159 | 160 | return cmd 161 | } 162 | 163 | func runAutoscalingUpdateCmd(_ *cobra.Command, args []string) error { 164 | client, err := newClient() 165 | if err != nil { 166 | return errors.Wrap(err, "newClient failed:") 167 | } 168 | 169 | if len(args) == 0 { 170 | return errors.New("AUTO_SCALING_GROUP_NAME is required") 171 | } 172 | 173 | desiredCapacity := viper.GetInt64("autoscaling.update.desired-capacity") 174 | if desiredCapacity == -1 { 175 | return errors.New("--desired-capacity is required") 176 | } 177 | 178 | options := myaws.AutoscalingUpdateOptions{ 179 | AsgName: args[0], 180 | DesiredCapacity: desiredCapacity, 181 | Wait: viper.GetBool("autoscaling.update.wait"), 182 | } 183 | 184 | return client.AutoscalingUpdate(options) 185 | } 186 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | RootCmd.AddCommand(newCompletionCmd()) 11 | } 12 | 13 | func newCompletionCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "completion", 16 | Short: "Generates shell completion scripts", 17 | Run: func(cmd *cobra.Command, _ []string) { 18 | cmd.Help() // nolint: errcheck 19 | }, 20 | } 21 | 22 | cmd.AddCommand( 23 | newCompletionBashCmd(), 24 | newCompletionZshCmd(), 25 | ) 26 | 27 | return cmd 28 | } 29 | 30 | func newCompletionBashCmd() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "bash", 33 | Short: "Generates bash completion scripts", 34 | Run: func(_ *cobra.Command, _ []string) { 35 | RootCmd.GenBashCompletion(os.Stdout) // nolint: errcheck 36 | }, 37 | } 38 | 39 | return cmd 40 | } 41 | 42 | func newCompletionZshCmd() *cobra.Command { 43 | cmd := &cobra.Command{ 44 | Use: "zsh", 45 | Short: "Generates zsh completion scripts", 46 | Run: func(_ *cobra.Command, _ []string) { 47 | RootCmd.GenZshCompletion(os.Stdout) // nolint: errcheck 48 | }, 49 | } 50 | 51 | return cmd 52 | } 53 | -------------------------------------------------------------------------------- /cmd/ec2.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | 11 | "github.com/minamijoyo/myaws/myaws" 12 | ) 13 | 14 | func init() { 15 | RootCmd.AddCommand(newEC2Cmd()) 16 | } 17 | 18 | func newEC2Cmd() *cobra.Command { 19 | cmd := &cobra.Command{ 20 | Use: "ec2", 21 | Short: "Manage EC2 resources", 22 | Run: func(cmd *cobra.Command, _ []string) { 23 | cmd.Help() // nolint: errcheck 24 | }, 25 | } 26 | 27 | cmd.AddCommand( 28 | newEC2LsCmd(), 29 | newEC2StartCmd(), 30 | newEC2StopCmd(), 31 | newEC2SSHCmd(), 32 | ) 33 | 34 | return cmd 35 | } 36 | 37 | func newEC2LsCmd() *cobra.Command { 38 | cmd := &cobra.Command{ 39 | Use: "ls", 40 | Short: "List EC2 instances", 41 | RunE: runEC2LsCmd, 42 | } 43 | 44 | flags := cmd.Flags() 45 | flags.BoolP("all", "a", false, "List all instances (by default, list running instances only)") 46 | flags.BoolP("quiet", "q", false, "Only display InstanceIDs") 47 | flags.StringP("filter-tag", "t", "", 48 | "Filter instances by tag, such as \"Name:app-production\". The value of tag is assumed to be a partial match", 49 | ) 50 | flags.StringP("fields", "F", "InstanceId InstanceType PublicIpAddress PrivateIpAddress AvailabilityZone StateName LaunchTime Tag:Name", "Output fields list separated by space") 51 | 52 | viper.BindPFlag("ec2.ls.all", flags.Lookup("all")) // nolint: errcheck 53 | viper.BindPFlag("ec2.ls.quiet", flags.Lookup("quiet")) // nolint: errcheck 54 | viper.BindPFlag("ec2.ls.filter-tag", flags.Lookup("filter-tag")) // nolint: errcheck 55 | viper.BindPFlag("ec2.ls.fields", flags.Lookup("fields")) // nolint: errcheck 56 | 57 | return cmd 58 | } 59 | 60 | func runEC2LsCmd(_ *cobra.Command, _ []string) error { 61 | client, err := newClient() 62 | if err != nil { 63 | return errors.Wrap(err, "newClient failed:") 64 | } 65 | 66 | options := myaws.EC2LsOptions{ 67 | All: viper.GetBool("ec2.ls.all"), 68 | Quiet: viper.GetBool("ec2.ls.quiet"), 69 | FilterTag: viper.GetString("ec2.ls.filter-tag"), 70 | Fields: viper.GetStringSlice("ec2.ls.fields"), 71 | } 72 | 73 | return client.EC2Ls(options) 74 | } 75 | 76 | func newEC2StartCmd() *cobra.Command { 77 | cmd := &cobra.Command{ 78 | Use: "start INSTANCE_ID [...]", 79 | Short: "Start EC2 instances", 80 | RunE: runEC2StartCmd, 81 | } 82 | 83 | flags := cmd.Flags() 84 | flags.BoolP("wait", "w", false, "Wait until instance running") 85 | 86 | viper.BindPFlag("ec2.start.wait", flags.Lookup("wait")) // nolint: errcheck 87 | 88 | return cmd 89 | } 90 | 91 | func runEC2StartCmd(_ *cobra.Command, args []string) error { 92 | client, err := newClient() 93 | if err != nil { 94 | return errors.Wrap(err, "newClient failed:") 95 | } 96 | 97 | if len(args) == 0 { 98 | return errors.New("INSTANCE_ID is required") 99 | } 100 | instanceIds := aws.StringSlice(args) 101 | 102 | options := myaws.EC2StartOptions{ 103 | InstanceIds: instanceIds, 104 | Wait: viper.GetBool("ec2.start.wait"), 105 | } 106 | 107 | return client.EC2Start(options) 108 | } 109 | 110 | func newEC2StopCmd() *cobra.Command { 111 | cmd := &cobra.Command{ 112 | Use: "stop INSTANCE_ID [...]", 113 | Short: "Stop EC2 instances", 114 | RunE: runEC2StopCmd, 115 | } 116 | 117 | flags := cmd.Flags() 118 | flags.BoolP("wait", "w", false, "Wait until instance stopped") 119 | 120 | viper.BindPFlag("ec2.stop.wait", flags.Lookup("wait")) // nolint: errcheck 121 | 122 | return cmd 123 | } 124 | 125 | func runEC2StopCmd(_ *cobra.Command, args []string) error { 126 | client, err := newClient() 127 | if err != nil { 128 | return errors.Wrap(err, "newClient failed:") 129 | } 130 | 131 | if len(args) == 0 { 132 | return errors.New("INSTANCE_ID is required") 133 | } 134 | instanceIds := aws.StringSlice(args) 135 | 136 | options := myaws.EC2StopOptions{ 137 | InstanceIds: instanceIds, 138 | Wait: viper.GetBool("ec2.stop.wait"), 139 | } 140 | 141 | return client.EC2Stop(options) 142 | } 143 | 144 | func newEC2SSHCmd() *cobra.Command { 145 | cmd := &cobra.Command{ 146 | Use: "ssh [USER@]INSTANCE_NAME [COMMAND...]", 147 | Short: "SSH to EC2 instances", 148 | RunE: runEC2SSHCmd, 149 | } 150 | 151 | flags := cmd.Flags() 152 | flags.StringP("login-name", "l", "", "Login username") 153 | flags.StringP("identity-file", "i", "~/.ssh/id_rsa", "SSH private key file") 154 | flags.BoolP("private", "", false, "Use private IP to connect") 155 | 156 | viper.BindPFlag("ec2.ssh.login-name", flags.Lookup("login-name")) // nolint: errcheck 157 | viper.BindPFlag("ec2.ssh.identity-file", flags.Lookup("identity-file")) // nolint: errcheck 158 | viper.BindPFlag("ec2.ssh.private", flags.Lookup("private")) // nolint: errcheck 159 | 160 | return cmd 161 | } 162 | 163 | func runEC2SSHCmd(_ *cobra.Command, args []string) error { 164 | client, err := newClient() 165 | if err != nil { 166 | return errors.Wrap(err, "newClient failed:") 167 | } 168 | 169 | if len(args) == 0 { 170 | return errors.New("Instance name is required") 171 | } 172 | 173 | var loginName, instanceName string 174 | if strings.Contains(args[0], "@") { 175 | // parse loginName@instanceName format 176 | splitted := strings.SplitN(args[0], "@", 2) 177 | loginName, instanceName = splitted[0], splitted[1] 178 | } else { 179 | loginName = viper.GetString("ec2.ssh.login-name") 180 | instanceName = args[0] 181 | } 182 | 183 | filterTag := "Name:" + instanceName 184 | 185 | var command string 186 | if len(args) >= 2 { 187 | command = strings.Join(args[1:], " ") 188 | } 189 | options := myaws.EC2SSHOptions{ 190 | FilterTag: filterTag, 191 | LoginName: loginName, 192 | IdentityFile: viper.GetString("ec2.ssh.identity-file"), 193 | Private: viper.GetBool("ec2.ssh.private"), 194 | Command: command, 195 | } 196 | 197 | return client.EC2SSH(options) 198 | } 199 | -------------------------------------------------------------------------------- /cmd/ec2ri.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/minamijoyo/myaws/myaws" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func init() { 11 | RootCmd.AddCommand(newEC2RICmd()) 12 | } 13 | 14 | func newEC2RICmd() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "ec2ri", 17 | Short: "Manage EC2 Reserved Instance resources", 18 | Run: func(cmd *cobra.Command, _ []string) { 19 | cmd.Help() // nolint: errcheck 20 | }, 21 | } 22 | 23 | cmd.AddCommand( 24 | newEC2RILsCmd(), 25 | ) 26 | 27 | return cmd 28 | } 29 | 30 | func newEC2RILsCmd() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "ls", 33 | Short: "List EC2 Reserved Instances", 34 | RunE: runEC2RILsCmd, 35 | } 36 | 37 | flags := cmd.Flags() 38 | flags.BoolP("all", "a", false, "List all reserved instances (by default, list active reserved instances only)") 39 | flags.BoolP("quiet", "q", false, "Only display ReservedInstanceIDs") 40 | flags.StringP("fields", "F", "ReservedInstancesId State Scope AvailabilityZone InstanceType InstanceCount Duration Start End", "Output fields list separated by space") 41 | 42 | viper.BindPFlag("ec2ri.ls.all", flags.Lookup("all")) // nolint: errcheck 43 | viper.BindPFlag("ec2ri.ls.quiet", flags.Lookup("quiet")) // nolint: errcheck 44 | viper.BindPFlag("ec2ri.ls.fields", flags.Lookup("fields")) // nolint: errcheck 45 | 46 | return cmd 47 | } 48 | 49 | func runEC2RILsCmd(_ *cobra.Command, _ []string) error { 50 | client, err := newClient() 51 | if err != nil { 52 | return errors.Wrap(err, "newClient failed:") 53 | } 54 | 55 | options := myaws.EC2RILsOptions{ 56 | All: viper.GetBool("ec2ri.ls.all"), 57 | Quiet: viper.GetBool("ec2ri.ls.quiet"), 58 | Fields: viper.GetStringSlice("ec2ri.ls.fields"), 59 | } 60 | 61 | return client.EC2RILs(options) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/ecr.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | 9 | "github.com/minamijoyo/myaws/myaws" 10 | ) 11 | 12 | func init() { 13 | RootCmd.AddCommand(newECRCmd()) 14 | } 15 | 16 | func newECRCmd() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "ecr", 19 | Short: "Manage ECR resources", 20 | Run: func(cmd *cobra.Command, _ []string) { 21 | cmd.Help() // nolint: errcheck 22 | }, 23 | } 24 | 25 | cmd.AddCommand( 26 | newECRGetLoginCmd(), 27 | ) 28 | 29 | return cmd 30 | } 31 | 32 | func newECRGetLoginCmd() *cobra.Command { 33 | cmd := &cobra.Command{ 34 | Use: "get-login", 35 | Short: "Get docker login command for ECR", 36 | RunE: runECRGetLoginCmd, 37 | } 38 | 39 | flags := cmd.Flags() 40 | flags.StringSliceP("registry-ids", "r", []string{}, "A list of AWS account IDs") 41 | 42 | viper.BindPFlag("ecr.get-login.registry-ids", flags.Lookup("registry-ids")) // nolint: errcheck 43 | 44 | return cmd 45 | } 46 | 47 | func runECRGetLoginCmd(_ *cobra.Command, _ []string) error { 48 | client, err := newClient() 49 | if err != nil { 50 | return errors.Wrap(err, "newClient failed:") 51 | } 52 | 53 | registryIds := aws.StringSlice(viper.GetStringSlice("ecr.get-login.registry-ids")) 54 | options := myaws.ECRGetLoginOptions{ 55 | RegistryIds: registryIds, 56 | } 57 | 58 | return client.ECRGetLogin(options) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/ecs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/minamijoyo/myaws/myaws" 8 | "github.com/pkg/errors" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func init() { 14 | RootCmd.AddCommand(newECSCmd()) 15 | } 16 | 17 | func newECSCmd() *cobra.Command { 18 | cmd := &cobra.Command{ 19 | Use: "ecs", 20 | Short: "Manage ECS resources", 21 | Run: func(cmd *cobra.Command, _ []string) { 22 | cmd.Help() // nolint: errcheck 23 | }, 24 | } 25 | 26 | cmd.AddCommand( 27 | newECSStatusCmd(), 28 | newECSNodeCmd(), 29 | newECSServiceCmd(), 30 | ) 31 | 32 | return cmd 33 | } 34 | 35 | func newECSStatusCmd() *cobra.Command { 36 | cmd := &cobra.Command{ 37 | Use: "status CLUSTER", 38 | Short: "Print ECS status", 39 | RunE: runECSStatusCmd, 40 | } 41 | 42 | return cmd 43 | } 44 | 45 | func runECSStatusCmd(_ *cobra.Command, args []string) error { 46 | client, err := newClient() 47 | if err != nil { 48 | return errors.Wrap(err, "newClient failed:") 49 | } 50 | 51 | if len(args) == 0 { 52 | return errors.New("CLUSTER is required") 53 | } 54 | 55 | options := myaws.ECSStatusOptions{ 56 | Cluster: args[0], 57 | } 58 | return client.ECSStatus(options) 59 | } 60 | 61 | func newECSNodeCmd() *cobra.Command { 62 | cmd := &cobra.Command{ 63 | Use: "node", 64 | Short: "Manage ECS node resources (container instances)", 65 | Run: func(cmd *cobra.Command, _ []string) { 66 | cmd.Help() // nolint: errcheck 67 | }, 68 | } 69 | 70 | cmd.AddCommand( 71 | newECSNodeLsCmd(), 72 | newECSNodeUpdateCmd(), 73 | newECSNodeDrainCmd(), 74 | newECSNodeRenewCmd(), 75 | ) 76 | 77 | return cmd 78 | } 79 | 80 | func newECSNodeLsCmd() *cobra.Command { 81 | cmd := &cobra.Command{ 82 | Use: "ls CLUSTER", 83 | Short: "List ECS nodes (container instances)", 84 | RunE: runECSNodeLsCmd, 85 | } 86 | 87 | flags := cmd.Flags() 88 | flags.BoolP("print-header", "H", false, "Print Header") 89 | 90 | viper.BindPFlag("ecs.node.ls.print-header", flags.Lookup("print-header")) // nolint: errcheck 91 | 92 | return cmd 93 | } 94 | 95 | func runECSNodeLsCmd(_ *cobra.Command, args []string) error { 96 | client, err := newClient() 97 | if err != nil { 98 | return errors.Wrap(err, "newClient failed:") 99 | } 100 | 101 | if len(args) == 0 { 102 | return errors.New("CLUSTER is required") 103 | } 104 | 105 | options := myaws.ECSNodeLsOptions{ 106 | Cluster: args[0], 107 | PrintHeader: viper.GetBool("ecs.node.ls.print-header"), 108 | } 109 | return client.ECSNodeLs(options) 110 | } 111 | 112 | func newECSNodeUpdateCmd() *cobra.Command { 113 | cmd := &cobra.Command{ 114 | Use: "update CLUSTER", 115 | Short: "Update ECS nodes (container instances)", 116 | RunE: runECSNodeUpdateCmd, 117 | } 118 | 119 | flags := cmd.Flags() 120 | flags.StringP("container-instances", "i", "", "A list of container instance IDs or full ARN entries separated by space") 121 | flags.StringP("status", "s", "", "container instance state (ACTIVE | DRAINING)") 122 | 123 | viper.BindPFlag("ecs.node.update.container-instances", flags.Lookup("container-instances")) // nolint: errcheck 124 | viper.BindPFlag("ecs.node.update.status", flags.Lookup("status")) // nolint: errcheck 125 | 126 | return cmd 127 | } 128 | 129 | func runECSNodeUpdateCmd(_ *cobra.Command, args []string) error { 130 | client, err := newClient() 131 | if err != nil { 132 | return errors.Wrap(err, "newClient failed:") 133 | } 134 | 135 | if len(args) == 0 { 136 | return errors.New("CLUSTER is required") 137 | } 138 | 139 | containerInstances := aws.StringSlice(viper.GetStringSlice("ecs.node.update.container-instances")) 140 | if len(containerInstances) == 0 { 141 | return errors.New("container-instances is required") 142 | } 143 | 144 | status := viper.GetString("ecs.node.update.status") 145 | if len(status) == 0 { 146 | return errors.New("status is required") 147 | } 148 | 149 | options := myaws.ECSNodeUpdateOptions{ 150 | Cluster: args[0], 151 | ContainerInstances: containerInstances, 152 | Status: status, 153 | } 154 | 155 | return client.ECSNodeUpdate(options) 156 | } 157 | 158 | func newECSNodeDrainCmd() *cobra.Command { 159 | cmd := &cobra.Command{ 160 | Use: "drain CLUSTER", 161 | Short: "Drain ECS nodes (container instances)", 162 | RunE: runECSNodeDrainCmd, 163 | } 164 | 165 | flags := cmd.Flags() 166 | flags.StringP("container-instances", "i", "", "A list of container instance IDs or full ARN entries separated by space") 167 | flags.BoolP("wait", "w", false, "Wait until container instances are drained") 168 | flags.Int64P("timeout", "t", 600, "Number of secconds to wait before timeout") 169 | 170 | viper.BindPFlag("ecs.node.drain.container-instances", flags.Lookup("container-instances")) // nolint: errcheck 171 | viper.BindPFlag("ecs.node.drain.wait", flags.Lookup("wait")) // nolint: errcheck 172 | viper.BindPFlag("ecs.node.drain.timeout", flags.Lookup("timeout")) // nolint: errcheck 173 | 174 | return cmd 175 | } 176 | 177 | func runECSNodeDrainCmd(_ *cobra.Command, args []string) error { 178 | client, err := newClient() 179 | if err != nil { 180 | return errors.Wrap(err, "newClient failed:") 181 | } 182 | 183 | if len(args) == 0 { 184 | return errors.New("CLUSTER is required") 185 | } 186 | 187 | containerInstances := aws.StringSlice(viper.GetStringSlice("ecs.node.drain.container-instances")) 188 | if len(containerInstances) == 0 { 189 | return errors.New("container-instances is required") 190 | } 191 | 192 | timeout := time.Duration(viper.GetInt64("ecs.node.drain.timeout")) * time.Second 193 | 194 | options := myaws.ECSNodeDrainOptions{ 195 | Cluster: args[0], 196 | ContainerInstances: containerInstances, 197 | Wait: viper.GetBool("ecs.node.drain.wait"), 198 | Timeout: timeout, 199 | } 200 | 201 | return client.ECSNodeDrain(options) 202 | } 203 | 204 | func newECSNodeRenewCmd() *cobra.Command { 205 | cmd := &cobra.Command{ 206 | Use: "renew CLUSTER", 207 | Short: "Renew ECS nodes (container instances) with blue-grean deployment", 208 | RunE: runECSNodeRenewCmd, 209 | } 210 | 211 | flags := cmd.Flags() 212 | flags.StringP("asg-name", "a", "", "A name of AutoScalingGroup to which the ECS container instances belong") 213 | 214 | // Note that this is a total timeout, and indivisual wait operations can 215 | // timeout in shorter amount of time. 216 | flags.Int64P("timeout", "t", 3600, "Number of secconds to wait before timeout") 217 | 218 | viper.BindPFlag("ecs.node.renew.asg-name", flags.Lookup("asg-name")) // nolint: errcheck 219 | viper.BindPFlag("ecs.node.renew.timeout", flags.Lookup("timeout")) // nolint: errcheck 220 | 221 | return cmd 222 | } 223 | 224 | func runECSNodeRenewCmd(_ *cobra.Command, args []string) error { 225 | client, err := newClient() 226 | if err != nil { 227 | return errors.Wrap(err, "newClient failed:") 228 | } 229 | 230 | if len(args) == 0 { 231 | return errors.New("CLUSTER is required") 232 | } 233 | 234 | asgName := viper.GetString("ecs.node.renew.asg-name") 235 | if len(asgName) == 0 { 236 | return errors.New("asg-name is required") 237 | } 238 | 239 | timeout := time.Duration(viper.GetInt64("ecs.node.renew.timeout")) * time.Second 240 | 241 | options := myaws.ECSNodeRenewOptions{ 242 | Cluster: args[0], 243 | AsgName: asgName, 244 | Timeout: timeout, 245 | } 246 | 247 | return client.ECSNodeRenew(options) 248 | } 249 | 250 | func newECSServiceCmd() *cobra.Command { 251 | cmd := &cobra.Command{ 252 | Use: "service", 253 | Short: "Manage ECS service resources", 254 | Run: func(cmd *cobra.Command, _ []string) { 255 | cmd.Help() // nolint: errcheck 256 | }, 257 | } 258 | 259 | cmd.AddCommand( 260 | newECSServiceLsCmd(), 261 | newECSServiceUpdateCmd(), 262 | ) 263 | 264 | return cmd 265 | } 266 | 267 | func newECSServiceLsCmd() *cobra.Command { 268 | cmd := &cobra.Command{ 269 | Use: "ls CLUSTER", 270 | Short: "List ECS services", 271 | RunE: runECSServiceLsCmd, 272 | } 273 | 274 | flags := cmd.Flags() 275 | flags.BoolP("print-header", "H", false, "Print Header") 276 | 277 | viper.BindPFlag("ecs.service.ls.print-header", flags.Lookup("print-header")) // nolint: errcheck 278 | 279 | return cmd 280 | } 281 | 282 | func runECSServiceLsCmd(_ *cobra.Command, args []string) error { 283 | client, err := newClient() 284 | if err != nil { 285 | return errors.Wrap(err, "newClient failed:") 286 | } 287 | 288 | if len(args) == 0 { 289 | return errors.New("CLUSTER is required") 290 | } 291 | 292 | options := myaws.ECSServiceLsOptions{ 293 | Cluster: args[0], 294 | PrintHeader: viper.GetBool("ecs.service.ls.print-header"), 295 | } 296 | return client.ECSServiceLs(options) 297 | } 298 | 299 | func newECSServiceUpdateCmd() *cobra.Command { 300 | cmd := &cobra.Command{ 301 | Use: "update CLUSTER", 302 | Short: "Update ECS services", 303 | RunE: runECSServiceUpdateCmd, 304 | } 305 | 306 | flags := cmd.Flags() 307 | flags.StringP("service", "s", "", "Name of service to be updated") 308 | flags.Int64P("desired-capacity", "c", -1, "Number of task to place and keep running") 309 | flags.BoolP("wait", "w", false, "Wait until desired capacity tasks are InService") 310 | 311 | // We may use time.Duration directly here via flags.Duration, 312 | // but time.Duration is unfamiliar for non-Gopher 313 | // so we use simple int64 as seconds for CLI interface. 314 | flags.Int64P("timeout", "t", 600, "Number of secconds to wait before timeout") 315 | 316 | flags.BoolP("force", "f", false, "Force new deployment") 317 | 318 | viper.BindPFlag("ecs.service.update.service", flags.Lookup("service")) // nolint: errcheck 319 | viper.BindPFlag("ecs.service.update.desired-capacity", flags.Lookup("desired-capacity")) // nolint: errcheck 320 | viper.BindPFlag("ecs.service.update.wait", flags.Lookup("wait")) // nolint: errcheck 321 | viper.BindPFlag("ecs.service.update.timeout", flags.Lookup("timeout")) // nolint: errcheck 322 | viper.BindPFlag("ecs.service.update.force", flags.Lookup("force")) // nolint: errcheck 323 | return cmd 324 | } 325 | 326 | func runECSServiceUpdateCmd(_ *cobra.Command, args []string) error { 327 | client, err := newClient() 328 | if err != nil { 329 | return errors.Wrap(err, "newClient failed:") 330 | } 331 | 332 | if len(args) == 0 { 333 | return errors.New("CLUSTER is required") 334 | } 335 | 336 | service := viper.GetString("ecs.service.update.service") 337 | if len(service) == 0 { 338 | return errors.New("--service is required") 339 | } 340 | 341 | // For desiredCapacity, 0 is valid value. 342 | // So we use -1 as a default value which indicates unset. 343 | // In this case, ECSServiceUpdateOptions.DesiredCount should be nil to allow us force deploy 344 | desiredCapacity := viper.GetInt64("ecs.service.update.desired-capacity") 345 | var desiredCapacityP *int64 346 | if desiredCapacity != -1 { 347 | desiredCapacityP = &desiredCapacity 348 | } 349 | 350 | timeout := time.Duration(viper.GetInt64("ecs.service.update.timeout")) * time.Second 351 | 352 | options := myaws.ECSServiceUpdateOptions{ 353 | Cluster: args[0], 354 | Service: service, 355 | DesiredCount: desiredCapacityP, 356 | Wait: viper.GetBool("ecs.service.update.wait"), 357 | Timeout: timeout, 358 | Force: viper.GetBool("ecs.service.update.force"), 359 | } 360 | return client.ECSServiceUpdate(options) 361 | } 362 | -------------------------------------------------------------------------------- /cmd/elb.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/minamijoyo/myaws/myaws" 8 | ) 9 | 10 | func init() { 11 | RootCmd.AddCommand(newELBCmd()) 12 | } 13 | 14 | func newELBCmd() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "elb", 17 | Short: "Manage ELB resources", 18 | Run: func(cmd *cobra.Command, _ []string) { 19 | cmd.Help() // nolint: errcheck 20 | }, 21 | } 22 | 23 | cmd.AddCommand( 24 | newELBLsCmd(), 25 | newELBPsCmd(), 26 | ) 27 | 28 | return cmd 29 | } 30 | 31 | func newELBLsCmd() *cobra.Command { 32 | cmd := &cobra.Command{ 33 | Use: "ls", 34 | Short: "List ELB instances", 35 | RunE: runELBLsCmd, 36 | } 37 | 38 | return cmd 39 | } 40 | 41 | func runELBLsCmd(_ *cobra.Command, _ []string) error { 42 | client, err := newClient() 43 | if err != nil { 44 | return errors.Wrap(err, "newClient failed:") 45 | } 46 | 47 | return client.ELBLs() 48 | } 49 | 50 | func newELBPsCmd() *cobra.Command { 51 | cmd := &cobra.Command{ 52 | Use: "ps ELB_NAME", 53 | Short: "Show ELB instances", 54 | RunE: runELBPsCmd, 55 | } 56 | 57 | return cmd 58 | } 59 | 60 | func runELBPsCmd(_ *cobra.Command, args []string) error { 61 | client, err := newClient() 62 | if err != nil { 63 | return errors.Wrap(err, "newClient failed:") 64 | } 65 | 66 | if len(args) == 0 { 67 | return errors.New("ELB_NAME is required") 68 | } 69 | 70 | options := myaws.ELBPsOptions{ 71 | LoadBalancerName: args[0], 72 | } 73 | 74 | return client.ELBPs(options) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/elbv2.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/minamijoyo/myaws/myaws" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | RootCmd.AddCommand(newELBV2Cmd()) 11 | } 12 | 13 | func newELBV2Cmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "elbv2", 16 | Short: "Manage ELBV2 resources", 17 | Run: func(cmd *cobra.Command, _ []string) { 18 | cmd.Help() // nolint: errcheck 19 | }, 20 | } 21 | 22 | cmd.AddCommand( 23 | newELBV2LsCmd(), 24 | newELBV2PsCmd(), 25 | ) 26 | 27 | return cmd 28 | } 29 | 30 | func newELBV2LsCmd() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "ls", 33 | Short: "List ELBV2 instances", 34 | RunE: runELBV2LsCmd, 35 | } 36 | 37 | return cmd 38 | } 39 | 40 | func runELBV2LsCmd(_ *cobra.Command, _ []string) error { 41 | client, err := newClient() 42 | if err != nil { 43 | return errors.Wrap(err, "newClient failed:") 44 | } 45 | 46 | return client.ELBV2Ls() 47 | } 48 | 49 | func newELBV2PsCmd() *cobra.Command { 50 | cmd := &cobra.Command{ 51 | Use: "ps TARGET_GROUP_NAME", 52 | Short: "Show ELBV2 target group health", 53 | RunE: runELBV2PsCmd, 54 | } 55 | 56 | return cmd 57 | } 58 | 59 | func runELBV2PsCmd(_ *cobra.Command, args []string) error { 60 | client, err := newClient() 61 | if err != nil { 62 | return errors.Wrap(err, "newClient failed:") 63 | } 64 | 65 | if len(args) == 0 { 66 | return errors.New("TARGET_GROUP_NAME is required") 67 | } 68 | 69 | options := myaws.ELBV2PsOptions{ 70 | TargetGroupName: args[0], 71 | } 72 | 73 | return client.ELBV2Ps(options) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/iam.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/minamijoyo/myaws/myaws" 8 | ) 9 | 10 | func init() { 11 | RootCmd.AddCommand(newIAMCmd()) 12 | } 13 | 14 | func newIAMCmd() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "iam", 17 | Short: "Manage IAM resources", 18 | Run: func(cmd *cobra.Command, _ []string) { 19 | cmd.Help() // nolint: errcheck 20 | }, 21 | } 22 | 23 | cmd.AddCommand( 24 | newIAMUserCmd(), 25 | ) 26 | 27 | return cmd 28 | } 29 | 30 | func newIAMUserCmd() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "user", 33 | Short: "Manage IAM user resources", 34 | Run: func(cmd *cobra.Command, _ []string) { 35 | cmd.Help() // nolint: errcheck 36 | }, 37 | } 38 | 39 | cmd.AddCommand( 40 | newIAMUserLsCmd(), 41 | newIAMUserResetPasswordCmd(), 42 | ) 43 | 44 | return cmd 45 | } 46 | 47 | func newIAMUserLsCmd() *cobra.Command { 48 | cmd := &cobra.Command{ 49 | Use: "ls", 50 | Short: "List IAM users", 51 | RunE: runIAMUserLsCmd, 52 | } 53 | 54 | return cmd 55 | } 56 | 57 | func runIAMUserLsCmd(_ *cobra.Command, _ []string) error { 58 | client, err := newClient() 59 | if err != nil { 60 | return errors.Wrap(err, "newClient failed:") 61 | } 62 | 63 | return client.IAMUserLs() 64 | } 65 | 66 | func newIAMUserResetPasswordCmd() *cobra.Command { 67 | cmd := &cobra.Command{ 68 | Use: "reset-password USERNAME", 69 | Short: "Reset login password for IAM user", 70 | RunE: runIAMUserResetPasswordCmd, 71 | } 72 | 73 | return cmd 74 | } 75 | 76 | func runIAMUserResetPasswordCmd(_ *cobra.Command, args []string) error { 77 | client, err := newClient() 78 | if err != nil { 79 | return errors.Wrap(err, "newClient failed:") 80 | } 81 | 82 | if len(args) == 0 { 83 | return errors.New("USERNAME is required") 84 | } 85 | 86 | options := myaws.IAMUserResetPasswordOptions{ 87 | UserName: args[0], 88 | } 89 | return client.IAMUserResetPassword(options) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/rds.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/viper" 7 | 8 | "github.com/minamijoyo/myaws/myaws" 9 | ) 10 | 11 | func init() { 12 | RootCmd.AddCommand(newRDSCmd()) 13 | } 14 | 15 | func newRDSCmd() *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "rds", 18 | Short: "Manage RDS resources", 19 | Run: func(cmd *cobra.Command, _ []string) { 20 | cmd.Help() // nolint: errcheck 21 | }, 22 | } 23 | 24 | cmd.AddCommand( 25 | newRDSLsCmd(), 26 | ) 27 | 28 | return cmd 29 | } 30 | 31 | func newRDSLsCmd() *cobra.Command { 32 | cmd := &cobra.Command{ 33 | Use: "ls", 34 | Short: "List RDS instances", 35 | RunE: runRDSLsCmd, 36 | } 37 | 38 | flags := cmd.Flags() 39 | flags.BoolP("quiet", "q", false, "Only display DBInstanceIdentifier") 40 | flags.StringP("fields", "F", "DBInstanceClass Engine AllocatedStorage StorageTypeIops InstanceCreateTime DBInstanceIdentifier ReadReplicaSource", "Output fields list separated by space") 41 | 42 | viper.BindPFlag("rds.ls.quiet", flags.Lookup("quiet")) // nolint: errcheck 43 | viper.BindPFlag("rds.ls.fields", flags.Lookup("fields")) // nolint: errcheck 44 | 45 | return cmd 46 | } 47 | 48 | func runRDSLsCmd(_ *cobra.Command, _ []string) error { 49 | client, err := newClient() 50 | if err != nil { 51 | return errors.Wrap(err, "newClient failed:") 52 | } 53 | 54 | options := myaws.RDSLsOptions{ 55 | Quiet: viper.GetBool("rds.ls.quiet"), 56 | Fields: viper.GetStringSlice("rds.ls.fields"), 57 | } 58 | 59 | return client.RDSLs(options) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | 9 | "github.com/minamijoyo/myaws/myaws" 10 | ) 11 | 12 | var cfgFile string 13 | 14 | // RootCmd is a top level command instance 15 | var RootCmd = &cobra.Command{ 16 | Use: "myaws", 17 | Short: "A human friendly AWS CLI written in Go.", 18 | SilenceErrors: true, 19 | SilenceUsage: true, 20 | } 21 | 22 | func init() { 23 | cobra.OnInitialize(initConfig) 24 | 25 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default $HOME/.myaws.yml)") 26 | RootCmd.PersistentFlags().StringP("profile", "", "", "AWS profile (default none and used AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY environment variables.)") 27 | RootCmd.PersistentFlags().StringP("region", "", "", "AWS region (default none and used AWS_DEFAULT_REGION environment variable.") 28 | RootCmd.PersistentFlags().StringP("timezone", "", "Local", "Time zone, such as UTC, Asia/Tokyo") 29 | RootCmd.PersistentFlags().BoolP("humanize", "", true, "Use Human friendly format for time") 30 | RootCmd.PersistentFlags().BoolP("debug", "", false, "Enable debug mode") 31 | 32 | viper.BindPFlag("profile", RootCmd.PersistentFlags().Lookup("profile")) // nolint: errcheck 33 | viper.BindPFlag("region", RootCmd.PersistentFlags().Lookup("region")) // nolint: errcheck 34 | viper.BindPFlag("timezone", RootCmd.PersistentFlags().Lookup("timezone")) // nolint: errcheck 35 | viper.BindPFlag("humanize", RootCmd.PersistentFlags().Lookup("humanize")) // nolint: errcheck 36 | viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")) // nolint: errcheck 37 | 38 | } 39 | 40 | func initConfig() { 41 | if cfgFile != "" { 42 | viper.SetConfigFile(cfgFile) 43 | } 44 | 45 | viper.SetConfigName(".myaws") 46 | viper.AddConfigPath("$HOME") 47 | viper.AutomaticEnv() 48 | 49 | viper.ReadInConfig() // nolint: errcheck 50 | } 51 | 52 | func newClient() (*myaws.Client, error) { 53 | return myaws.NewClient( 54 | os.Stdin, 55 | os.Stdout, 56 | os.Stderr, 57 | viper.GetString("profile"), 58 | viper.GetString("region"), 59 | viper.GetString("timezone"), 60 | viper.GetBool("humanize"), 61 | viper.GetBool("debug"), 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/ssm.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | 9 | "github.com/minamijoyo/myaws/myaws" 10 | ) 11 | 12 | func init() { 13 | RootCmd.AddCommand(newSSMCmd()) 14 | } 15 | 16 | func newSSMCmd() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "ssm", 19 | Short: "Manage SSM resources", 20 | Run: func(cmd *cobra.Command, _ []string) { 21 | cmd.Help() // nolint: errcheck 22 | }, 23 | } 24 | 25 | cmd.AddCommand( 26 | newSSMParameterCmd(), 27 | ) 28 | 29 | return cmd 30 | } 31 | 32 | func newSSMParameterCmd() *cobra.Command { 33 | cmd := &cobra.Command{ 34 | Use: "parameter", 35 | Short: "Manage SSM parameter resources", 36 | Run: func(cmd *cobra.Command, _ []string) { 37 | cmd.Help() // nolint: errcheck 38 | }, 39 | } 40 | 41 | cmd.AddCommand( 42 | newSSMParameterPutCmd(), 43 | newSSMParameterGetCmd(), 44 | newSSMParameterLsCmd(), 45 | newSSMParameterEnvCmd(), 46 | newSSMParameterDelCmd(), 47 | ) 48 | 49 | return cmd 50 | } 51 | 52 | func newSSMParameterPutCmd() *cobra.Command { 53 | cmd := &cobra.Command{ 54 | Use: "put NAME VALUE", 55 | Short: "Put SSM parameter", 56 | RunE: runSSMParameterPutCmd, 57 | } 58 | 59 | flags := cmd.Flags() 60 | flags.StringP("key-id", "k", "", "KMS key ID or alias") 61 | 62 | viper.BindPFlag("ssm.parameter.put.key-id", flags.Lookup("key-id")) // nolint: errcheck 63 | 64 | return cmd 65 | } 66 | 67 | func runSSMParameterPutCmd(_ *cobra.Command, args []string) error { 68 | client, err := newClient() 69 | if err != nil { 70 | return errors.Wrap(err, "newClient failed:") 71 | } 72 | 73 | if len(args) != 2 { 74 | return errors.New("NAME and VALUE are required") 75 | } 76 | 77 | options := myaws.SSMParameterPutOptions{ 78 | Name: args[0], 79 | Value: args[1], 80 | KeyID: viper.GetString("ssm.parameter.put.key-id"), 81 | } 82 | 83 | return client.SSMParameterPut(options) 84 | } 85 | 86 | func newSSMParameterGetCmd() *cobra.Command { 87 | cmd := &cobra.Command{ 88 | Use: "get NAME [...]", 89 | Short: "Get SSM parameter", 90 | RunE: runSSMParameterGetCmd, 91 | } 92 | 93 | flags := cmd.Flags() 94 | flags.BoolP("with-decryption", "d", true, "with KMS decryption") 95 | 96 | viper.BindPFlag("ssm.parameter.get.with-decryption", flags.Lookup("with-decryption")) // nolint: errcheck 97 | return cmd 98 | } 99 | 100 | func runSSMParameterGetCmd(_ *cobra.Command, args []string) error { 101 | client, err := newClient() 102 | if err != nil { 103 | return errors.Wrap(err, "newClient failed:") 104 | } 105 | 106 | if len(args) == 0 { 107 | return errors.New("NAME is required") 108 | } 109 | 110 | names := aws.StringSlice(args) 111 | options := myaws.SSMParameterGetOptions{ 112 | Names: names, 113 | WithDecryption: viper.GetBool("ssm.parameter.get.with-decryption"), 114 | } 115 | 116 | return client.SSMParameterGet(options) 117 | } 118 | 119 | func newSSMParameterLsCmd() *cobra.Command { 120 | cmd := &cobra.Command{ 121 | Use: "ls", 122 | Short: "List SSM parameters", 123 | RunE: runSSMParameterLsCmd, 124 | } 125 | 126 | flags := cmd.Flags() 127 | flags.StringP("name", "n", "", 128 | "Filter parameters by Name, such as foo.dev. The value of tag is assumed to be a prefix match", 129 | ) 130 | 131 | viper.BindPFlag("ssm.parameter.ls.name", flags.Lookup("name")) // nolint: errcheck 132 | return cmd 133 | } 134 | 135 | func runSSMParameterLsCmd(_ *cobra.Command, _ []string) error { 136 | client, err := newClient() 137 | if err != nil { 138 | return errors.Wrap(err, "newClient failed:") 139 | } 140 | 141 | options := myaws.SSMParameterLsOptions{ 142 | Name: viper.GetString("ssm.parameter.ls.name"), 143 | } 144 | 145 | return client.SSMParameterLs(options) 146 | } 147 | 148 | func newSSMParameterEnvCmd() *cobra.Command { 149 | cmd := &cobra.Command{ 150 | Use: "env NAME", 151 | Short: "Print SSM parameters as a list of environment variables", 152 | RunE: runSSMParameterEnvCmd, 153 | } 154 | 155 | flags := cmd.Flags() 156 | flags.BoolP("docker-format", "e", false, "Output in docker environment variables format such as -e KEY=VALUE") 157 | flags.BoolP("quote", "q", false, "Wrap each value in single quote") 158 | 159 | viper.BindPFlag("ssm.parameter.env.docker-format", flags.Lookup("docker-format")) // nolint: errcheck 160 | viper.BindPFlag("ssm.parameter.env.quote", flags.Lookup("quote")) // nolint: errcheck 161 | return cmd 162 | } 163 | 164 | func runSSMParameterEnvCmd(_ *cobra.Command, args []string) error { 165 | client, err := newClient() 166 | if err != nil { 167 | return errors.Wrap(err, "newClient failed:") 168 | } 169 | 170 | if len(args) == 0 { 171 | return errors.New("NAME is required") 172 | } 173 | 174 | options := myaws.SSMParameterEnvOptions{ 175 | Name: args[0], 176 | DockerFormat: viper.GetBool("ssm.parameter.env.docker-format"), 177 | QuoteValue: viper.GetBool("ssm.parameter.env.quote"), 178 | } 179 | 180 | return client.SSMParameterEnv(options) 181 | } 182 | 183 | func newSSMParameterDelCmd() *cobra.Command { 184 | cmd := &cobra.Command{ 185 | Use: "del NAME", 186 | Short: "Delete SSM parameter", 187 | RunE: runSSMParameterDelCmd, 188 | } 189 | 190 | return cmd 191 | } 192 | 193 | func runSSMParameterDelCmd(_ *cobra.Command, args []string) error { 194 | client, err := newClient() 195 | if err != nil { 196 | return errors.Wrap(err, "newClient failed:") 197 | } 198 | 199 | if len(args) == 0 { 200 | return errors.New("NAME is required") 201 | } 202 | 203 | options := myaws.SSMParameterDelOptions{ 204 | Name: args[0], 205 | } 206 | 207 | return client.SSMParameterDel(options) 208 | } 209 | -------------------------------------------------------------------------------- /cmd/sts.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func init() { 9 | RootCmd.AddCommand(newSTSCmd()) 10 | } 11 | 12 | func newSTSCmd() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "sts", 15 | Short: "Manage STS resources", 16 | Run: func(cmd *cobra.Command, _ []string) { 17 | cmd.Help() // nolint: errcheck 18 | }, 19 | } 20 | 21 | cmd.AddCommand( 22 | newSTSIDCmd(), 23 | ) 24 | 25 | return cmd 26 | } 27 | 28 | func newSTSIDCmd() *cobra.Command { 29 | cmd := &cobra.Command{ 30 | Use: "id", 31 | Short: "Get caller identity", 32 | RunE: runSTSIDCmd, 33 | } 34 | 35 | return cmd 36 | } 37 | 38 | func runSTSIDCmd(_ *cobra.Command, _ []string) error { 39 | client, err := newClient() 40 | if err != nil { 41 | return errors.Wrap(err, "newClient failed:") 42 | } 43 | 44 | return client.STSID() 45 | } 46 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // version is a version number. 10 | var version = "0.4.9" 11 | 12 | func init() { 13 | RootCmd.AddCommand(newVersionCmd()) 14 | } 15 | 16 | func newVersionCmd() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "version", 19 | Short: "Print version", 20 | Run: func(_ *cobra.Command, _ []string) { 21 | fmt.Printf("%s\n", version) 22 | }, 23 | } 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/minamijoyo/myaws 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.38.39 7 | github.com/dustin/go-humanize v1.0.0 8 | github.com/pkg/errors v0.9.1 9 | github.com/spf13/cobra v0.0.5 10 | github.com/spf13/viper v1.3.2 11 | github.com/thoas/go-funk v0.0.0-20181020164546-fbae87fb5b5c 12 | golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 13 | ) 14 | 15 | require ( 16 | github.com/fsnotify/fsnotify v1.4.7 // indirect 17 | github.com/hashicorp/hcl v1.0.0 // indirect 18 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 19 | github.com/jmespath/go-jmespath v0.4.0 // indirect 20 | github.com/magiconair/properties v1.8.0 // indirect 21 | github.com/mitchellh/mapstructure v1.1.2 // indirect 22 | github.com/pelletier/go-toml v1.2.0 // indirect 23 | github.com/spf13/afero v1.1.2 // indirect 24 | github.com/spf13/cast v1.3.0 // indirect 25 | github.com/spf13/jwalterweatherman v1.0.0 // indirect 26 | github.com/spf13/pflag v1.0.3 // indirect 27 | golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 // indirect 28 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect 29 | golang.org/x/text v0.3.7 // indirect 30 | gopkg.in/yaml.v2 v2.2.8 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 4 | github.com/aws/aws-sdk-go v1.38.39 h1:n4jkKlE3DfZBN800njuHmOEQlDht4aO/kE2VNk0/6T4= 5 | github.com/aws/aws-sdk-go v1.38.39/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 6 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 7 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 8 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 9 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 14 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 15 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 16 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 17 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 18 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 19 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 20 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 21 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 22 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 23 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 24 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 25 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 26 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 27 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 28 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 29 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 30 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 31 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 32 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 33 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 37 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 38 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 39 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 40 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 41 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 42 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 43 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 44 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 45 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 46 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 47 | github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= 48 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 51 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 52 | github.com/thoas/go-funk v0.0.0-20181020164546-fbae87fb5b5c h1:3sFKuGerP3mGyXo7gDR1dGQ6GdIrI8s5KWmct0R5J6A= 53 | github.com/thoas/go-funk v0.0.0-20181020164546-fbae87fb5b5c/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA= 54 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 55 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 56 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 57 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 58 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 59 | golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= 60 | golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 61 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 62 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 63 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 h1:TyKJRhyo17yWxOMCTHKWrc5rddHORMlnZ/j57umaUd8= 69 | golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 72 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 73 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 74 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 75 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 80 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 81 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 82 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/viper" 8 | 9 | "github.com/minamijoyo/myaws/cmd" 10 | ) 11 | 12 | func main() { 13 | if err := cmd.RootCmd.Execute(); err != nil { 14 | if viper.GetBool("debug") { 15 | fmt.Fprintf(os.Stderr, "%+v\n", err) 16 | } else { 17 | fmt.Fprintf(os.Stderr, "%v\n", err) 18 | } 19 | os.Exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /myaws/autoscaling.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/autoscaling" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // getAutoScalingGroupDesiredCapacity is a helper function which returns 9 | // DesiredCapacity of the specific AutoScalingGroup. 10 | func (client *Client) getAutoScalingGroupDesiredCapacity(asgName string) (int64, error) { 11 | input := &autoscaling.DescribeAutoScalingGroupsInput{ 12 | AutoScalingGroupNames: []*string{&asgName}, 13 | } 14 | 15 | response, err := client.AutoScaling.DescribeAutoScalingGroups(input) 16 | if err != nil { 17 | return 0, errors.Wrap(err, "getAutoScalingGroupDesiredCapacity failed:") 18 | } 19 | 20 | desiredCapacity := response.AutoScalingGroups[0].DesiredCapacity 21 | 22 | return *desiredCapacity, nil 23 | } 24 | -------------------------------------------------------------------------------- /myaws/autoscaling_attach.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/autoscaling" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // AutoscalingAttachOptions customize the behavior of the Attach command. 11 | type AutoscalingAttachOptions struct { 12 | AsgName string 13 | InstanceIds []*string 14 | LoadBalancerNames []*string 15 | Wait bool 16 | } 17 | 18 | // AutoscalingAttach attaches instances or load balancers from autoscaling group. 19 | func (client *Client) AutoscalingAttach(options AutoscalingAttachOptions) error { 20 | if len(options.InstanceIds) > 0 { 21 | if err := client.autoscalingAttachInstances(options.AsgName, options.InstanceIds); err != nil { 22 | return err 23 | } 24 | } 25 | 26 | if len(options.LoadBalancerNames) > 0 { 27 | if err := client.autoscalingAttachLoadBalancers(options.AsgName, options.LoadBalancerNames); err != nil { 28 | return err 29 | } 30 | } 31 | 32 | if options.Wait { 33 | fmt.Fprintln(client.stdout, "Wait until desired capacity instances are InService...") 34 | return client.WaitUntilAutoScalingGroupStable(options.AsgName) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (client *Client) autoscalingAttachInstances(asgName string, instanceIds []*string) error { 41 | params := &autoscaling.AttachInstancesInput{ 42 | AutoScalingGroupName: &asgName, 43 | InstanceIds: instanceIds, 44 | } 45 | 46 | if _, err := client.AutoScaling.AttachInstances(params); err != nil { 47 | return errors.Wrap(err, "AttachInstances failed:") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (client *Client) autoscalingAttachLoadBalancers(asgName string, loadBalancerNames []*string) error { 54 | params := &autoscaling.AttachLoadBalancersInput{ 55 | AutoScalingGroupName: &asgName, 56 | LoadBalancerNames: loadBalancerNames, 57 | } 58 | 59 | if _, err := client.AutoScaling.AttachLoadBalancers(params); err != nil { 60 | return errors.Wrap(err, "AttachLoadBalancers failed:") 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /myaws/autoscaling_detach.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/autoscaling" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // AutoscalingDetachOptions customize the behavior of the Detach command. 11 | type AutoscalingDetachOptions struct { 12 | AsgName string 13 | InstanceIds []*string 14 | LoadBalancerNames []*string 15 | Wait bool 16 | } 17 | 18 | // AutoscalingDetach detaches instances or load balancers from autoscaling group. 19 | func (client *Client) AutoscalingDetach(options AutoscalingDetachOptions) error { 20 | if len(options.InstanceIds) > 0 { 21 | if err := client.autoscalingDetachInstances(options.AsgName, options.InstanceIds); err != nil { 22 | return err 23 | } 24 | } 25 | 26 | if len(options.LoadBalancerNames) > 0 { 27 | if err := client.autoscalingDetachLoadBalancers(options.AsgName, options.LoadBalancerNames); err != nil { 28 | return err 29 | } 30 | } 31 | 32 | if options.Wait { 33 | fmt.Fprintln(client.stdout, "Wait until desired capacity instances are InService...") 34 | return client.WaitUntilAutoScalingGroupStable(options.AsgName) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (client *Client) autoscalingDetachInstances(asgName string, instanceIds []*string) error { 41 | decrementCapacity := true 42 | params := &autoscaling.DetachInstancesInput{ 43 | AutoScalingGroupName: &asgName, 44 | InstanceIds: instanceIds, 45 | ShouldDecrementDesiredCapacity: &decrementCapacity, 46 | } 47 | 48 | if _, err := client.AutoScaling.DetachInstances(params); err != nil { 49 | return errors.Wrap(err, "DetachInstances failed:") 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (client *Client) autoscalingDetachLoadBalancers(asgName string, loadBalancerNames []*string) error { 56 | params := &autoscaling.DetachLoadBalancersInput{ 57 | AutoScalingGroupName: &asgName, 58 | LoadBalancerNames: loadBalancerNames, 59 | } 60 | 61 | if _, err := client.AutoScaling.DetachLoadBalancers(params); err != nil { 62 | return errors.Wrap(err, "DetachLoadBalancers failed:") 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /myaws/autoscaling_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/autoscaling" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // AutoscalingLsOptions customize the behavior of the Ls command. 14 | type AutoscalingLsOptions struct { 15 | All bool 16 | } 17 | 18 | // AutoscalingLs describes autoscaling groups. 19 | func (client *Client) AutoscalingLs(options AutoscalingLsOptions) error { 20 | params := &autoscaling.DescribeAutoScalingGroupsInput{} 21 | 22 | response, err := client.AutoScaling.DescribeAutoScalingGroups(params) 23 | if err != nil { 24 | return errors.Wrap(err, "DescribeAutoScalingGroups failed:") 25 | } 26 | 27 | for _, asg := range response.AutoScalingGroups { 28 | if options.All || len(asg.Instances) > 0 { 29 | fmt.Fprintln(client.stdout, formatAutoscalingGroup(asg)) 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func formatAutoscalingGroup(asg *autoscaling.Group) string { 37 | output := []string{ 38 | formatAutoscalingInstacesLen(asg.Instances), 39 | *asg.AutoScalingGroupName, 40 | formatAutoscalingInstanceIds(asg.Instances), 41 | formatAutoscalingLoadBalancerNames(asg.LoadBalancerNames), 42 | } 43 | 44 | return strings.Join(output[:], "\t") 45 | } 46 | 47 | func formatAutoscalingInstacesLen(instances []*autoscaling.Instance) string { 48 | if instances == nil { 49 | return "0" 50 | } 51 | return strconv.Itoa(len(instances)) 52 | } 53 | 54 | func formatAutoscalingInstanceIds(instances []*autoscaling.Instance) string { 55 | if instances == nil { 56 | return "" 57 | } 58 | instanceIds := lookupAutoscalingInstanceIds(instances) 59 | return strings.Join(instanceIds[:], " ") 60 | } 61 | 62 | func lookupAutoscalingInstanceIds(instances []*autoscaling.Instance) []string { 63 | var instanceIds []string 64 | for _, instance := range instances { 65 | instanceIds = append(instanceIds, *instance.InstanceId) 66 | } 67 | return instanceIds 68 | } 69 | 70 | func formatAutoscalingLoadBalancerNames(lbNames []*string) string { 71 | if lbNames == nil { 72 | return "" 73 | } 74 | return strings.Join(aws.StringValueSlice(lbNames)[:], " ") 75 | } 76 | -------------------------------------------------------------------------------- /myaws/autoscaling_set_instance_protection.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/autoscaling" 5 | "github.com/pkg/errors" 6 | funk "github.com/thoas/go-funk" 7 | ) 8 | 9 | // AutoScalingSetInstanceProtectionOptions customizes the behavior of the Attach command. 10 | type AutoScalingSetInstanceProtectionOptions struct { 11 | AsgName string 12 | InstanceIds []*string 13 | ProtectedFromScaleIn bool 14 | } 15 | 16 | // AutoScalingSetInstanceProtection protects from termination when scale in your autoscaling group. 17 | func (client *Client) AutoScalingSetInstanceProtection(options AutoScalingSetInstanceProtectionOptions) error { 18 | // the number of maximum InstanceIds is limited to 19. 19 | // https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_SetInstanceProtection.html 20 | maxInstanceIDCount := 19 21 | chunks := (funk.Chunk(options.InstanceIds, maxInstanceIDCount)).([][]*string) 22 | for _, c := range chunks { 23 | if err := client.autoScalingSetInstanceProtectionInstances(options.AsgName, c, options.ProtectedFromScaleIn); err != nil { 24 | return err 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | func (client *Client) autoScalingSetInstanceProtectionInstances(asgName string, instanceIds []*string, protectedFromScaleIn bool) error { 31 | params := &autoscaling.SetInstanceProtectionInput{ 32 | AutoScalingGroupName: &asgName, 33 | InstanceIds: instanceIds, 34 | ProtectedFromScaleIn: &protectedFromScaleIn, 35 | } 36 | 37 | if _, err := client.AutoScaling.SetInstanceProtection(params); err != nil { 38 | return errors.Wrap(err, "SetInstanceProtection failed:") 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /myaws/autoscaling_update.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/autoscaling" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // AutoscalingUpdateOptions customize the behavior of the Update command. 11 | type AutoscalingUpdateOptions struct { 12 | AsgName string 13 | DesiredCapacity int64 14 | Wait bool 15 | } 16 | 17 | // AutoscalingUpdate updates autoscaling group setting. 18 | // Available param is currently desired-capacity only. 19 | func (client *Client) AutoscalingUpdate(options AutoscalingUpdateOptions) error { 20 | params := &autoscaling.SetDesiredCapacityInput{ 21 | AutoScalingGroupName: &options.AsgName, 22 | DesiredCapacity: &options.DesiredCapacity, 23 | } 24 | 25 | if _, err := client.AutoScaling.SetDesiredCapacity(params); err != nil { 26 | return errors.Wrap(err, "SetDesiredCapacity failed:") 27 | } 28 | 29 | if options.Wait { 30 | fmt.Fprintln(client.stdout, "Wait until desired capacity instances are InService...") 31 | return client.WaitUntilAutoScalingGroupStable(options.AsgName) 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /myaws/autoscaling_waiter.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/request" 9 | "github.com/aws/aws-sdk-go/service/autoscaling" 10 | ) 11 | 12 | // WaitUntilAutoScalingGroupStable is a helper function which waits until 13 | // the AutoScaling Group converges to the desired state. We only check the 14 | // status of AutoScaling Group. If the ASG has an ELB, the health check status 15 | // of ELB can link with the health status of ASG, so we don't check the status 16 | // of ELB here. 17 | // Due to the current limitation of the implementation of `request.Waiter`, 18 | // we need to wait it in two steps. 19 | // 1. Wait until the number of instances equals `DesiredCapacity`. 20 | // 2. Wait until all instances are InService. 21 | func (client *Client) WaitUntilAutoScalingGroupStable(asgName string) error { 22 | desiredCapacity, err := client.getAutoScalingGroupDesiredCapacity(asgName) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | ctx := aws.BackgroundContext() 28 | input := &autoscaling.DescribeAutoScalingGroupsInput{ 29 | AutoScalingGroupNames: []*string{&asgName}, 30 | } 31 | 32 | // make sure instances are created or terminated. 33 | err = client.waitUntilAutoScalingGroupNumberOfInstancesEqualsDesiredCapacityWithContext( 34 | ctx, 35 | desiredCapacity, 36 | input, 37 | ) 38 | 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // if the desired state is no instance, we just return here. 44 | if desiredCapacity == 0 { 45 | return nil 46 | } 47 | 48 | // check all instances are InService state. 49 | return client.waitUntilAutoScalingGroupAllInstancesAreInServiceWithContext(ctx, input) 50 | } 51 | 52 | // waitUntilAutoScalingGroupNumberOfInstancesEqualsDesiredCapacityWithContext 53 | // waits the number of instances equals DesiredCapacity. 54 | func (client *Client) waitUntilAutoScalingGroupNumberOfInstancesEqualsDesiredCapacityWithContext(ctx aws.Context, desiredCapacity int64, input *autoscaling.DescribeAutoScalingGroupsInput, opts ...request.WaiterOption) error { 55 | // We implicitly assume that the number of AutoScalingGroup is only one to 56 | // simplify checking desiredCapacity. In our case, multiple AutoScalingGroup 57 | // doesn't pass this function. 58 | // Properties in the response returned by aws-sdk-go are reference types and 59 | // not primitive. Thus we cannot be directly compared on JMESPath. 60 | matcher := fmt.Sprintf("AutoScalingGroups[].[length(Instances) == `%d`][]", desiredCapacity) 61 | 62 | w := request.Waiter{ 63 | Name: "WaitUntilAutoScalingGroupNumberOfInstancesEqualsDesiredCapacity", 64 | MaxAttempts: 20, 65 | Delay: request.ConstantWaiterDelay(15 * time.Second), 66 | Acceptors: []request.WaiterAcceptor{ 67 | { 68 | State: request.SuccessWaiterState, 69 | Matcher: request.PathAllWaiterMatch, Argument: matcher, 70 | Expected: true, 71 | }, 72 | }, 73 | Logger: client.config.Logger, 74 | NewRequest: func(opts []request.Option) (*request.Request, error) { 75 | var inCpy *autoscaling.DescribeAutoScalingGroupsInput 76 | if input != nil { 77 | tmp := *input 78 | inCpy = &tmp 79 | } 80 | req, _ := client.AutoScaling.DescribeAutoScalingGroupsRequest(inCpy) 81 | req.SetContext(ctx) 82 | req.ApplyOptions(opts...) 83 | return req, nil 84 | }, 85 | } 86 | w.ApplyOptions(opts...) 87 | 88 | return w.WaitWithContext(ctx) 89 | } 90 | 91 | // waitUntilAutoScalingGroupAllInstancesAreInService waits until all instances 92 | // are in service. Since the official `WaitUntilGroupInServiceWithContext` in 93 | // aws-sdk-go checks as follow: 94 | // contains(AutoScalingGroups[].[length(Instances[?LifecycleState=='InService']) >= MinSize][], `false`) 95 | // But we found this doesn't work as expected. Properties in the response 96 | // returned by aws-sdk-go are reference type and not primitive. Thus we can not 97 | // be directly compared on JMESPath. So we implement a customized waiter here. 98 | // When the number of desired instances increase or decrease, the affected 99 | // instances are in states other than InService until the operation completes. 100 | // So we should check that all the states of instances are InService. 101 | func (client *Client) waitUntilAutoScalingGroupAllInstancesAreInServiceWithContext(ctx aws.Context, input *autoscaling.DescribeAutoScalingGroupsInput, opts ...request.WaiterOption) error { 102 | w := request.Waiter{ 103 | Name: "WaitUntilAutoScalingGroupAllInstancesAreInService", 104 | MaxAttempts: 20, 105 | Delay: request.ConstantWaiterDelay(15 * time.Second), 106 | Acceptors: []request.WaiterAcceptor{ 107 | { 108 | State: request.SuccessWaiterState, 109 | Matcher: request.PathAllWaiterMatch, Argument: "AutoScalingGroups[].Instances[].LifecycleState", 110 | Expected: "InService", 111 | }, 112 | }, 113 | Logger: client.config.Logger, 114 | NewRequest: func(opts []request.Option) (*request.Request, error) { 115 | var inCpy *autoscaling.DescribeAutoScalingGroupsInput 116 | if input != nil { 117 | tmp := *input 118 | inCpy = &tmp 119 | } 120 | req, _ := client.AutoScaling.DescribeAutoScalingGroupsRequest(inCpy) 121 | req.SetContext(ctx) 122 | req.ApplyOptions(opts...) 123 | return req, nil 124 | }, 125 | } 126 | w.ApplyOptions(opts...) 127 | 128 | return w.WaitWithContext(ctx) 129 | } 130 | -------------------------------------------------------------------------------- /myaws/client.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/autoscaling" 12 | "github.com/aws/aws-sdk-go/service/ec2" 13 | "github.com/aws/aws-sdk-go/service/ecr" 14 | "github.com/aws/aws-sdk-go/service/ecs" 15 | "github.com/aws/aws-sdk-go/service/elb" 16 | "github.com/aws/aws-sdk-go/service/elbv2" 17 | "github.com/aws/aws-sdk-go/service/iam" 18 | "github.com/aws/aws-sdk-go/service/rds" 19 | "github.com/aws/aws-sdk-go/service/ssm" 20 | "github.com/aws/aws-sdk-go/service/sts" 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | // Client represents myaws CLI 25 | type Client struct { 26 | config *aws.Config 27 | stdin io.Reader 28 | stdout io.Writer 29 | stderr io.Writer 30 | profile string 31 | region string 32 | timezone string 33 | humanize bool 34 | debug bool 35 | AutoScaling *autoscaling.AutoScaling 36 | EC2 *ec2.EC2 37 | ECS *ecs.ECS 38 | ECR *ecr.ECR 39 | ELB *elb.ELB 40 | ELBV2 *elbv2.ELBV2 41 | IAM *iam.IAM 42 | RDS *rds.RDS 43 | SSM *ssm.SSM 44 | STS *sts.STS 45 | } 46 | 47 | // NewClient initializes Client instance 48 | func NewClient(stdin io.Reader, stdout io.Writer, stderr io.Writer, profile string, region string, timezone string, humanize bool, debug bool) (*Client, error) { 49 | session := session.New() // nolint: staticcheck 50 | config := newConfig(profile, region, debug) 51 | client := &Client{ 52 | config: config, 53 | stdin: stdin, 54 | stdout: stdout, 55 | stderr: stderr, 56 | profile: profile, 57 | region: region, 58 | timezone: timezone, 59 | humanize: humanize, 60 | AutoScaling: autoscaling.New(session, config), 61 | EC2: ec2.New(session, config), 62 | ECS: ecs.New(session, config), 63 | ECR: ecr.New(session, config), 64 | ELB: elb.New(session, config), 65 | ELBV2: elbv2.New(session, config), 66 | IAM: iam.New(session, config), 67 | RDS: rds.New(session, config), 68 | SSM: ssm.New(session, config), 69 | STS: sts.New(session, config), 70 | } 71 | return client, nil 72 | } 73 | 74 | // Confirmation asks user for confirmation. 75 | // "y" and "Y" returns true and others are false. 76 | func (client *Client) Confirmation(message string) (bool, error) { 77 | fmt.Fprintf(client.stdout, "%s [y/n]: ", message) 78 | 79 | reader := bufio.NewReader(client.stdin) 80 | input, err := reader.ReadString('\n') 81 | if err != nil { 82 | return false, errors.Wrap(err, "ReadString failed:") 83 | } 84 | 85 | normalized := strings.ToLower(strings.TrimSpace(input)) 86 | return normalized == "y", nil 87 | } 88 | -------------------------------------------------------------------------------- /myaws/config.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/credentials" 8 | "github.com/aws/aws-sdk-go/aws/defaults" 9 | ) 10 | 11 | // newConfig creates *aws.config from profile and region options. 12 | // AWS credentials are checked in the order of 13 | // profile, environment variables, IAM Task Role (ECS), IAM Role. 14 | // Unlike the aws default, load profile before environment variables 15 | // because we want to prioritize explicit arguments over the environment. 16 | func newConfig(profile string, region string, debug bool) *aws.Config { 17 | defaultConfig := defaults.Get().Config 18 | cred := newCredentials(getenv(profile, "AWS_DEFAULT_PROFILE"), getenv(region, "AWS_DEFAULT_REGION")) 19 | 20 | logLevel := aws.LogLevel(aws.LogOff) 21 | if debug { 22 | // enable AWS API request and response logging in debug mode 23 | logLevel = aws.LogLevel(aws.LogDebugWithHTTPBody | aws.LogDebugWithRequestRetries | aws.LogDebugWithRequestErrors) 24 | } 25 | 26 | config := defaultConfig. 27 | WithCredentials(cred). 28 | WithRegion(getenv(region, "AWS_DEFAULT_REGION")). 29 | WithLogLevel(*logLevel) 30 | 31 | return config 32 | } 33 | 34 | func newCredentials(profile string, region string) *credentials.Credentials { 35 | // temporary config to resolve RemoteCredProvider 36 | tmpConfig := defaults.Get().Config.WithRegion(region) 37 | tmpHandlers := defaults.Handlers() 38 | 39 | return credentials.NewChainCredentials( 40 | []credentials.Provider{ 41 | // Read profile before environment variables 42 | &credentials.SharedCredentialsProvider{ 43 | Profile: profile, 44 | }, 45 | &credentials.EnvProvider{}, 46 | // for IAM Task Role (ECS) and IAM Role 47 | defaults.RemoteCredProvider(*tmpConfig, tmpHandlers), 48 | }) 49 | } 50 | 51 | func getenv(value, key string) string { 52 | if len(value) == 0 { 53 | return os.Getenv(key) 54 | } 55 | return value 56 | } 57 | -------------------------------------------------------------------------------- /myaws/ec2.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // FindEC2Instances returns an array of instances matching the conditions. 12 | func (client *Client) FindEC2Instances(filterTag string, all bool) ([]*ec2.Instance, error) { 13 | params := &ec2.DescribeInstancesInput{ 14 | Filters: []*ec2.Filter{ 15 | buildEC2StateFilter(all), 16 | buildEC2TagFilter(filterTag), 17 | }, 18 | } 19 | 20 | response, err := client.EC2.DescribeInstances(params) 21 | if err != nil { 22 | return nil, errors.Wrap(err, "DescribeInstances failed") 23 | } 24 | 25 | var instances []*ec2.Instance 26 | for _, reservation := range response.Reservations { 27 | for _, instance := range reservation.Instances { // nolint: gosimple 28 | instances = append(instances, instance) 29 | } 30 | } 31 | 32 | return instances, nil 33 | } 34 | 35 | func buildEC2StateFilter(all bool) *ec2.Filter { 36 | var stateFilter *ec2.Filter 37 | if !all { 38 | stateFilter = &ec2.Filter{ 39 | Name: aws.String("instance-state-name"), 40 | Values: []*string{ 41 | aws.String("running"), 42 | }, 43 | } 44 | } 45 | return stateFilter 46 | } 47 | 48 | func buildEC2TagFilter(filterTag string) *ec2.Filter { 49 | var tagFilter *ec2.Filter 50 | if filterTag != "" { 51 | tagParts := strings.Split(filterTag, ":") 52 | tagFilter = &ec2.Filter{ 53 | Name: aws.String("tag:" + tagParts[0]), 54 | Values: []*string{ 55 | aws.String("*" + tagParts[1] + "*"), 56 | }, 57 | } 58 | } 59 | return tagFilter 60 | } 61 | -------------------------------------------------------------------------------- /myaws/ec2_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | ) 9 | 10 | // EC2LsOptions customize the behavior of the Ls command. 11 | type EC2LsOptions struct { 12 | All bool 13 | Quiet bool 14 | FilterTag string 15 | Fields []string 16 | } 17 | 18 | // EC2Ls describes EC2 instances. 19 | func (client *Client) EC2Ls(options EC2LsOptions) error { 20 | instances, err := client.FindEC2Instances(options.FilterTag, options.All) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | for _, instance := range instances { 26 | fmt.Fprintln(client.stdout, formatEC2Instance(client, options, instance)) 27 | } 28 | return nil 29 | } 30 | 31 | func formatEC2Instance(client *Client, options EC2LsOptions, instance *ec2.Instance) string { 32 | formatFuncs := map[string]func(client *Client, options EC2LsOptions, instance *ec2.Instance) string{ 33 | "InstanceId": formatEC2InstanceID, 34 | "InstanceType": formatEC2InstanceType, 35 | "PublicIpAddress": formatEC2PublicIPAddress, 36 | "PrivateIpAddress": formatEC2PrivateIPAddress, 37 | "AvailabilityZone": formatEC2AvailabilityZone, 38 | "StateName": formatEC2StateName, 39 | "LaunchTime": formatEC2LaunchTime, 40 | } 41 | 42 | var outputFields []string 43 | if options.Quiet { 44 | outputFields = []string{"InstanceId"} 45 | } else { 46 | outputFields = options.Fields 47 | } 48 | 49 | output := []string{} 50 | 51 | for _, field := range outputFields { 52 | value := "" 53 | if strings.Index(field, "Tag:") != -1 { // nolint: gosimple 54 | key := strings.Split(field, ":")[1] 55 | value = formatEC2Tag(instance, key) 56 | } else { 57 | value = formatFuncs[field](client, options, instance) 58 | } 59 | output = append(output, value) 60 | } 61 | return strings.Join(output[:], "\t") 62 | } 63 | 64 | func formatEC2InstanceID(_ *Client, _ EC2LsOptions, instance *ec2.Instance) string { 65 | return *instance.InstanceId 66 | } 67 | 68 | func formatEC2InstanceType(_ *Client, _ EC2LsOptions, instance *ec2.Instance) string { 69 | return fmt.Sprintf("%-11s", *instance.InstanceType) 70 | } 71 | 72 | func formatEC2PublicIPAddress(_ *Client, _ EC2LsOptions, instance *ec2.Instance) string { 73 | if instance.PublicIpAddress == nil { 74 | return "___.___.___.___" 75 | } 76 | return *instance.PublicIpAddress 77 | } 78 | 79 | func formatEC2PrivateIPAddress(_ *Client, _ EC2LsOptions, instance *ec2.Instance) string { 80 | if instance.PrivateIpAddress == nil { 81 | return "___.___.___.___" 82 | } 83 | return *instance.PrivateIpAddress 84 | } 85 | 86 | func formatEC2StateName(_ *Client, _ EC2LsOptions, instance *ec2.Instance) string { 87 | return *instance.State.Name 88 | } 89 | 90 | func formatEC2LaunchTime(client *Client, _ EC2LsOptions, instance *ec2.Instance) string { 91 | return client.FormatTime(instance.LaunchTime) 92 | } 93 | 94 | func formatEC2Tag(instance *ec2.Instance, key string) string { 95 | var value string 96 | for _, t := range instance.Tags { 97 | if *t.Key == key { 98 | value = *t.Value 99 | break 100 | } 101 | } 102 | return value 103 | } 104 | 105 | func formatEC2AvailabilityZone(_ *Client, _ EC2LsOptions, instance *ec2.Instance) string { 106 | return *instance.Placement.AvailabilityZone 107 | } 108 | -------------------------------------------------------------------------------- /myaws/ec2_ssh.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/aws/aws-sdk-go/service/ec2" 10 | "github.com/pkg/errors" 11 | "golang.org/x/crypto/ssh" 12 | "golang.org/x/crypto/ssh/terminal" // nolint: staticcheck 13 | ) 14 | 15 | // EC2SSHOptions customize the behavior of the SSH command. 16 | type EC2SSHOptions struct { 17 | FilterTag string 18 | LoginName string 19 | IdentityFile string 20 | Private bool 21 | Command string 22 | } 23 | 24 | // EC2SSH resolves IP address of EC2 instance and connects to it by SSH. 25 | func (client *Client) EC2SSH(options EC2SSHOptions) error { 26 | config, err := buildSSHConfig(options.LoginName, options.IdentityFile) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | instances, err := client.FindEC2Instances(options.FilterTag, false) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if len(instances) == 0 { 37 | return errors.Errorf("no such instance: %s", options.FilterTag) 38 | } 39 | 40 | if len(instances) >= 2 && options.Command == "" { 41 | return errors.Errorf("multiple instances found") 42 | } 43 | 44 | hostnames := []string{} 45 | for _, instance := range instances { 46 | hostname, err := client.resolveEC2IPAddress(instance, options.Private) 47 | if err != nil { 48 | return err 49 | } 50 | hostnames = append(hostnames, hostname) 51 | } 52 | 53 | // Start single ssh session with terminal 54 | if options.Command == "" { 55 | return client.startSSHSessionWithTerminal(hostnames[0], "22", config) 56 | } 57 | 58 | // Execute ssh command to multiple hosts in series 59 | for _, hostname := range hostnames { 60 | if err := client.executeSSHCommand(hostname, "22", config, options.Command); err != nil { 61 | return err 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (client *Client) resolveEC2IPAddress(instance *ec2.Instance, private bool) (string, error) { 69 | if private { 70 | return client.resolveEC2PrivateIPAddress(instance) 71 | } 72 | return client.resolveEC2PublicIPAddress(instance) 73 | } 74 | 75 | func (client *Client) resolveEC2PrivateIPAddress(instance *ec2.Instance) (string, error) { 76 | if instance.PrivateIpAddress == nil { 77 | return "", errors.Errorf("no private ip address: %s", *instance.InstanceId) 78 | } 79 | return *instance.PrivateIpAddress, nil 80 | } 81 | 82 | func (client *Client) resolveEC2PublicIPAddress(instance *ec2.Instance) (string, error) { 83 | if instance.PublicIpAddress == nil { 84 | return "", errors.Errorf("no public ip address: %s", *instance.InstanceId) 85 | } 86 | return *instance.PublicIpAddress, nil 87 | } 88 | 89 | func buildSSHConfig(loginName string, identityFile string) (*ssh.ClientConfig, error) { 90 | normalizedIdentityFile := strings.Replace(identityFile, "~", os.Getenv("HOME"), 1) 91 | key, err := os.ReadFile(normalizedIdentityFile) 92 | if err != nil { 93 | return nil, errors.Wrap(err, "unable to read private key:") 94 | } 95 | 96 | signer, err := ssh.ParsePrivateKey(key) 97 | if err != nil { 98 | return nil, errors.Wrap(err, "unable to parse private key:") 99 | } 100 | 101 | config := &ssh.ClientConfig{ 102 | User: loginName, 103 | Auth: []ssh.AuthMethod{ 104 | ssh.PublicKeys(signer), 105 | }, 106 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), // nolint: gosec 107 | } 108 | 109 | return config, nil 110 | } 111 | 112 | func buildSSHSessionPipe(session *ssh.Session) error { 113 | stdin, err := session.StdinPipe() 114 | if err != nil { 115 | return errors.Wrap(err, "unable to setup stdin for session:") 116 | } 117 | go io.Copy(stdin, os.Stdin) // nolint: errcheck 118 | 119 | stdout, err := session.StdoutPipe() 120 | if err != nil { 121 | return errors.Wrap(err, "unable to setup stdout for session:") 122 | } 123 | go io.Copy(os.Stdout, stdout) // nolint: errcheck 124 | 125 | stderr, err := session.StderrPipe() 126 | if err != nil { 127 | return errors.Wrap(err, "unable to setup stderr for session:") 128 | } 129 | go io.Copy(os.Stderr, stderr) // nolint: errcheck 130 | 131 | return nil 132 | } 133 | 134 | func (client *Client) startSSHSessionWithTerminal(hostname string, port string, config *ssh.ClientConfig) error { 135 | addr := fmt.Sprintf("%s:%s", hostname, port) 136 | connection, err := ssh.Dial("tcp", addr, config) 137 | if err != nil { 138 | return errors.Wrap(err, "unable to connect:") 139 | } 140 | defer connection.Close() 141 | 142 | session, err := connection.NewSession() 143 | if err != nil { 144 | return errors.Wrap(err, "unable to new session failed:") 145 | } 146 | defer session.Close() 147 | 148 | modes := ssh.TerminalModes{ 149 | ssh.ECHO: 1, // enable echoing 150 | ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 151 | ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 152 | } 153 | 154 | fd := int(os.Stdin.Fd()) 155 | oldState, err := terminal.MakeRaw(fd) 156 | if err != nil { 157 | return errors.Wrap(err, "unable to put terminal in Raw Mode:") 158 | } 159 | defer terminal.Restore(fd, oldState) // nolint: errcheck 160 | 161 | width, height, _ := terminal.GetSize(fd) 162 | 163 | if err := session.RequestPty("xterm", height, width, modes); err != nil { 164 | return errors.Wrap(err, "request for pseudo terminal failed:") 165 | } 166 | 167 | if err := buildSSHSessionPipe(session); err != nil { 168 | return err 169 | } 170 | 171 | if err := session.Shell(); err != nil { 172 | return errors.Wrap(err, "failed to start shell:") 173 | } 174 | session.Wait() // nolint: errcheck 175 | 176 | return nil 177 | } 178 | 179 | func (client *Client) executeSSHCommand(hostname string, port string, config *ssh.ClientConfig, command string) error { 180 | addr := fmt.Sprintf("%s:%s", hostname, port) 181 | connection, err := ssh.Dial("tcp", addr, config) 182 | if err != nil { 183 | return errors.Wrap(err, "unable to connect:") 184 | } 185 | defer connection.Close() 186 | 187 | session, err := connection.NewSession() 188 | if err != nil { 189 | return errors.Wrap(err, "unable to new session failed:") 190 | } 191 | defer session.Close() 192 | 193 | // Request pty for sudo 194 | fd := int(os.Stdin.Fd()) 195 | width, height, _ := terminal.GetSize(fd) 196 | if err := session.RequestPty("xterm", height, width, ssh.TerminalModes{}); err != nil { 197 | return errors.Wrap(err, "request for pseudo terminal failed:") 198 | } 199 | 200 | out, err := session.CombinedOutput(command) 201 | 202 | fmt.Fprintf(client.stdout, "========== Start output on host: %s ==========\n", hostname) 203 | fmt.Fprintln(client.stdout, string(out)) 204 | fmt.Fprintf(client.stdout, "========== End output on host: %s ==========\n", hostname) 205 | 206 | if err != nil { 207 | return errors.Wrapf(err, "failed to execute command: %s", command) 208 | } 209 | 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /myaws/ec2_start.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/ec2" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // EC2StartOptions customize the behavior of the Start command. 11 | type EC2StartOptions struct { 12 | InstanceIds []*string 13 | Wait bool 14 | } 15 | 16 | // EC2Start starts EC2 instances. 17 | // If wait flag is true, wait until instance is in running state. 18 | func (client *Client) EC2Start(options EC2StartOptions) error { 19 | params := &ec2.StartInstancesInput{ 20 | InstanceIds: options.InstanceIds, 21 | } 22 | 23 | response, err := client.EC2.StartInstances(params) 24 | if err != nil { 25 | return errors.Wrap(err, "StartInstances failed:") 26 | } 27 | 28 | fmt.Fprintln(client.stdout, response) 29 | 30 | if options.Wait { 31 | fmt.Fprintln(client.stdout, "Wait until instance running...") 32 | err := client.EC2.WaitUntilInstanceRunning( 33 | &ec2.DescribeInstancesInput{ 34 | InstanceIds: options.InstanceIds, 35 | }, 36 | ) 37 | if err != nil { 38 | return errors.Wrap(err, "WaitUntilInstanceRunning failed:") 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /myaws/ec2_stop.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/ec2" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // EC2StopOptions customize the behavior of the Stop command. 11 | type EC2StopOptions struct { 12 | InstanceIds []*string 13 | Wait bool 14 | } 15 | 16 | // EC2Stop stops EC2 instances. 17 | // If wait flag is true, wait until instance is in stopped state. 18 | func (client *Client) EC2Stop(options EC2StopOptions) error { 19 | params := &ec2.StopInstancesInput{ 20 | InstanceIds: options.InstanceIds, 21 | } 22 | 23 | response, err := client.EC2.StopInstances(params) 24 | if err != nil { 25 | return errors.Wrap(err, "StopInstances failed:") 26 | } 27 | 28 | fmt.Fprintln(client.stdout, response) 29 | 30 | if options.Wait { 31 | fmt.Fprintln(client.stdout, "Wait until instance stopped...") 32 | err := client.EC2.WaitUntilInstanceStopped( 33 | &ec2.DescribeInstancesInput{ 34 | InstanceIds: options.InstanceIds, 35 | }, 36 | ) 37 | if err != nil { 38 | return errors.Wrap(err, "WaitUntilInstanceStopped failed:") 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /myaws/ec2ri.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/service/ec2" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // FindEC2ReservedInstances return an array of reserved instances matching the conditions. 10 | func (client *Client) FindEC2ReservedInstances(all bool) ([]*ec2.ReservedInstances, error) { 11 | params := &ec2.DescribeReservedInstancesInput{ 12 | Filters: []*ec2.Filter{ 13 | buildEC2RIStateFilter(all), 14 | }, 15 | } 16 | 17 | response, err := client.EC2.DescribeReservedInstances(params) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "DescribeReservedInstances failed") 20 | } 21 | 22 | var reservedInstances []*ec2.ReservedInstances 23 | for _, ri := range response.ReservedInstances { // nolint: gosimple 24 | reservedInstances = append(reservedInstances, ri) 25 | } 26 | 27 | return reservedInstances, nil 28 | } 29 | 30 | func buildEC2RIStateFilter(all bool) *ec2.Filter { 31 | var stateFilter *ec2.Filter 32 | if !all { 33 | stateFilter = &ec2.Filter{ 34 | Name: aws.String("state"), 35 | Values: []*string{ 36 | aws.String("active"), 37 | }, 38 | } 39 | } 40 | return stateFilter 41 | } 42 | -------------------------------------------------------------------------------- /myaws/ec2ri_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | ) 9 | 10 | // EC2RILsOptions customize the behavior of the Ls command. 11 | type EC2RILsOptions struct { 12 | All bool 13 | Quiet bool 14 | Fields []string 15 | } 16 | 17 | // EC2RILs describes EC2 Reserved Instances. 18 | func (client *Client) EC2RILs(options EC2RILsOptions) error { 19 | instances, err := client.FindEC2ReservedInstances(options.All) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | for _, instance := range instances { 25 | fmt.Fprintln(client.stdout, formatEC2RIInstance(client, options, instance)) 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func formatEC2RIInstance(client *Client, options EC2RILsOptions, instance *ec2.ReservedInstances) string { 32 | formatFuncs := map[string]func(client *Client, options EC2RILsOptions, instance *ec2.ReservedInstances) string{ 33 | "ReservedInstancesId": formatEC2ReservedInstanceID, 34 | "AvailabilityZone": formatEC2RIAvailabilityZone, 35 | "InstanceType": formatEC2RIInstanceType, 36 | "InstanceCount": formatEC2RIInstanceCount, 37 | "State": formatEC2RIState, 38 | "Scope": formatEC2RIScope, 39 | "Start": formatEC2RIStart, 40 | "End": formatEC2RIEnd, 41 | "Duration": formatEC2RIDuration, 42 | } 43 | 44 | var outputFields []string 45 | if options.Quiet { 46 | outputFields = []string{"InstanceId"} 47 | } else { 48 | outputFields = options.Fields 49 | } 50 | 51 | output := []string{} 52 | 53 | for _, field := range outputFields { 54 | value := formatFuncs[field](client, options, instance) 55 | output = append(output, value) 56 | } 57 | 58 | return strings.Join(output[:], "\t") 59 | } 60 | 61 | func formatEC2ReservedInstanceID(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 62 | return *instance.ReservedInstancesId 63 | } 64 | 65 | func formatEC2RIAvailabilityZone(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 66 | if instance.AvailabilityZone != nil { 67 | return *instance.AvailabilityZone 68 | } 69 | return "N/A" 70 | } 71 | 72 | func formatEC2RIInstanceType(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 73 | return *instance.InstanceType 74 | } 75 | 76 | func formatEC2RIInstanceCount(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 77 | return fmt.Sprintf("%3d", *instance.InstanceCount) 78 | } 79 | 80 | func formatEC2RIState(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 81 | return *instance.State 82 | } 83 | 84 | func formatEC2RIScope(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 85 | return *instance.Scope 86 | } 87 | 88 | func formatEC2RIStart(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 89 | return instance.Start.Format("2006-01-02") 90 | } 91 | 92 | func formatEC2RIEnd(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 93 | return instance.End.Format("2006-01-02") 94 | } 95 | 96 | func formatEC2RIDuration(_ *Client, _ EC2RILsOptions, instance *ec2.ReservedInstances) string { 97 | return fmt.Sprintf("%2dyear", *instance.Duration/(3600*24*365)) 98 | } 99 | -------------------------------------------------------------------------------- /myaws/ecr_get_login.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/service/ecr" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // ECRGetLoginOptions customize the behavior of the ECRGetLogin command. 13 | type ECRGetLoginOptions struct { 14 | RegistryIds []*string 15 | } 16 | 17 | // ECRGetLogin gets docker login command with authorization token for ECR. 18 | func (client *Client) ECRGetLogin(options ECRGetLoginOptions) error { 19 | params := &ecr.GetAuthorizationTokenInput{} 20 | 21 | if len(options.RegistryIds) > 0 { 22 | params.RegistryIds = options.RegistryIds // nolint: staticcheck 23 | } 24 | 25 | response, err := client.ECR.GetAuthorizationToken(params) 26 | if err != nil { 27 | return errors.Wrap(err, "GetAuthorizationToken failed:") 28 | } 29 | fmt.Fprintln(client.stdout, formatECRAuthorizationData(response.AuthorizationData)) 30 | 31 | return nil 32 | } 33 | 34 | func formatECRAuthorizationData(authDataList []*ecr.AuthorizationData) string { 35 | output := []string{} 36 | for _, authData := range authDataList { 37 | output = append(output, formatECRDockerLoginCommand(authData)) 38 | } 39 | return strings.Join(output[:], "\n") 40 | } 41 | 42 | func formatECRDockerLoginCommand(authData *ecr.AuthorizationData) string { 43 | return fmt.Sprintf( 44 | "docker login -u AWS -p %s %s", 45 | decodeECRPassword(*authData.AuthorizationToken), 46 | *authData.ProxyEndpoint, 47 | ) 48 | } 49 | 50 | func decodeECRPassword(authToken string) string { 51 | userAndPassword, _ := base64.StdEncoding.DecodeString(authToken) 52 | s := strings.Split(string(userAndPassword), ":") 53 | return s[1] 54 | } 55 | -------------------------------------------------------------------------------- /myaws/ecs.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/ecs" 7 | "github.com/pkg/errors" 8 | funk "github.com/thoas/go-funk" 9 | ) 10 | 11 | // findECSNodes finds ECS container instances 12 | func (client *Client) findECSNodes(cluster string) ([]*ecs.ContainerInstance, error) { 13 | arns, err := client.ECS.ListContainerInstances( 14 | &ecs.ListContainerInstancesInput{ 15 | Cluster: &cluster, 16 | }, 17 | ) 18 | if err != nil { 19 | return nil, errors.Wrapf(err, "ListContainerInstances failed") 20 | } 21 | 22 | if len(arns.ContainerInstanceArns) == 0 { 23 | return nil, errors.New("container instances not found") 24 | } 25 | 26 | instances, err := client.ECS.DescribeContainerInstances( 27 | &ecs.DescribeContainerInstancesInput{ 28 | Cluster: &cluster, 29 | ContainerInstances: arns.ContainerInstanceArns, 30 | }, 31 | ) 32 | if err != nil { 33 | return nil, errors.Wrapf(err, "DescribeContainerInstances failed") 34 | } 35 | 36 | if len(instances.ContainerInstances) == 0 { 37 | return nil, errors.New("ListContainerInstances succeed, but DescribeContainerInstances returns no instances") 38 | } 39 | 40 | return instances.ContainerInstances, nil 41 | } 42 | 43 | // findECSService find ECS services. 44 | func (client *Client) findECSServices(cluster string) ([]*ecs.Service, error) { 45 | serviceArns := []*string{} 46 | 47 | err := client.ECS.ListServicesPages( 48 | &ecs.ListServicesInput{ 49 | Cluster: &cluster, 50 | }, 51 | func(p *ecs.ListServicesOutput, _ bool) bool { 52 | serviceArns = append(serviceArns, p.ServiceArns...) 53 | return true 54 | }, 55 | ) 56 | if err != nil { 57 | return nil, errors.Wrapf(err, "ListServices failed") 58 | } 59 | 60 | if len(serviceArns) == 0 { 61 | return nil, errors.New("services not found") 62 | } 63 | 64 | // We can specify up to 10 services to describe in a single operation. 65 | // So we need to divide the list by 10. 66 | chunks := (funk.Chunk(serviceArns, 10)).([][]*string) 67 | services := []*ecs.Service{} 68 | for _, c := range chunks { 69 | ss, err := client.ECS.DescribeServices( 70 | &ecs.DescribeServicesInput{ 71 | Cluster: &cluster, 72 | Services: c, 73 | }, 74 | ) 75 | if err != nil { 76 | return nil, errors.Wrapf(err, "DescribeServices failed") 77 | } 78 | services = append(services, (ss.Services)...) 79 | } 80 | 81 | if len(services) == 0 { 82 | return nil, errors.New("ListServices succeed, but DescribeServices returns no services") 83 | } 84 | 85 | return services, nil 86 | } 87 | 88 | func (client *Client) printECSStatus(cluster string) error { 89 | fmt.Fprintln(client.stdout, "[Service]") 90 | err := client.ECSServiceLs(ECSServiceLsOptions{ 91 | Cluster: cluster, 92 | PrintHeader: true, 93 | }) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | fmt.Fprintln(client.stdout, "[Node]") 99 | err = client.ECSNodeLs(ECSNodeLsOptions{ 100 | Cluster: cluster, 101 | PrintHeader: true, 102 | }) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /myaws/ecs_node_drain.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/ecs" 10 | "github.com/pkg/errors" 11 | funk "github.com/thoas/go-funk" 12 | ) 13 | 14 | // ECSNodeDrainOptions customize the behavior of the Drain command. 15 | type ECSNodeDrainOptions struct { 16 | Cluster string 17 | ContainerInstances []*string 18 | Wait bool 19 | Timeout time.Duration 20 | } 21 | 22 | // ECSNodeDrain Drain ECS container instances. 23 | // We want to wait until drain action is completed, but the ECSNodeUpdate 24 | // method is general purpose, so we implement a wait option to specialized 25 | // method for draining. 26 | func (client *Client) ECSNodeDrain(options ECSNodeDrainOptions) error { 27 | return client.ecsNodeDrainWithContext(context.Background(), options) 28 | } 29 | 30 | func (client *Client) ecsNodeDrainWithContext(ctx context.Context, options ECSNodeDrainOptions) error { 31 | // We can specify up to 10 container instances to update state in a single operation. 32 | // This constraint is not specified in the API reference, but it returns the following error: 33 | // InvalidParameterException: instanceIds can have at most 10 items. 34 | // So we need to divide the list by 10. 35 | chunks := (funk.Chunk(options.ContainerInstances, 10)).([][]*string) 36 | for _, c := range chunks { 37 | _, err := client.ECS.UpdateContainerInstancesState( 38 | &ecs.UpdateContainerInstancesStateInput{ 39 | Cluster: &options.Cluster, 40 | ContainerInstances: c, 41 | Status: aws.String("DRAINING"), 42 | }, 43 | ) 44 | if err != nil { 45 | return errors.Wrapf(err, "UpdateContainerInstancesState failed") 46 | } 47 | } 48 | 49 | if options.Wait { 50 | fmt.Fprintln(client.stdout, "Wait until container instances are drained...") 51 | ctx, cancel := context.WithTimeout(ctx, options.Timeout) 52 | defer cancel() 53 | return client.WaitUntilECSContainerInstancesAreDrainedWithContext(ctx, options.Cluster, options.ContainerInstances) 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /myaws/ecs_node_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/ecs" 8 | ) 9 | 10 | // ECSNodeLsOptions customize the behavior of the Ls command. 11 | type ECSNodeLsOptions struct { 12 | Cluster string 13 | PrintHeader bool 14 | } 15 | 16 | // ECSNodeLs describes ECS container instances. 17 | func (client *Client) ECSNodeLs(options ECSNodeLsOptions) error { 18 | instances, err := client.findECSNodes(options.Cluster) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | if options.PrintHeader { 24 | header := fmt.Sprintf("%-32s\t%-19s\t%-10s\t%s\t%s\t%s", 25 | "ContainerInstanceId", 26 | "Ec2InstanceId", 27 | "Status", 28 | "Running", 29 | "Pending", 30 | "RegisteredAt", 31 | ) 32 | fmt.Fprintln(client.stdout, header) 33 | } 34 | 35 | for _, instance := range instances { 36 | fmt.Fprintln(client.stdout, formatECSNode(client, instance)) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func formatECSNode(client *Client, instance *ecs.ContainerInstance) string { 43 | arn := strings.Split(*instance.ContainerInstanceArn, "/") 44 | // To fix misalignment, we use the width of state is 10 characters here, 45 | // because 8 characters + 2 characters as future margin of change. 46 | // The valid values of status are ACTIVE, INACTIVE, or DRAINING. 47 | return fmt.Sprintf("%s\t%s\t%-10s\t%d\t%d\t%s", 48 | arn[2], 49 | *instance.Ec2InstanceId, 50 | *instance.Status, 51 | *instance.RunningTasksCount, 52 | *instance.PendingTasksCount, 53 | client.FormatTime(instance.RegisteredAt), 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /myaws/ecs_node_renew.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws/awsutil" 9 | "github.com/aws/aws-sdk-go/service/autoscaling" 10 | "github.com/aws/aws-sdk-go/service/ecs" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // ECSNodeRenewOptions customize the behavior of the Renew command. 15 | type ECSNodeRenewOptions struct { 16 | Cluster string 17 | AsgName string 18 | Timeout time.Duration 19 | } 20 | 21 | // ECSNodeRenew renew ECS container instances with blue-green deployment. 22 | // This method is an automation process to renew your ECS container instances 23 | // if you update the AMI. creates new instances, drains the old instances, 24 | // and discards the old instances. 25 | func (client *Client) ECSNodeRenew(options ECSNodeRenewOptions) error { 26 | // We should use a context everywhere in all AWS API calls, 27 | // but for the moment only some are supported. 28 | // So this is a total timeout, and indivisual wait operations can timeout in 29 | // shorter amount of time if it has fixed timeout in the waiter. 30 | ctx, cancel := context.WithTimeout(context.Background(), options.Timeout) 31 | defer cancel() 32 | 33 | done := make(chan error, 1) 34 | go func() { 35 | err := client.ecsNodeRenewWithContext(ctx, options) 36 | if err != nil { 37 | done <- err 38 | } 39 | done <- nil 40 | }() 41 | 42 | select { 43 | case e := <-done: 44 | return e 45 | case <-ctx.Done(): 46 | return ctx.Err() 47 | } 48 | } 49 | 50 | func (client *Client) ecsNodeRenewWithContext(ctx context.Context, options ECSNodeRenewOptions) error { 51 | fmt.Fprintf(client.stdout, "start: ecs node renew\noptions: %s\n", awsutil.Prettify(options)) 52 | 53 | if err := client.printECSStatus(options.Cluster); err != nil { 54 | return err 55 | } 56 | 57 | // get the current desired capacity 58 | desiredCapacity, err := client.getAutoScalingGroupDesiredCapacity(options.AsgName) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // list the current container instances 64 | oldNodes, err := client.findECSNodes(options.Cluster) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if len(oldNodes) != int(desiredCapacity) { 70 | return errors.Errorf("assertion failed: currentCapacity(%d) != desiredCapacity(%d)", len(oldNodes), desiredCapacity) 71 | } 72 | 73 | // Update the desired capacity and wait until new instances are InService 74 | // We simply double the number of instances here. 75 | // If you need more flexible control, please implement a strategy such as 76 | // rolling update. 77 | targetCapacity := desiredCapacity * 2 78 | 79 | fmt.Fprintf(client.stdout, "Update autoscaling group %s (DesiredCapacity: %d => %d)\n", options.AsgName, desiredCapacity, targetCapacity) 80 | 81 | err = client.AutoscalingUpdate(AutoscalingUpdateOptions{ 82 | AsgName: options.AsgName, 83 | DesiredCapacity: targetCapacity, 84 | Wait: true, 85 | }) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | if err = client.printECSStatus(options.Cluster); err != nil { 91 | return err 92 | } 93 | 94 | // A status of instance in autoscaling group is InService doesn't mean the 95 | // container instance is registered. We should make sure container instances 96 | // are registered 97 | fmt.Fprintln(client.stdout, "Wait until ECS container instances are registered...") 98 | err = client.WaitUntilECSContainerInstancesAreRegistered(options.Cluster, targetCapacity) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | if err = client.printECSStatus(options.Cluster); err != nil { 104 | return err 105 | } 106 | 107 | // drain old container instances and wait until no task running 108 | oldNodeArns := []*string{} 109 | for _, oldNode := range oldNodes { 110 | oldNodeArns = append(oldNodeArns, oldNode.ContainerInstanceArn) 111 | } 112 | fmt.Fprintf(client.stdout, "Drain old container instances and wait until no task running...\n%v\n", awsutil.Prettify(oldNodeArns)) 113 | err = client.ecsNodeDrainWithContext(ctx, ECSNodeDrainOptions{ 114 | Cluster: options.Cluster, 115 | ContainerInstances: oldNodeArns, 116 | Wait: true, 117 | Timeout: options.Timeout, 118 | }) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if err = client.printECSStatus(options.Cluster); err != nil { 124 | return err 125 | } 126 | 127 | // All old container instances are drained doesn't mean all services are stable. 128 | // It depends on the deployment strategy of each service. 129 | // We should make sure all services are stable 130 | fmt.Fprintln(client.stdout, "Wait until all ECS services stable...") 131 | err = client.WaitUntilECSAllServicesStableWithContext(ctx, options.Cluster) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | if err = client.printECSStatus(options.Cluster); err != nil { 137 | return err 138 | } 139 | 140 | // A stable state for all services does not mean that all targets are healthy. 141 | // We need to explicitly confirm it. 142 | fmt.Fprintln(client.stdout, "Wait until all targets healthy...") 143 | err = client.WaitUntilECSAllTargetsInService(options.Cluster) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | if err = client.printECSStatus(options.Cluster); err != nil { 149 | return err 150 | } 151 | 152 | // Select instances to protect from scale in. 153 | // By setting "scale-in protection" to instances created at scale-out, 154 | // the intended instances (instances created before scale-in) are only terminated at scale-in process. 155 | protectInstanceIds, err := client.selectInstanceToProtectFromScaleIn(oldNodes, options.Cluster) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | // Enable scale in protection for specific instances before scaling in. 161 | // In the default termination policy of auto scaling, instances close to the next billing time will terminate when the launch configuration is the same. 162 | // By running this function, your auto scaling group scales out, then scales in. 163 | // During scale in, instances created during scale out may be subject to termination. 164 | // To prevent this, set scale in protection for instances created at scale out. 165 | // https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-instance-termination.html 166 | fmt.Fprintln(client.stdout, "Setting scale in protection: ", awsutil.Prettify(protectInstanceIds)) 167 | // set "scale in protection" to instances created at scale-out. 168 | err = client.AutoScalingSetInstanceProtection(AutoScalingSetInstanceProtectionOptions{ 169 | options.AsgName, 170 | protectInstanceIds, 171 | true}) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | // restore the desired capacity and wait until old instances are discarded 177 | fmt.Fprintf(client.stdout, "Update autoscaling group %s (DesiredCapacity: %d => %d)\n", options.AsgName, targetCapacity, desiredCapacity) 178 | 179 | err = client.AutoscalingUpdate(AutoscalingUpdateOptions{ 180 | AsgName: options.AsgName, 181 | DesiredCapacity: desiredCapacity, 182 | Wait: true, 183 | }) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | // remove "scale in protection" to instances created at scale-out. 189 | fmt.Fprintln(client.stdout, "Removing scale in protection: ", awsutil.Prettify(protectInstanceIds)) 190 | err = client.AutoScalingSetInstanceProtection(AutoScalingSetInstanceProtectionOptions{ 191 | options.AsgName, 192 | protectInstanceIds, 193 | false}) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | if err = client.printECSStatus(options.Cluster); err != nil { 199 | return err 200 | } 201 | 202 | fmt.Fprintln(client.stdout, "end: ecs node renew") 203 | return nil 204 | } 205 | 206 | // selectInstanceToProtectFromScaleIn selects instance to protect from Scale in. 207 | // instance select rule: 208 | // instances after scale out - instances before scale out - instances which already set `InstanceProtection==true` 209 | func (client *Client) selectInstanceToProtectFromScaleIn(oldNodes []*ecs.ContainerInstance, cluster string) ([]*string, error) { 210 | // Get a list of instance IDs before auto scaling 211 | var oldInstanceIds []*string 212 | for _, oldNode := range oldNodes { 213 | oldInstanceIds = append(oldInstanceIds, oldNode.Ec2InstanceId) 214 | } 215 | 216 | // Get a list of instances after auto scaling 217 | allNodes, err := client.findECSNodes(cluster) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | // Get a list of instance IDs after auto scaling 223 | var allInstanceIds []*string 224 | for _, allNode := range allNodes { 225 | allInstanceIds = append(allInstanceIds, allNode.Ec2InstanceId) 226 | } 227 | 228 | // get newly created nodes (allInstanceIds - oldInstanceIds) 229 | newInstanceIds := difference(allInstanceIds, oldInstanceIds) 230 | 231 | // exclude ProtectedFromScaleIn == true nodes 232 | params := &autoscaling.DescribeAutoScalingInstancesInput{ 233 | InstanceIds: newInstanceIds, 234 | } 235 | response, err := client.AutoScaling.DescribeAutoScalingInstances(params) 236 | if err != nil { 237 | return nil, errors.Wrap(err, "DescribeAutoScalingGroups failed:") 238 | } 239 | 240 | var targetInstanceIds []*string 241 | for _, instance := range response.AutoScalingInstances { 242 | if *instance.ProtectedFromScaleIn == false { // nolint: gosimple 243 | targetInstanceIds = append(targetInstanceIds, instance.InstanceId) 244 | } 245 | } 246 | return targetInstanceIds, nil 247 | } 248 | 249 | // difference returns the elements in `a` that aren't in `b`. 250 | func difference(a, b []*string) []*string { 251 | mb := make(map[string]struct{}, len(b)) 252 | for _, x := range b { 253 | mb[*x] = struct{}{} 254 | } 255 | var diff []*string 256 | for _, x := range a { 257 | if _, ok := mb[*x]; !ok { 258 | diff = append(diff, x) 259 | } 260 | } 261 | return diff 262 | } 263 | -------------------------------------------------------------------------------- /myaws/ecs_node_update.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/ecs" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // ECSNodeUpdateOptions customize the behavior of the Update command. 9 | type ECSNodeUpdateOptions struct { 10 | Cluster string 11 | ContainerInstances []*string 12 | Status string 13 | } 14 | 15 | // ECSNodeUpdate Update ECS container instances. 16 | func (client *Client) ECSNodeUpdate(options ECSNodeUpdateOptions) error { 17 | _, err := client.ECS.UpdateContainerInstancesState( 18 | &ecs.UpdateContainerInstancesStateInput{ 19 | Cluster: &options.Cluster, 20 | ContainerInstances: options.ContainerInstances, 21 | Status: &options.Status, 22 | }, 23 | ) 24 | 25 | if err != nil { 26 | return errors.Wrapf(err, "UpdateContainerInstancesState failed") 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /myaws/ecs_service_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/ecs" 8 | ) 9 | 10 | // ECSServiceLsOptions customize the behavior of the Ls command. 11 | type ECSServiceLsOptions struct { 12 | Cluster string 13 | PrintHeader bool 14 | } 15 | 16 | // ECSServiceLs describes ECS services. 17 | func (client *Client) ECSServiceLs(options ECSServiceLsOptions) error { 18 | services, err := client.findECSServices(options.Cluster) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | if options.PrintHeader { 24 | header := fmt.Sprintf("%s\t%s\t%s\t%s\t%-32s\t%s", 25 | "Desired", 26 | "Running", 27 | "Pending", 28 | "Deploy", 29 | "Service", 30 | "TaskDefinition", 31 | ) 32 | fmt.Fprintln(client.stdout, header) 33 | } 34 | 35 | for _, service := range services { 36 | fmt.Fprintln(client.stdout, formatECSService(client, options, service)) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func formatECSService(_ *Client, _ ECSServiceLsOptions, service *ecs.Service) string { 43 | taskDefinitions := strings.Split(*service.TaskDefinition, "/") 44 | 45 | return fmt.Sprintf("%d\t%d\t%d\t%d\t%-32s\t%s", 46 | *service.DesiredCount, 47 | *service.RunningCount, 48 | *service.PendingCount, 49 | len(service.Deployments), 50 | *service.ServiceName, 51 | taskDefinitions[1], 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /myaws/ecs_service_update.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go/service/ecs" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // ECSServiceUpdateOptions customize the behavior of the Update command. 14 | type ECSServiceUpdateOptions struct { 15 | Cluster string 16 | Service string 17 | DesiredCount *int64 18 | Wait bool 19 | Timeout time.Duration 20 | Force bool 21 | } 22 | 23 | // ECSServiceUpdate update ECS services. 24 | func (client *Client) ECSServiceUpdate(options ECSServiceUpdateOptions) error { 25 | input := &ecs.UpdateServiceInput{ 26 | Cluster: &options.Cluster, 27 | Service: &options.Service, 28 | DesiredCount: options.DesiredCount, 29 | ForceNewDeployment: &options.Force, 30 | } 31 | 32 | if options.Force { 33 | // When updating a ECS task definition, We want to deploy a new task with 34 | // new revision. 35 | // The ECS UpdateService API doesn't update the revision without the 36 | // TaskDefinition parameter. To update the revision, we set only a task 37 | // family without revision. If a revision is not specified, the latest 38 | // ACTIVE revision is used. If the current task uses the latest ACTIVE one, 39 | // it hasn't no side-effect. 40 | // Since the Force option expects to deploy and converge to the latest 41 | // state, when Force option is enabled, we implicitly fetch and set the 42 | // task family. 43 | family, err := client.getECSTaskDefinitionFamily(options.Cluster, options.Service) 44 | if err != nil { 45 | return err 46 | } 47 | input.TaskDefinition = &family 48 | } 49 | 50 | _, err := client.ECS.UpdateService(input) 51 | if err != nil { 52 | return errors.Wrapf(err, "UpdateService failed") 53 | } 54 | 55 | if options.Wait { 56 | fmt.Fprintln(client.stdout, "Wait until the service stable...") 57 | ctx, cancel := context.WithTimeout(context.Background(), options.Timeout) 58 | defer cancel() 59 | err = client.WaitUntilECSServicesStableWithContext(ctx, options.Cluster, []string{options.Service}) 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // getECSTaskDefinitionFamily returns a family name of ECS task definition used by a given ECS service. 69 | func (client *Client) getECSTaskDefinitionFamily(cluster string, service string) (string, error) { 70 | input := &ecs.DescribeServicesInput{ 71 | Cluster: &cluster, 72 | Services: []*string{&service}, 73 | } 74 | 75 | resp, err := client.ECS.DescribeServices(input) 76 | if err != nil { 77 | return "", errors.Wrapf(err, "DescribeServices failed") 78 | } 79 | 80 | if len(resp.Services) == 0 { 81 | return "", fmt.Errorf("service not fould: cluster = %s, service = %s", cluster, service) 82 | } 83 | 84 | arn := *resp.Services[0].TaskDefinition 85 | 86 | // arn:aws:ecs:::task-definition/: 87 | re := regexp.MustCompile(`^arn:aws:ecs:.+:.+:task-definition/(.+):.+$`) 88 | matched := re.FindStringSubmatch(arn) 89 | if len(matched) != 2 || len(matched[1]) == 0 { 90 | return "", fmt.Errorf("failed to parse task definition arn: %s", arn) 91 | } 92 | family := matched[1] 93 | 94 | return family, nil 95 | } 96 | -------------------------------------------------------------------------------- /myaws/ecs_status.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | // ECSStatusOptions customize the behavior of the Ls command. 4 | type ECSStatusOptions struct { 5 | Cluster string 6 | } 7 | 8 | // ECSStatus prints ECS status. 9 | func (client *Client) ECSStatus(options ECSStatusOptions) error { 10 | return client.printECSStatus(options.Cluster) 11 | } 12 | -------------------------------------------------------------------------------- /myaws/ecs_waiter.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/awserr" 9 | "github.com/aws/aws-sdk-go/aws/request" 10 | "github.com/aws/aws-sdk-go/service/ecs" 11 | "github.com/aws/aws-sdk-go/service/elbv2" 12 | "github.com/pkg/errors" 13 | funk "github.com/thoas/go-funk" 14 | ) 15 | 16 | // WaitUntilECSContainerInstancesAreDrainedWithContext is a helper function 17 | // which waits until the ECS container instances are drained. 18 | // Due to the current limitation of the implementation of `request.Waiter`, 19 | // we need to wait it in two steps. 20 | // 1. Wait until container instances are DRAINING state. 21 | // 2. Wait until no running tasks on the container instances. 22 | func (client *Client) WaitUntilECSContainerInstancesAreDrainedWithContext(ctx context.Context, cluster string, containerInstances []*string) error { 23 | input := &ecs.DescribeContainerInstancesInput{ 24 | Cluster: &cluster, 25 | ContainerInstances: containerInstances, 26 | } 27 | 28 | // make sure container instances are DRAINING state 29 | if err := client.WaitUntilECSContainerInstancesStatusWithContext(ctx, input, "DRAINING"); err != nil { 30 | return err 31 | } 32 | 33 | // wait until no running tasks on the container instances 34 | if err := client.WaitUntilECSContainerInstancesNoRunningTaskWithContext(ctx, input); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // WaitUntilECSContainerInstancesStatusWithContext waits until ECS ContainerInstances in a given Status. 42 | // The waitUntilECSContainerInstancesStatusWithContext has fixed MaxAttempts(40) and Delay(15), 43 | // we can't wait more than 10 minutes. 44 | // We may be able to set a longer timeout, but there is no single appropriate value to meet any case. 45 | // So we wrap it and allow timeout with a given context. 46 | // Note that this function never timeout itself. 47 | func (client *Client) WaitUntilECSContainerInstancesStatusWithContext(ctx aws.Context, input *ecs.DescribeContainerInstancesInput, status string, opts ...request.WaiterOption) error { 48 | for { 49 | err := client.waitUntilECSContainerInstancesStatusWithContext(ctx, input, status, opts...) 50 | if err != nil { 51 | if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == request.WaiterResourceNotReadyErrorCode { 52 | // internal waiter timeout, retry. 53 | continue 54 | } 55 | return errors.Wrapf(err, "waitUntilECSContainerInstancesStatusWithContext failed") 56 | } 57 | break 58 | } 59 | return nil 60 | } 61 | 62 | func (client *Client) waitUntilECSContainerInstancesStatusWithContext(ctx aws.Context, input *ecs.DescribeContainerInstancesInput, status string, opts ...request.WaiterOption) error { 63 | w := request.Waiter{ 64 | Name: "WaitUntilECSContainerInstancesStatus", 65 | MaxAttempts: 40, 66 | Delay: request.ConstantWaiterDelay(15 * time.Second), 67 | Acceptors: []request.WaiterAcceptor{ 68 | { 69 | State: request.SuccessWaiterState, 70 | Matcher: request.PathAllWaiterMatch, Argument: "ContainerInstances[].Status", 71 | Expected: status, 72 | }, 73 | }, 74 | Logger: client.config.Logger, 75 | NewRequest: func(opts []request.Option) (*request.Request, error) { 76 | var inCpy *ecs.DescribeContainerInstancesInput 77 | if input != nil { 78 | tmp := *input 79 | inCpy = &tmp 80 | } 81 | req, _ := client.ECS.DescribeContainerInstancesRequest(inCpy) 82 | req.SetContext(ctx) 83 | req.ApplyOptions(opts...) 84 | return req, nil 85 | }, 86 | } 87 | w.ApplyOptions(opts...) 88 | 89 | return w.WaitWithContext(ctx) 90 | } 91 | 92 | // WaitUntilECSContainerInstancesNoRunningTaskWithContext waits until ECS ContainerInstances has no running tasks. 93 | // The waitUntilECSContainerInstancesNoRunningTaskWithContext has fixed MaxAttempts(40) and Delay(15), 94 | // we can't wait more than 10 minutes. 95 | // We may be able to set a longer timeout, but there is no single appropriate value to meet any case. 96 | // So we wrap it and allow timeout with a given context. 97 | // Note that this function never timeout itself. 98 | func (client *Client) WaitUntilECSContainerInstancesNoRunningTaskWithContext(ctx aws.Context, input *ecs.DescribeContainerInstancesInput, opts ...request.WaiterOption) error { 99 | for { 100 | err := client.waitUntilECSContainerInstancesNoRunningTaskWithContext(ctx, input, opts...) 101 | if err != nil { 102 | if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == request.WaiterResourceNotReadyErrorCode { 103 | // internal waiter timeout, retry. 104 | continue 105 | } 106 | return errors.Wrapf(err, "waitUntilECSContainerInstancesNoRunningTaskWithContext failed") 107 | } 108 | break 109 | } 110 | return nil 111 | } 112 | 113 | func (client *Client) waitUntilECSContainerInstancesNoRunningTaskWithContext(ctx aws.Context, input *ecs.DescribeContainerInstancesInput, opts ...request.WaiterOption) error { 114 | w := request.Waiter{ 115 | Name: "WaitUntilECSContainerInstancesNoRunningTask", 116 | MaxAttempts: 40, 117 | Delay: request.ConstantWaiterDelay(15 * time.Second), 118 | Acceptors: []request.WaiterAcceptor{ 119 | { 120 | State: request.SuccessWaiterState, 121 | Matcher: request.PathAllWaiterMatch, Argument: "ContainerInstances[].RunningTasksCount", 122 | Expected: aws.Int64(0), 123 | }, 124 | }, 125 | Logger: client.config.Logger, 126 | NewRequest: func(opts []request.Option) (*request.Request, error) { 127 | var inCpy *ecs.DescribeContainerInstancesInput 128 | if input != nil { 129 | tmp := *input 130 | inCpy = &tmp 131 | } 132 | req, _ := client.ECS.DescribeContainerInstancesRequest(inCpy) 133 | req.SetContext(ctx) 134 | req.ApplyOptions(opts...) 135 | return req, nil 136 | }, 137 | } 138 | w.ApplyOptions(opts...) 139 | 140 | return w.WaitWithContext(ctx) 141 | } 142 | 143 | // WaitUntilECSContainerInstancesAreRegistered is a helper function which waits until 144 | // the ECS container instances are registered. 145 | // Due to the current limitation of the implementation of `request.Waiter`, 146 | // we need to wait it in two steps. 147 | // 1. Wait until the number of container instances is targetCapacity. 148 | // 2. Wait until container instances are ACTIVE state. 149 | func (client *Client) WaitUntilECSContainerInstancesAreRegistered(cluster string, targetCapacity int64) error { 150 | ctx := aws.BackgroundContext() 151 | 152 | listInput := &ecs.ListContainerInstancesInput{ 153 | Cluster: &cluster, 154 | } 155 | 156 | // Simple count the number of container instances 157 | if err := client.waitUntilECSContainerInstancesCountWithContext(ctx, listInput, targetCapacity); err != nil { 158 | return err 159 | } 160 | 161 | // build descirbe input 162 | arns, err := client.ECS.ListContainerInstancesWithContext(ctx, listInput) 163 | 164 | if err != nil { 165 | return errors.Wrapf(err, "ListContainerInstances failed") 166 | } 167 | 168 | describeInput := &ecs.DescribeContainerInstancesInput{ 169 | Cluster: &cluster, 170 | ContainerInstances: arns.ContainerInstanceArns, 171 | } 172 | 173 | // make sure container instances are ACTIVE state 174 | if err := client.waitUntilECSContainerInstancesStatusWithContext(ctx, describeInput, "ACTIVE"); err != nil { 175 | return err 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (client *Client) waitUntilECSContainerInstancesCountWithContext(ctx aws.Context, input *ecs.ListContainerInstancesInput, targetCapacity int64, opts ...request.WaiterOption) error { 182 | w := request.Waiter{ 183 | Name: "WaitUntilECSContainerInstancesCount", 184 | MaxAttempts: 20, 185 | Delay: request.ConstantWaiterDelay(15 * time.Second), 186 | Acceptors: []request.WaiterAcceptor{ 187 | { 188 | State: request.SuccessWaiterState, 189 | Matcher: request.PathAllWaiterMatch, Argument: "length(ContainerInstanceArns[])", 190 | Expected: float64(targetCapacity), 191 | }, 192 | }, 193 | Logger: client.config.Logger, 194 | NewRequest: func(opts []request.Option) (*request.Request, error) { 195 | var inCpy *ecs.ListContainerInstancesInput 196 | if input != nil { 197 | tmp := *input 198 | inCpy = &tmp 199 | } 200 | req, _ := client.ECS.ListContainerInstancesRequest(inCpy) 201 | req.SetContext(ctx) 202 | req.ApplyOptions(opts...) 203 | return req, nil 204 | }, 205 | } 206 | w.ApplyOptions(opts...) 207 | 208 | return w.WaitWithContext(ctx) 209 | } 210 | 211 | func (client *Client) getECSServiceArns(cluster string) ([]*string, error) { 212 | serviceArns := []*string{} 213 | 214 | err := client.ECS.ListServicesPages( 215 | &ecs.ListServicesInput{ 216 | Cluster: &cluster, 217 | }, 218 | func(p *ecs.ListServicesOutput, _ bool) bool { 219 | serviceArns = append(serviceArns, p.ServiceArns...) 220 | return true 221 | }, 222 | ) 223 | if err != nil { 224 | return nil, errors.Wrapf(err, "ListServices failed") 225 | } 226 | 227 | if len(serviceArns) == 0 { 228 | return nil, errors.New("services not found") 229 | } 230 | 231 | return serviceArns, nil 232 | } 233 | 234 | func (client *Client) getECSTargetGroupArns(cluster string, serviceArn string) ([]string, error) { 235 | input := &ecs.DescribeServicesInput{ 236 | Cluster: &cluster, 237 | Services: []*string{&serviceArn}, 238 | } 239 | response, err := client.ECS.DescribeServices(input) 240 | if err != nil { 241 | return nil, errors.Wrapf(err, "DescribeServices failed") 242 | } 243 | 244 | if len(response.Services) != 1 { 245 | return nil, errors.Errorf("ECS.DescribeServices expects to return 1 service, but found %d services", len(response.Services)) 246 | } 247 | 248 | s := response.Services[0] 249 | 250 | targetGroupArns := []string{} 251 | for _, lb := range s.LoadBalancers { 252 | targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn) 253 | } 254 | 255 | return targetGroupArns, nil 256 | } 257 | 258 | func (client *Client) getECSDesiredCount(cluster string, serviceArn string) (int64, error) { 259 | input := &ecs.DescribeServicesInput{ 260 | Cluster: &cluster, 261 | Services: []*string{&serviceArn}, 262 | } 263 | response, err := client.ECS.DescribeServices(input) 264 | if err != nil { 265 | return 0, errors.Wrapf(err, "DescribeServices failed") 266 | } 267 | 268 | if len(response.Services) != 1 { 269 | return 0, errors.Errorf("ECS.DescribeServices expects to return 1 service, but found %d services", len(response.Services)) 270 | } 271 | 272 | s := response.Services[0] 273 | desiredCount := s.DesiredCount 274 | 275 | return *desiredCount, nil 276 | } 277 | 278 | // WaitUntilECSAllServicesStableWithContext is a helper function which wait 279 | // until all ECS servcies are running the desired number of containers. 280 | // The official (*ECS) WaitUntilServicesStable does not support more than 10 281 | // services. 282 | // We need to check 10 services at a time. 283 | func (client *Client) WaitUntilECSAllServicesStableWithContext(ctx context.Context, cluster string) error { 284 | serviceArns, err := client.getECSServiceArns(cluster) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | // We can specify up to 10 services to describe in a single operation. 290 | // So we need to divide the list by 10. 291 | chunks := (funk.Chunk(serviceArns, 10)).([][]*string) 292 | for _, c := range chunks { 293 | // We use custome wait function to allow us wait more than 10 minutes with context. 294 | err := client.WaitUntilECSServicesStableWithContext(ctx, cluster, aws.StringValueSlice(c)) 295 | if err != nil { 296 | return errors.Wrapf(err, "WaitUntilServicesStable failed") 297 | } 298 | } 299 | 300 | return nil 301 | } 302 | 303 | // WaitUntilECSServicesStableWithContext waits until ECS services stable. 304 | // The official (*ECS) WaitUntilServicesStableWithContext has fixed MaxAttempts(40) and Delay(15), 305 | // we can't wait more than 10 minutes. 306 | // So we wrap it and allow timeout with a given context. 307 | // Note that this function never timeout itself. 308 | func (client *Client) WaitUntilECSServicesStableWithContext(ctx context.Context, cluster string, services []string) error { 309 | for { 310 | err := client.ECS.WaitUntilServicesStableWithContext( 311 | ctx, 312 | &ecs.DescribeServicesInput{ 313 | Cluster: &cluster, 314 | Services: aws.StringSlice(services), 315 | }, 316 | ) 317 | if err != nil { 318 | if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == request.WaiterResourceNotReadyErrorCode { 319 | // internal waiter timeout, retry. 320 | continue 321 | } 322 | return errors.Wrapf(err, "WaitUntilServicesStable failed") 323 | } 324 | break 325 | } 326 | return nil 327 | } 328 | 329 | // WaitUntilECSAllTargetsInService is a helper function which wait until all 330 | // target related to ECS servcies are healthy. 331 | func (client *Client) WaitUntilECSAllTargetsInService(cluster string) error { 332 | serviceArns, err := client.getECSServiceArns(cluster) 333 | if err != nil { 334 | return err 335 | } 336 | 337 | for _, s := range serviceArns { 338 | desiredCount, err := client.getECSDesiredCount(cluster, *s) 339 | if err != nil { 340 | return err 341 | } 342 | if desiredCount == 0 { 343 | // ELBV2.WaitUntilTargetInService wait forever even if desiredCount is 0. 344 | // so we skip it. 345 | continue 346 | } 347 | 348 | targetGroupArns, err := client.getECSTargetGroupArns(cluster, *s) 349 | if err != nil { 350 | return err 351 | } 352 | for _, t := range targetGroupArns { 353 | input := &elbv2.DescribeTargetHealthInput{ 354 | TargetGroupArn: &t, // nolint: gosec 355 | } 356 | err := client.ELBV2.WaitUntilTargetInService(input) 357 | if err != nil { 358 | return errors.Wrapf(err, "WaitUntilTargetInService failed") 359 | } 360 | } 361 | } 362 | 363 | return nil 364 | } 365 | -------------------------------------------------------------------------------- /myaws/elb_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/elb" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // ELBLs describes ELBs. 12 | func (client *Client) ELBLs() error { 13 | params := &elb.DescribeLoadBalancersInput{} 14 | 15 | response, err := client.ELB.DescribeLoadBalancers(params) 16 | if err != nil { 17 | return errors.Wrap(err, "DescribeLoadBalancers failed:") 18 | } 19 | 20 | for _, lb := range response.LoadBalancerDescriptions { 21 | fmt.Fprintln(client.stdout, formatLoadBalancer(lb)) 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func formatLoadBalancer(lb *elb.LoadBalancerDescription) string { 28 | output := []string{ 29 | *lb.LoadBalancerName, 30 | } 31 | 32 | return strings.Join(output[:], "\t") 33 | } 34 | -------------------------------------------------------------------------------- /myaws/elb_ps.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/elb" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // ELBPsOptions customize the behavior of the Ps command. 12 | type ELBPsOptions struct { 13 | LoadBalancerName string 14 | } 15 | 16 | // ELBPs describes ELB's instance health status. 17 | func (client *Client) ELBPs(options ELBPsOptions) error { 18 | params := &elb.DescribeInstanceHealthInput{ 19 | LoadBalancerName: &options.LoadBalancerName, 20 | } 21 | 22 | response, err := client.ELB.DescribeInstanceHealth(params) 23 | if err != nil { 24 | return errors.Wrap(err, "DescribeInstanceHealth failed:") 25 | } 26 | 27 | for _, state := range response.InstanceStates { 28 | fmt.Fprintln(client.stdout, formatELBInstanceState(state)) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func formatELBInstanceState(state *elb.InstanceState) string { 35 | output := []string{ 36 | *state.InstanceId, 37 | *state.State, 38 | } 39 | 40 | return strings.Join(output[:], "\t") 41 | } 42 | -------------------------------------------------------------------------------- /myaws/elbv2_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/elbv2" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // ELBV2Ls describes ELBV2s. 12 | func (client *Client) ELBV2Ls() error { 13 | params := &elbv2.DescribeLoadBalancersInput{} 14 | 15 | response, err := client.ELBV2.DescribeLoadBalancers(params) 16 | if err != nil { 17 | return errors.Wrap(err, "DescribeLoadBalancers failed:") 18 | } 19 | 20 | for _, lb := range response.LoadBalancers { 21 | fmt.Fprintln(client.stdout, formatLoadBalancerV2(lb)) 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func formatLoadBalancerV2(lb *elbv2.LoadBalancer) string { 28 | output := []string{ 29 | *lb.Type, 30 | *lb.LoadBalancerName, 31 | } 32 | 33 | return strings.Join(output[:], "\t") 34 | } 35 | -------------------------------------------------------------------------------- /myaws/elbv2_ps.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/elbv2" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // ELBV2PsOptions customize the behavior of the Ps command. 13 | type ELBV2PsOptions struct { 14 | TargetGroupName string 15 | } 16 | 17 | // ELBV2Ps describes ELBV2's instance health status. 18 | func (client *Client) ELBV2Ps(options ELBV2PsOptions) error { 19 | targetGroupArn, err := client.findELBV2TargetGroup(options.TargetGroupName) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | params := &elbv2.DescribeTargetHealthInput{ 25 | TargetGroupArn: &targetGroupArn, 26 | } 27 | 28 | response, err := client.ELBV2.DescribeTargetHealth(params) 29 | if err != nil { 30 | return errors.Wrap(err, "DescribeTargetHealth failed:") 31 | } 32 | 33 | for _, d := range response.TargetHealthDescriptions { 34 | fmt.Fprintln(client.stdout, formatELBV2TargetHealthDescription(d)) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (client *Client) findELBV2TargetGroup(name string) (string, error) { 41 | params := &elbv2.DescribeTargetGroupsInput{ 42 | Names: []*string{&name}, 43 | } 44 | 45 | response, err := client.ELBV2.DescribeTargetGroups(params) 46 | if err != nil { 47 | return "", errors.Wrap(err, "DescribeTargetGroups failed:") 48 | } 49 | 50 | if len(response.TargetGroups) != 1 { 51 | return "", errors.Errorf("ELBV2.DescribeTargetGroups expects to return 1 group, but found %d groups", len(response.TargetGroups)) 52 | } 53 | 54 | t := response.TargetGroups[0] 55 | return *t.TargetGroupArn, nil 56 | } 57 | 58 | func formatELBV2TargetHealthDescription(d *elbv2.TargetHealthDescription) string { 59 | t := *d.Target 60 | h := *d.TargetHealth 61 | 62 | // If the state is healthy, a reason and a description are not provided. 63 | // All states and reasons are described in the API reference. 64 | // https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/APIReference/API_TargetHealth.html 65 | output := []string{ 66 | *t.Id, 67 | fmt.Sprintf("%d", *t.Port), 68 | fmt.Sprintf("%12s", *h.State), 69 | aws.StringValue(h.Reason), 70 | aws.StringValue(h.Description), 71 | } 72 | 73 | return strings.Join(output[:], "\t") 74 | } 75 | -------------------------------------------------------------------------------- /myaws/iam_user_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/iam" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // IAMUserLs describes IAM users 11 | func (client *Client) IAMUserLs() error { 12 | response, err := client.IAM.ListUsers(&iam.ListUsersInput{}) 13 | if err != nil { 14 | return errors.Wrap(err, "ListUsers failed:") 15 | } 16 | 17 | // TODO: support pagenation 18 | for _, user := range response.Users { 19 | fmt.Fprintln(client.stdout, formatIAMUser(client, user)) 20 | } 21 | return nil 22 | } 23 | 24 | func formatIAMUser(client *Client, user *iam.User) string { 25 | return fmt.Sprintf("%s\t%s\t%s", *user.UserName, client.FormatTime(user.CreateDate), client.FormatTime(user.PasswordLastUsed)) 26 | } 27 | -------------------------------------------------------------------------------- /myaws/iam_user_reset_password.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/awserr" 9 | "github.com/aws/aws-sdk-go/service/iam" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // IAMUserResetPasswordOptions customize the behavior of the IAMUserResetPassword command. 14 | type IAMUserResetPasswordOptions struct { 15 | UserName string 16 | } 17 | 18 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 19 | 20 | func generateRandomPassword(length int) string { 21 | b := make([]rune, length) 22 | for i := range b { 23 | b[i] = letters[rand.Intn(len(letters))] // nolint: gosec 24 | } 25 | return string(b) 26 | } 27 | 28 | // IAMUserResetPassword reset password for IAM user. 29 | func (client *Client) IAMUserResetPassword(options IAMUserResetPasswordOptions) error { 30 | user, err := client.IAMGetUser(options.UserName) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | fmt.Fprintf(client.stdout, "%v\n", user) 36 | 37 | confirm, err := client.Confirmation("Are you sure want to reset password?") 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if !confirm { 43 | // cancel reset password. 44 | fmt.Fprintln(client.stdout, "Cancelled.") 45 | return nil 46 | } 47 | 48 | password := generateRandomPassword(16) 49 | changeRequired := true 50 | 51 | // Check if IAM user has a login profile. 52 | _, err = client.IAM.GetLoginProfile(&iam.GetLoginProfileInput{ 53 | UserName: aws.String(options.UserName), 54 | }) 55 | 56 | if err != nil { 57 | if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoSuchEntity" { 58 | // if IAM user has no login profile, create a login profile with initial password. 59 | err = client.IAMUserCreateLoginProfile(options.UserName, password, changeRequired) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | fmt.Fprintf(client.stdout, "InitialPassword: %s\n", password) 65 | 66 | return nil 67 | } 68 | // unexpected error 69 | return err 70 | } 71 | 72 | // if IAM user has a login profile already, update password. 73 | err = client.IAMUserUpdatePassword(options.UserName, password, changeRequired) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | fmt.Fprintf(client.stdout, "NewPassword: %s\n", password) 79 | 80 | return nil 81 | } 82 | 83 | // IAMGetUser returns IAM user. 84 | func (client *Client) IAMGetUser(username string) (*iam.User, error) { 85 | params := &iam.GetUserInput{ 86 | UserName: &username, 87 | } 88 | 89 | response, err := client.IAM.GetUser(params) 90 | if err != nil { 91 | return nil, errors.Wrap(err, "GetUser failed:") 92 | } 93 | 94 | return response.User, nil 95 | } 96 | 97 | // IAMUserCreateLoginProfile creates a login profile for IAM User with initial password. 98 | func (client *Client) IAMUserCreateLoginProfile(username string, password string, changeRequired bool) error { 99 | params := &iam.CreateLoginProfileInput{ 100 | UserName: &username, 101 | Password: &password, 102 | PasswordResetRequired: &changeRequired, 103 | } 104 | 105 | _, err := client.IAM.CreateLoginProfile(params) 106 | if err != nil { 107 | return errors.Wrap(err, "CreateLoginProfile failed:") 108 | } 109 | 110 | return nil 111 | } 112 | 113 | // IAMUserUpdatePassword updates the password of existing login profile for IAM user. 114 | func (client *Client) IAMUserUpdatePassword(username string, password string, changeRequired bool) error { 115 | params := &iam.UpdateLoginProfileInput{ 116 | UserName: &username, 117 | Password: &password, 118 | PasswordResetRequired: &changeRequired, 119 | } 120 | 121 | _, err := client.IAM.UpdateLoginProfile(params) 122 | if err != nil { 123 | return errors.Wrap(err, "UpdateLoginProfile failed:") 124 | } 125 | 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /myaws/rds_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/rds" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // RDSLsOptions customize the behavior of the Ls command. 12 | type RDSLsOptions struct { 13 | Quiet bool 14 | Fields []string 15 | } 16 | 17 | // RDSLs describes RDSs. 18 | func (client *Client) RDSLs(options RDSLsOptions) error { 19 | params := &rds.DescribeDBInstancesInput{} 20 | 21 | response, err := client.RDS.DescribeDBInstances(params) 22 | if err != nil { 23 | return errors.Wrap(err, "DescribeDBInstances failed:") 24 | } 25 | 26 | for _, db := range response.DBInstances { 27 | fmt.Fprintln(client.stdout, formatDBInstance(client, options, db)) 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func formatDBInstance(client *Client, options RDSLsOptions, db *rds.DBInstance) string { 34 | formatFuncs := map[string]func(client *Client, options RDSLsOptions, db *rds.DBInstance) string{ 35 | "DBInstanceClass": formatRDSDBInstanceClass, 36 | "Engine": formatRDSEngine, 37 | "AllocatedStorage": formatRDSAllocatedStorage, 38 | "StorageType": formatRDSStorageType, 39 | "StorageTypeIops": formatRDSStorageTypeIops, 40 | "DBInstanceIdentifier": formatRDSDBInstanceIdentifier, 41 | "ReadReplicaSource": formatRDSReadReplicaSource, 42 | "InstanceCreateTime": formatRDSInstanceCreateTime, 43 | } 44 | 45 | var outputFields []string 46 | if options.Quiet { 47 | outputFields = []string{"DBInstanceIdentifier"} 48 | } else { 49 | outputFields = options.Fields 50 | } 51 | 52 | output := []string{} 53 | 54 | for _, field := range outputFields { 55 | value := formatFuncs[field](client, options, db) 56 | output = append(output, value) 57 | } 58 | 59 | return strings.Join(output[:], "\t") 60 | } 61 | 62 | func formatRDSDBInstanceIdentifier(_ *Client, _ RDSLsOptions, db *rds.DBInstance) string { 63 | return *db.DBInstanceIdentifier 64 | } 65 | 66 | func formatRDSDBInstanceClass(_ *Client, _ RDSLsOptions, db *rds.DBInstance) string { 67 | if *db.MultiAZ { 68 | return fmt.Sprintf("%s:multi", *db.DBInstanceClass) 69 | } 70 | return fmt.Sprintf("%s:single", *db.DBInstanceClass) 71 | } 72 | 73 | func formatRDSEngine(_ *Client, _ RDSLsOptions, db *rds.DBInstance) string { 74 | return fmt.Sprintf("%-15s", fmt.Sprintf("%s:%s", *db.Engine, *db.EngineVersion)) 75 | } 76 | 77 | func formatRDSAllocatedStorage(_ *Client, _ RDSLsOptions, db *rds.DBInstance) string { 78 | return fmt.Sprintf("%4dGB", *db.AllocatedStorage) 79 | } 80 | 81 | func formatRDSStorageType(_ *Client, _ RDSLsOptions, db *rds.DBInstance) string { 82 | return *db.StorageType 83 | } 84 | 85 | func formatRDSStorageTypeIops(_ *Client, _ RDSLsOptions, db *rds.DBInstance) string { 86 | iops := "-" 87 | if db.Iops != nil { 88 | iops = fmt.Sprint(*db.Iops) 89 | } 90 | 91 | return fmt.Sprintf("%-8s", fmt.Sprintf("%s:%s", *db.StorageType, iops)) 92 | } 93 | 94 | func formatRDSReadReplicaSource(_ *Client, _ RDSLsOptions, db *rds.DBInstance) string { 95 | if db.ReadReplicaSourceDBInstanceIdentifier == nil { 96 | return "source:---" 97 | } 98 | return fmt.Sprintf("source:%s", *db.ReadReplicaSourceDBInstanceIdentifier) 99 | } 100 | 101 | func formatRDSInstanceCreateTime(client *Client, _ RDSLsOptions, db *rds.DBInstance) string { 102 | return client.FormatTime(db.InstanceCreateTime) 103 | } 104 | -------------------------------------------------------------------------------- /myaws/ssm_parameter.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/awsutil" 9 | "github.com/aws/aws-sdk-go/service/ssm" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // FindSSMParameterMetadata returns an array of parameter metadata matching the name. 14 | func (client *Client) FindSSMParameterMetadata(name string) ([]*ssm.ParameterMetadata, error) { 15 | var filter *ssm.ParametersFilter 16 | if len(name) > 0 { 17 | filter = &ssm.ParametersFilter{ 18 | Key: aws.String("Name"), 19 | Values: []*string{ 20 | aws.String(name), 21 | }, 22 | } 23 | } 24 | filters := []*ssm.ParametersFilter{filter} 25 | 26 | input := &ssm.DescribeParametersInput{ 27 | Filters: filters, 28 | } 29 | 30 | // We need to fetch all pages to get results. 31 | // The request timeout should be set in the caller context, 32 | // but for the moment we will create a context here. 33 | metadata := []*ssm.ParameterMetadata{} 34 | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 35 | defer cancel() 36 | err := client.SSM.DescribeParametersPagesWithContext(ctx, 37 | input, 38 | func(page *ssm.DescribeParametersOutput, _ bool) bool { 39 | metadata = append(metadata, page.Parameters...) 40 | return true 41 | }) 42 | 43 | if err != nil { 44 | return nil, errors.Wrap(err, "DescribeParameters failed:") 45 | } 46 | 47 | return metadata, nil 48 | } 49 | 50 | // GetSSMParameters returns an array of parameters at once. 51 | func (client *Client) GetSSMParameters(names []*string, withDecryption bool) ([]*ssm.Parameter, error) { 52 | results := []*ssm.Parameter{} 53 | 54 | // The AWS SSM GetPrameters API can only get 10 parameters at once. 55 | // To get 10 or more parameters, we need to call API multiple time. 56 | // https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameters.html 57 | chunkSize := 10 58 | 59 | for i := 0; i < len(names); i += chunkSize { 60 | end := i + chunkSize 61 | if end > len(names) { 62 | end = len(names) 63 | } 64 | chunk := names[i:end] 65 | resultsPerChunk, err := client.getSSMParametersPerChunk(chunk, withDecryption) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | results = append(results, resultsPerChunk...) 71 | } 72 | 73 | return results, nil 74 | } 75 | 76 | // getSSMParametersPerChunk returns an array of parameters per chunk. 77 | func (client *Client) getSSMParametersPerChunk(names []*string, withDecryption bool) ([]*ssm.Parameter, error) { 78 | input := &ssm.GetParametersInput{ 79 | Names: names, 80 | WithDecryption: aws.Bool(withDecryption), 81 | } 82 | 83 | response, err := client.SSM.GetParameters(input) 84 | if err != nil { 85 | return nil, errors.Wrap(err, "GetParameters failed:") 86 | } 87 | 88 | if len(response.InvalidParameters) > 0 { 89 | return nil, errors.Errorf("InvalidParameters: %v", awsutil.Prettify(response.InvalidParameters)) 90 | } 91 | 92 | return response.Parameters, nil 93 | } 94 | 95 | // GetParametersByPath returns a list of parameters that start with the specified path. 96 | func (client *Client) GetParametersByPath(path *string, withDecryption bool) ([]*ssm.Parameter, error) { 97 | input := &ssm.GetParametersByPathInput{ 98 | Path: path, 99 | Recursive: aws.Bool(true), 100 | WithDecryption: aws.Bool(withDecryption), 101 | } 102 | 103 | var parameters []*ssm.Parameter 104 | err := client.SSM.GetParametersByPathPages(input, 105 | func(page *ssm.GetParametersByPathOutput, _ bool) bool { 106 | parameters = append(parameters, page.Parameters...) 107 | return true 108 | }) 109 | if err != nil { 110 | return nil, errors.Wrap(err, "GetParametersByPath failed:") 111 | } 112 | return parameters, nil 113 | } 114 | -------------------------------------------------------------------------------- /myaws/ssm_parameter_del.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/service/ssm" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // SSMParameterDelOptions customize the behavior of the ParameterDel command. 10 | type SSMParameterDelOptions struct { 11 | Name string 12 | } 13 | 14 | // SSMParameterDel deletes SSM parameter. 15 | func (client *Client) SSMParameterDel(options SSMParameterDelOptions) error { 16 | input := &ssm.DeleteParameterInput{ 17 | Name: aws.String(options.Name), 18 | } 19 | 20 | _, err := client.SSM.DeleteParameter(input) 21 | if err != nil { 22 | return errors.Wrap(err, "DeleteParameters failed:") 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /myaws/ssm_parameter_env.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/ssm" 8 | ) 9 | 10 | // SSMParameterEnvOptions customize the behavior of the ParameterEnv command. 11 | type SSMParameterEnvOptions struct { 12 | Name string 13 | DockerFormat bool 14 | QuoteValue bool 15 | } 16 | 17 | // SSMParameterEnv prints SSM parameters as a list of environment variables. 18 | func (client *Client) SSMParameterEnv(options SSMParameterEnvOptions) error { 19 | var parameters []*ssm.Parameter 20 | // Since GetSSMParameters does not have a hierarchy, it is necessary to 21 | // retrieve all keys at first, then filter the target keys. To do this, we 22 | // need to call the DescribeParameters API multiple times, but its rate limit 23 | // seems to be quite low and undocumented. 24 | // https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_DescribeParameters.html 25 | // This causes a rate limit exception if your SSM Parameter store have large 26 | // number of keys. 27 | // https://github.com/minamijoyo/myaws/issues/31 28 | // 29 | // The GetParametersByPath API has a hierarchy separated by '/'. This means 30 | // it has less impact to the rate limit. 31 | // https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParametersByPath.html 32 | // 33 | // So we use it if the path seems to have a hierarchy and fall back to 34 | // the original behavior if not. 35 | if strings.HasPrefix(options.Name, "/") { 36 | var err error 37 | parameters, err = client.GetParametersByPath(&options.Name, true) 38 | if err != nil { 39 | return err 40 | } 41 | } else { 42 | metadata, err := client.FindSSMParameterMetadata(options.Name) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | names := []*string{} 48 | for _, m := range metadata { 49 | names = append(names, m.Name) 50 | } 51 | 52 | parameters, err = client.GetSSMParameters(names, true) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | output := []string{} 58 | for _, parameter := range parameters { 59 | output = append(output, formatSSMParameterAsEnv(parameter, options.Name, options.DockerFormat, options.QuoteValue)) 60 | } 61 | 62 | fmt.Fprint(client.stdout, strings.Join(output[:], " ")) 63 | return nil 64 | } 65 | 66 | func formatSSMParameterAsEnv(parameter *ssm.Parameter, prefix string, dockerFormat bool, quoteValue bool) string { 67 | // Drop prefix and get suffix as a key name. 68 | suffix := strings.Replace(*parameter.Name, prefix, "", 1) 69 | // if first character is period, then drop it. 70 | if suffix[0] == '.' || suffix[0] == '/' { 71 | suffix = suffix[1:] 72 | } 73 | // Flatten period and slash to underscore for nested keys. 74 | flattenDot := strings.Replace(suffix, ".", "_", -1) 75 | flattenSlash := strings.Replace(flattenDot, "/", "_", -1) 76 | // The name of environment variable should be uppercase. 77 | name := strings.ToUpper(flattenSlash) 78 | outputOptionName := "" 79 | 80 | if dockerFormat { 81 | // Output in docker environment variables format such as -e KEY=VALUE 82 | outputOptionName = "-e " 83 | } 84 | 85 | value := *parameter.Value 86 | if quoteValue { 87 | value = "'" + *parameter.Value + "'" 88 | } 89 | return fmt.Sprintf("%s%s=%s", outputOptionName, name, value) 90 | } 91 | -------------------------------------------------------------------------------- /myaws/ssm_parameter_get.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/ssm" 7 | ) 8 | 9 | // SSMParameterGetOptions customize the behavior of the ParameterGet command. 10 | type SSMParameterGetOptions struct { 11 | Names []*string 12 | WithDecryption bool 13 | } 14 | 15 | // SSMParameterGet get values from SSM parameter store with KMS decryption. 16 | func (client *Client) SSMParameterGet(options SSMParameterGetOptions) error { 17 | parameters, err := client.GetSSMParameters(options.Names, options.WithDecryption) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | for _, parameter := range parameters { 23 | fmt.Fprintln(client.stdout, formatSSMParameter(parameter)) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func formatSSMParameter(parameter *ssm.Parameter) string { 30 | return *parameter.Value 31 | } 32 | -------------------------------------------------------------------------------- /myaws/ssm_parameter_ls.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/ssm" 7 | ) 8 | 9 | // SSMParameterLsOptions customize the behavior of the ParameterLs command. 10 | type SSMParameterLsOptions struct { 11 | Name string 12 | } 13 | 14 | // SSMParameterLs describes SSM parameters. 15 | func (client *Client) SSMParameterLs(options SSMParameterLsOptions) error { 16 | metadata, err := client.FindSSMParameterMetadata(options.Name) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | for _, m := range metadata { 22 | fmt.Fprintln(client.stdout, formatSSMParameterMetadata(m)) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func formatSSMParameterMetadata(m *ssm.ParameterMetadata) string { 29 | keyid := formatSSMParameterKeyID(m) 30 | return fmt.Sprintf("%s\t%s\t%s", *m.Name, *m.Type, keyid) 31 | } 32 | 33 | func formatSSMParameterKeyID(m *ssm.ParameterMetadata) string { 34 | if m.KeyId == nil { 35 | return "" 36 | } 37 | return *m.KeyId 38 | } 39 | -------------------------------------------------------------------------------- /myaws/ssm_parameter_put.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/ssm" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // SSMParameterPutOptions customize the behavior of the ParameterPut command. 9 | type SSMParameterPutOptions struct { 10 | Name string 11 | Value string 12 | KeyID string 13 | } 14 | 15 | // SSMParameterPut put value to SSM parameter store with KMS encryption. 16 | func (client *Client) SSMParameterPut(options SSMParameterPutOptions) error { 17 | overwrite := true 18 | 19 | var parameterType string 20 | var keyID *string 21 | if options.KeyID != "" { 22 | parameterType = "SecureString" 23 | keyID = &options.KeyID 24 | } else { 25 | parameterType = "String" 26 | // keyID must be nil when type is String. 27 | keyID = nil 28 | } 29 | 30 | input := &ssm.PutParameterInput{ 31 | Name: &options.Name, 32 | Value: &options.Value, 33 | KeyId: keyID, 34 | Type: ¶meterType, 35 | Overwrite: &overwrite, 36 | } 37 | 38 | _, err := client.SSM.PutParameter(input) 39 | if err != nil { 40 | return errors.Wrap(err, "PutParameter failed:") 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /myaws/sts_id.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/service/sts" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // STSID gets caller identity. 11 | func (client *Client) STSID() error { 12 | response, err := client.STS.GetCallerIdentity(&sts.GetCallerIdentityInput{}) 13 | if err != nil { 14 | return errors.Wrap(err, "GetCallerIdentity failed:") 15 | } 16 | 17 | fmt.Fprintln(client.stdout, formatSTSID(response)) 18 | return nil 19 | } 20 | 21 | func formatSTSID(id *sts.GetCallerIdentityOutput) string { 22 | return fmt.Sprintf("Account: %s\nUserId: %s\nArn: %s", 23 | *id.Account, *id.UserId, *id.Arn) 24 | } 25 | -------------------------------------------------------------------------------- /myaws/time.go: -------------------------------------------------------------------------------- 1 | package myaws 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dustin/go-humanize" 7 | ) 8 | 9 | // FormatTime returns a localized time string. 10 | // If humanize flag is true, it is converted to human frendly representation. 11 | func (client *Client) FormatTime(t *time.Time) string { 12 | if t == nil { 13 | return "" 14 | } 15 | 16 | location, err := time.LoadLocation(client.timezone) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | if client.humanize { 22 | // humanized format 23 | return humanize.Time(t.In(location)) 24 | } 25 | 26 | // default format 27 | return t.In(location).Format("2006-01-02 15:04:05") 28 | } 29 | --------------------------------------------------------------------------------