├── .codecov.yml ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── awsec2 ├── client.go └── client_test.go ├── cmd └── lsec2 │ ├── cli │ ├── commands.go │ └── flags.go │ ├── main.go │ └── version │ ├── version.go │ └── version_test.go ├── go.mod ├── go.sum ├── printer ├── instance_info.go ├── instance_info_test.go ├── printer.go └── printer_test.go ├── scripts ├── _ldflags.sh ├── _packages.sh ├── _prepare.sh ├── _version.sh ├── build.sh ├── ci-test.sh └── install.sh └── tools └── tools.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: 50..100 9 | 10 | status: 11 | project: 12 | default: 13 | target: auto 14 | threshold: 5% 15 | patch: 16 | default: 17 | target: auto 18 | threshold: 5% 19 | changes: off 20 | 21 | parsers: 22 | gcov: 23 | branch_detection: 24 | conditional: yes 25 | loop: yes 26 | method: no 27 | macro: no 28 | 29 | comment: 30 | layout: "header, diff" 31 | behavior: default 32 | require_changes: no 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**/*.md' 9 | - 'LICENSE' 10 | - '**/.gitignore' 11 | pull_request: 12 | paths-ignore: 13 | - '**/*.md' 14 | - 'LICENSE' 15 | - '**/.gitignore' 16 | 17 | jobs: 18 | test: 19 | name: Test with Go version ${{ matrix.go }} 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [ 'ubuntu-latest' ] 24 | go: [ '1.11', '1.12', '1.13' ] 25 | 26 | steps: 27 | - name: Set up Go ${{ matrix.go }} 28 | id: setup-go 29 | uses: actions/setup-go@v2-beta 30 | with: 31 | go-version: ${{ matrix.go }} 32 | 33 | - name: Check out repositories 34 | id: checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Cache dependencies and tools 38 | id: cache 39 | uses: actions/cache@v1 40 | with: 41 | path: ~/go/pkg/mod 42 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 43 | restore-keys: | 44 | ${{ runner.os }}-go- 45 | 46 | - name: Download dependency go modules 47 | id: mod-dl 48 | run: make mod-dl 49 | 50 | - name: Install golint on module-aware mode 51 | id: mod-golint-install 52 | run: GOBIN=${GITHUB_WORKSPACE}/bin make mod-golint-install 53 | 54 | - name: Add GITHUB_WORKSPACE/bin into PATH 55 | id: add-gobin-path 56 | run: echo "::add-path::${GITHUB_WORKSPACE}/bin" 57 | 58 | - name: Run tests and linters 59 | id: ci 60 | run: make ci 61 | 62 | - name: Send a coverage to Codecov 63 | id: codecov 64 | uses: codecov/codecov-action@v1 65 | with: 66 | file: ./coverage.txt 67 | flags: unittests 68 | name: codecov 69 | fail_ci_if_error: false 70 | 71 | validate-goreleaser: 72 | name: Validate .goreleaser.yml 73 | runs-on: ${{ matrix.os }} 74 | strategy: 75 | matrix: 76 | os: [ 'ubuntu-latest' ] 77 | go: [ '1.13' ] 78 | 79 | steps: 80 | - name: Set up Go ${{ matrix.go }} 81 | id: setup-go 82 | uses: actions/setup-go@v2-beta 83 | with: 84 | go-version: ${{ matrix.go }} 85 | 86 | - name: Check out repositories 87 | id: checkout 88 | uses: actions/checkout@v2 89 | 90 | - name: Set GOVERSION env 91 | id: set-goversion-env 92 | run: echo "::set-env name=GOVERSION::$(go version)" 93 | 94 | - name: Validate .goreleaser.yml with goreleaser-action 95 | uses: goreleaser/goreleaser-action@v1 96 | with: 97 | version: latest 98 | args: release --snapshot --skip-publish --rm-dist --debug 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 101 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "!*" 7 | tags: 8 | - "v[0-9]+.[0-9]+.[0-9]+" 9 | 10 | jobs: 11 | goreleaser: 12 | name: GoReleaser with Go version ${{ matrix.go }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ 'ubuntu-latest' ] 17 | go: [ '1.13' ] 18 | 19 | steps: 20 | - name: Set up Go ${{ matrix.go }} 21 | id: setup-go 22 | uses: actions/setup-go@v2-beta 23 | with: 24 | go-version: ${{ matrix.go }} 25 | 26 | - name: Check out code into the Go module directory 27 | id: checkout 28 | uses: actions/checkout@v2 29 | 30 | - name: Set GOVERSION env 31 | id: set-goversion-env 32 | run: echo "::set-env name=GOVERSION::$(go version)" 33 | 34 | - name: Run GoReleaser 35 | uses: goreleaser/goreleaser-action@v1 36 | with: 37 | version: latest 38 | args: release --rm-dist 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.log 4 | .test 5 | .profile 6 | vendor 7 | 8 | bin 9 | releases 10 | pkg 11 | *formula.rb 12 | coverage.txt 13 | 14 | *.test 15 | 16 | .envrc 17 | dist/ 18 | vendor/ 19 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # you may remove this if you don't use vgo 6 | - go mod download 7 | 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | - GO111MODULE=on 12 | main: ./cmd/lsec2/main.go 13 | flags: 14 | - -a 15 | - -tags 16 | - netgo 17 | - -installsuffix 18 | - netgo 19 | ldflags: 20 | - -w 21 | - -s 22 | - -extldflags "-static" 23 | - -X "github.com/goldeneggg/lsec2/cmd/lsec2/cli.BuildDate={{.Date}}" 24 | - -X "github.com/goldeneggg/lsec2/cmd/lsec2/cli.BuildCommit={{.ShortCommit}}" 25 | - -X "github.com/goldeneggg/lsec2/cmd/lsec2/cli.GoVersion={{.Env.GOVERSION}}" 26 | goos: 27 | - linux 28 | - freebsd 29 | - darwin 30 | - windows 31 | goarch: 32 | - amd64 33 | - 386 34 | ignore: 35 | - goos: darwin 36 | goarch: 386 37 | - goos: windows 38 | goarch: 386 39 | 40 | archives: 41 | - 42 | format_overrides: 43 | - goos: windows 44 | format: zip 45 | 46 | checksum: 47 | name_template: 'checksums.txt' 48 | 49 | snapshot: 50 | name_template: "{{ .Tag }}-next" 51 | 52 | changelog: 53 | sort: asc 54 | filters: 55 | exclude: 56 | - '^docs:' 57 | - '^test:' 58 | 59 | brews: 60 | - 61 | github: 62 | owner: goldeneggg 63 | name: homebrew-tap 64 | commit_author: 65 | name: Fuminori Sakamoto 66 | email: jpshadowapps@gmail.com 67 | folder: Formula 68 | homepage: 'https://github.com/goldeneggg/lsec2' 69 | description: 'List view of aws ec2 instances' 70 | test: | 71 | system '#{bin}/lsec2 -v' 72 | 73 | # Note: 74 | # "brew install rpm" is required on OS X 75 | nfpms: 76 | - 77 | license: MIT 78 | maintainer: Fuminori Sakamoto 79 | homepage: https://github.com/goldeneggg/lsec2 80 | formats: 81 | - deb 82 | - rpm 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Guideline summary 2 | - Fork it 3 | - Create your feature branch (git checkout -b my-new-feature) 4 | - Commit your changes (git commit -am 'Add some feature') 5 | - Push to the branch (git push origin my-new-feature) 6 | - Create new Pull Request 7 | 8 | ## Install Go 9 | Please setup Golang environment, follow [Getting Started \- The Go Programming Language](https://golang.org/doc/instal) 10 | 11 | __(required version 1.11 or later)__ 12 | 13 | ## Setup 14 | * Fork [lsec2](https://github.com/goldeneggg/lsec2/fork) 15 | * Clone forked repositry in your machine. 16 | * Run `make` 17 | 18 | ```sh 19 | $ git clone 20 | $ cd lsec2 21 | $ git remote add upstream https://github.com/goldeneggg/lsec2 22 | 23 | # 1st go build 24 | # At first, it will find and download dependency libraries on [Module-aware mode](https://golang.org/cmd/go/#hdr-Module_aware_go_get) before building. 25 | $ make 26 | go: finding github.com/mattn/go-colorable v0.1.1 27 | go: finding github.com/aws/aws-sdk-go v1.17.14 28 | : 29 | : 30 | ``` 31 | 32 | ## Build 33 | 34 | * Run `make` 35 | 36 | ## Run tests 37 | 38 | * Run `make test` 39 | 40 | ## install 41 | 42 | * Run `make install` 43 | 44 | ## Validate codes 45 | 46 | * Run `make validate` 47 | 48 | ## Add some new dependency libraries 49 | ___On Module-aware mode___ 50 | 51 | 1. Edit your code and add import library. 52 | 1. Run `make rebuild` or `make test` 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Fuminori Sakamoto 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 := lsec2 2 | PROF_DIR := ./.profile 3 | PKG_AWS_SDK_GO := github.com/aws/aws-sdk-go 4 | PKG_URFAVE_CLI := github.com/urfave/cli 5 | MOD_QUERY_AWS_SDK_GO := select instance => SSH to selected instance 194 | 195 | ```sh 196 | # add function to your .bashrc or .bash_profile or other shell dotfile 197 | $ vi YOUR_DOTFILE 198 | 199 | lssh () { 200 | IP=$(lsec2 $@ | peco | awk -F "\t" '{print $2}') 201 | if [ $? -eq 0 -a "${IP}" != "" ] 202 | then 203 | ssh ${IP} 204 | fi 205 | } 206 | 207 | 208 | # load dotfile 209 | $ source YOUR_DOTFILE 210 | 211 | # shortcut "lsec2 OPTIONS TAG_FILTERS" => "ssh PRIVATE_IP" 212 | $ lssh TagName1=tagvalue1 213 | ``` 214 | 215 | ### With gat 216 | [gat](https://github.com/goldeneggg/gat) is a file posting tool to various services like `cat` command. 217 | 218 | * Example: print instances => share your slack 219 | 220 | ```sh 221 | # shortcut "lsec2 OPTIONS TAG_FILTERS" => copy this results to your slack channel 222 | $ lsec2 TagName1=tagvalue1 | gat slack 223 | ``` 224 | 225 | ## Contribute 226 | Please follow [Contributor's Guide](CONTRIBUTING.md) 227 | 228 | ## Contact 229 | 230 | Bugs: [issues](https://github.com/goldeneggg/lsec2/issues) 231 | 232 | 233 | ## ChangeLog 234 | [CHANGELOG](CHANGELOG.md) file for details. 235 | 236 | 237 | ## License 238 | 239 | [LICENSE](LICENSE) file for details. 240 | 241 | ## Special Thanks 242 | [@sugitak](https://github.com/sugitak) 243 | -------------------------------------------------------------------------------- /awsec2/client.go: -------------------------------------------------------------------------------- 1 | package awsec2 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/ec2" 11 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 12 | ) 13 | 14 | const ( 15 | tagFilterPrefix = "tag:" 16 | tagPairSeparator = "=" 17 | tagValueSeparator = "," 18 | 19 | defaultTagValuesCap = 3 20 | ) 21 | 22 | // Client is attributes definition for filtering ec2 instances 23 | type Client struct { 24 | ec2iface.EC2API 25 | StateName string 26 | Tags []string 27 | } 28 | 29 | // NewClient returns a new DefaultClient 30 | func NewClient(region, state, profile string, tags []string) (*Client, error) { 31 | return NewClientWithEC2API(region, state, profile, tags, nil) 32 | } 33 | 34 | // NewClientWithEC2API returns a new Client with assigned EC2API variable 35 | func NewClientWithEC2API(region, state, profile string, tags []string, c interface{}) (*Client, error) { 36 | client := new(Client) 37 | 38 | client.StateName = state 39 | client.Tags = tags 40 | 41 | if c != nil { 42 | if ec2, ok := c.(ec2iface.EC2API); ok { 43 | client.EC2API = ec2 44 | } else { 45 | return nil, fmt.Errorf("arg %#v does not implement ec2.EC2API methods", c) 46 | } 47 | } else { 48 | client.EC2API = defaultEC2Client(region, profile) 49 | } 50 | 51 | return client, nil 52 | } 53 | 54 | // EC2Instances gets filtered EC2 instances 55 | func (client *Client) EC2Instances() ([]*ec2.Instance, error) { 56 | output, err := client.EC2API.DescribeInstances(client.buildFilter()) 57 | if err != nil { 58 | return nil, fmt.Errorf("aws describe instances error: %v", err) 59 | } 60 | 61 | var res []*ec2.Instance 62 | for _, r := range output.Reservations { 63 | res = append(res, r.Instances...) 64 | } 65 | 66 | return res, nil 67 | } 68 | 69 | func defaultEC2Client(region string, profile string) ec2iface.EC2API { 70 | config := aws.NewConfig().WithCredentialsChainVerboseErrors(true) 71 | if region != "" { 72 | config = config.WithRegion(region) 73 | } 74 | 75 | sessOpts := session.Options{ 76 | Config: *config, 77 | AssumeRoleTokenProvider: stscreds.StdinTokenProvider, 78 | // Load ~/.aws/config regardless of whether AWS_SDK_LOAD_CONFIG env is set 79 | SharedConfigState: session.SharedConfigEnable, 80 | } 81 | if profile != "" { 82 | sessOpts.Profile = profile 83 | } 84 | 85 | return ec2.New(session.Must(session.NewSessionWithOptions(sessOpts))) 86 | } 87 | 88 | func (client *Client) buildFilter() *ec2.DescribeInstancesInput { 89 | var filters []*ec2.Filter 90 | 91 | // client.tags is separated key-value pair by "=", and values are separated by ","(comma) 92 | // ex. "Name=Value" 93 | // ex. "Name=Value1,Value2" 94 | for _, tag := range client.Tags { 95 | kv := strings.Split(tag, tagPairSeparator) 96 | name := aws.String(tagFilterPrefix + kv[0]) 97 | values := make([]*string, 0, defaultTagValuesCap) 98 | for _, v := range strings.Split(kv[1], tagValueSeparator) { 99 | values = append(values, aws.String(v)) 100 | } 101 | 102 | filters = append(filters, &ec2.Filter{Name: name, Values: values}) 103 | } 104 | 105 | if len(filters) == 0 { 106 | return nil 107 | } 108 | 109 | return &ec2.DescribeInstancesInput{Filters: filters} 110 | } 111 | -------------------------------------------------------------------------------- /awsec2/client_test.go: -------------------------------------------------------------------------------- 1 | package awsec2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 9 | 10 | . "github.com/goldeneggg/lsec2/awsec2" 11 | ) 12 | 13 | var ( 14 | dummyInstances = []*ec2.Instance{ 15 | &ec2.Instance{ 16 | InstanceId: aws.String("i-xxxxxxxxx1"), 17 | PrivateIpAddress: aws.String("10.0.0.1"), 18 | PublicIpAddress: aws.String("1.2.3.4"), 19 | InstanceType: aws.String("t1.micro"), 20 | State: &ec2.InstanceState{ 21 | Name: aws.String("running"), 22 | }, 23 | }, 24 | &ec2.Instance{ 25 | InstanceId: aws.String("i-xxxxxxxxx2"), 26 | PrivateIpAddress: aws.String("10.0.0.2"), 27 | PublicIpAddress: aws.String("1.2.3.5"), 28 | InstanceType: aws.String("t2.micro"), 29 | State: &ec2.InstanceState{ 30 | Name: aws.String("stopped"), 31 | }, 32 | }, 33 | &ec2.Instance{ 34 | InstanceId: aws.String("i-xxxxxxxxx9"), 35 | PrivateIpAddress: aws.String("10.0.0.9"), 36 | PublicIpAddress: aws.String("1.2.3.9"), 37 | InstanceType: aws.String("t2.large"), 38 | State: &ec2.InstanceState{ 39 | Name: aws.String("stopped"), 40 | }, 41 | }, 42 | } 43 | ) 44 | 45 | type mockEC2API struct { 46 | ec2iface.EC2API 47 | } 48 | 49 | // override for mock 50 | func (mock *mockEC2API) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { 51 | instancesOutput := &ec2.DescribeInstancesOutput{ 52 | Reservations: []*ec2.Reservation{ 53 | &ec2.Reservation{ 54 | Instances: []*ec2.Instance{dummyInstances[0], dummyInstances[1]}, 55 | }, 56 | &ec2.Reservation{ 57 | Instances: []*ec2.Instance{dummyInstances[2]}, 58 | }, 59 | }, 60 | } 61 | 62 | return instancesOutput, nil 63 | } 64 | 65 | func TestNewClient(t *testing.T) { 66 | cases := []struct { 67 | region string 68 | state string 69 | profile string 70 | tags []string 71 | expected *Client 72 | }{ 73 | { 74 | region: "ap-northeast-1", 75 | state: "running", 76 | profile: "", 77 | tags: []string{"Tag1=Value1", "Tag2=Value2"}, 78 | expected: &Client{ 79 | StateName: "running", 80 | Tags: []string{"Tag1=Value1", "Tag2=Value2"}, 81 | }, 82 | }, 83 | { 84 | region: "us-east-1", 85 | state: "stopped", 86 | profile: "test", 87 | tags: []string{}, 88 | expected: &Client{ 89 | StateName: "stopped", 90 | Tags: []string{}, 91 | }, 92 | }, 93 | } 94 | 95 | for _, c := range cases { 96 | client, err := NewClient(c.region, c.state, c.profile, c.tags) 97 | if err != nil { 98 | t.Errorf("error occured. err: %#v", err) 99 | } 100 | 101 | if client.StateName != c.expected.StateName { 102 | t.Errorf("expected: %#v, but actual: %#v", c.expected.StateName, client.StateName) 103 | } 104 | for i, tag := range client.Tags { 105 | if tag != c.expected.Tags[i] { 106 | t.Errorf("expected: %#v, but actual: %#v", c.expected.Tags[i], tag) 107 | } 108 | } 109 | if _, ok := client.EC2API.(*ec2.EC2); !ok { 110 | t.Errorf("expected: *ec2.EC2, but actual: %#v", client.EC2API) 111 | } 112 | } 113 | } 114 | 115 | func TestNewClientEC2API(t *testing.T) { 116 | mockClient := &mockEC2API{} 117 | 118 | cases := []struct { 119 | region string 120 | state string 121 | profile string 122 | tags []string 123 | ec2Client ec2iface.EC2API 124 | expected *Client 125 | }{ 126 | { 127 | region: "ap-northeast-1", 128 | state: "running", 129 | profile: "", 130 | tags: []string{"Tag1=Value1", "Tag2=Value2"}, 131 | ec2Client: mockClient, 132 | expected: &Client{ 133 | EC2API: mockClient, 134 | StateName: "running", 135 | Tags: []string{"Tag1=Value1", "Tag2=Value2"}, 136 | }, 137 | }, 138 | { 139 | region: "us-east-1", 140 | state: "stopped", 141 | profile: "test", 142 | tags: []string{}, 143 | ec2Client: mockClient, 144 | expected: &Client{ 145 | EC2API: mockClient, 146 | StateName: "stopped", 147 | Tags: []string{}, 148 | }, 149 | }, 150 | } 151 | 152 | for _, c := range cases { 153 | client, err := NewClientWithEC2API(c.region, c.state, c.profile, c.tags, c.ec2Client) 154 | if err != nil { 155 | t.Errorf("error occured. err: %#v", err) 156 | } 157 | 158 | if client.StateName != c.expected.StateName { 159 | t.Errorf("expected: %#v, but actual: %#v", c.expected.StateName, client.StateName) 160 | } 161 | for i, tag := range client.Tags { 162 | if tag != c.expected.Tags[i] { 163 | t.Errorf("expected: %#v, but actual: %#v", c.expected.Tags[i], tag) 164 | } 165 | } 166 | if client.EC2API != c.expected.EC2API { 167 | t.Errorf("expected: %#v, but actual: %#v", c.expected.EC2API, client.EC2API) 168 | } 169 | } 170 | } 171 | 172 | func TestEC2Instances(t *testing.T) { 173 | cases := []struct { 174 | client *Client 175 | expected []*ec2.Instance 176 | }{ 177 | { 178 | client: &Client{ 179 | EC2API: &mockEC2API{}, 180 | StateName: "running", 181 | Tags: []string{"Role=roleA,roleB", "Service=serviceX"}, 182 | }, 183 | expected: dummyInstances, 184 | }, 185 | { 186 | client: &Client{ 187 | EC2API: &mockEC2API{}, 188 | StateName: "stopped", 189 | }, 190 | expected: dummyInstances, 191 | }, 192 | } 193 | 194 | for _, c := range cases { 195 | insts, err := c.client.EC2Instances() 196 | 197 | if err != nil { 198 | t.Errorf("error occured. err: %#v", err) 199 | } 200 | 201 | if len(c.expected) != len(insts) { 202 | t.Errorf("not same instances length. expected: %#v, but actual: %#v", len(c.expected), len(insts)) 203 | } 204 | 205 | for i, inst := range insts { 206 | if inst.InstanceId != c.expected[i].InstanceId { 207 | t.Errorf("expected: %#v, but actual: %#v", c.expected[i].InstanceId, inst.InstanceId) 208 | } 209 | if inst.PrivateIpAddress != c.expected[i].PrivateIpAddress { 210 | t.Errorf("expected: %#v, but actual: %#v", c.expected[i].PrivateIpAddress, inst.PrivateIpAddress) 211 | } 212 | if inst.PublicIpAddress != c.expected[i].PublicIpAddress { 213 | t.Errorf("expected: %#v, but actual: %#v", c.expected[i].PublicIpAddress, inst.PublicIpAddress) 214 | } 215 | if inst.InstanceType != c.expected[i].InstanceType { 216 | t.Errorf("expected: %#v, but actual: %#v", c.expected[i].InstanceType, inst.InstanceType) 217 | } 218 | if inst.State.Name != c.expected[i].State.Name { 219 | t.Errorf("expected: %#v, but actual: %#v", c.expected[i].State.Name, inst.State.Name) 220 | } 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /cmd/lsec2/cli/commands.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/goldeneggg/lsec2/awsec2" 7 | "github.com/goldeneggg/lsec2/cmd/lsec2/version" 8 | "github.com/goldeneggg/lsec2/printer" 9 | "github.com/urfave/cli" 10 | ) 11 | 12 | var ( 13 | // BuildDate have a built date 14 | BuildDate string 15 | // BuildCommit have a latest commit hash 16 | BuildCommit string 17 | // GoVersion have a go version number used for build 18 | GoVersion string 19 | ) 20 | 21 | // Run run a cli command 22 | func Run(args []string) error { 23 | app := cli.NewApp() 24 | 25 | app.Name = "lsec2" 26 | app.Author = "goldeneggg" 27 | app.Version = version.VERSION 28 | app.Usage = "Listing information of aws ec2 instances" 29 | app.Flags = append(ec2Flags, printFlags...) 30 | app.Action = action 31 | 32 | return app.Run(args) 33 | } 34 | 35 | func action(c *cli.Context) error { 36 | if c.IsSet("show-build") { 37 | showBuildInfo(c) 38 | return nil 39 | } 40 | 41 | pr := newPrinter(c) 42 | client, err := newClient(c) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if err := pr.PrintAll(client); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func newPrinter(c *cli.Context) *printer.Printer { 55 | return printer.NewPrinter(c.String("d"), c.Bool("H"), c.Bool("p"), c.Bool("c"), nil) 56 | } 57 | 58 | func newClient(c *cli.Context) (*awsec2.Client, error) { 59 | return awsec2.NewClient(c.String("region"), c.String("s"), c.String("profile"), c.Args()) 60 | } 61 | 62 | func showBuildInfo(c *cli.Context) { 63 | fmt.Printf("build-date: %v\n", BuildDate) 64 | fmt.Printf("build-commit: %v\n", BuildCommit) 65 | fmt.Printf("go-version: %v\n", GoVersion) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/lsec2/cli/flags.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/urfave/cli" 5 | ) 6 | 7 | var ( 8 | // command line options for getting ec2 instances 9 | ec2Flags = []cli.Flag{ 10 | cli.StringFlag{ 11 | Name: "region", 12 | Usage: "assign target region", 13 | }, 14 | cli.StringFlag{ 15 | Name: "state, s", 16 | Usage: "filter only assigned state", 17 | }, 18 | } 19 | 20 | // command line options for printing informations 21 | printFlags = []cli.Flag{ 22 | cli.BoolFlag{ 23 | Name: "show-build", 24 | Usage: "show build info", 25 | }, 26 | cli.BoolFlag{ 27 | Name: "print-header, H", 28 | Usage: "print list header", 29 | }, 30 | cli.BoolFlag{ 31 | Name: "private-ip, p", 32 | Usage: "print only private ip", 33 | }, 34 | cli.BoolFlag{ 35 | Name: "with-color, c", 36 | Usage: "print state with color", 37 | }, 38 | cli.StringFlag{ 39 | Name: "delimiter, d", 40 | Usage: "column delemeter for printed informations", 41 | Value: "\t", 42 | }, 43 | cli.StringFlag{ 44 | Name: "profile", 45 | Usage: "AWS config profile name", 46 | }, 47 | } 48 | ) 49 | -------------------------------------------------------------------------------- /cmd/lsec2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/goldeneggg/lsec2/cmd/lsec2/cli" 8 | ) 9 | 10 | const ( 11 | exitStsOk = iota 12 | exitStsNg 13 | ) 14 | 15 | var ( 16 | sts int 17 | ) 18 | 19 | func main() { 20 | defer finalize() 21 | 22 | if err := cli.Run(os.Args); err != nil { 23 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 24 | sts = exitStsNg 25 | } 26 | } 27 | 28 | func finalize() { 29 | if err := recover(); err != nil { 30 | fmt.Fprintf(os.Stderr, "Panic: %v\n", err) 31 | sts = exitStsNg 32 | } 33 | 34 | os.Exit(sts) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/lsec2/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // VERSION says my version number 4 | const VERSION = "0.2.11" 5 | -------------------------------------------------------------------------------- /cmd/lsec2/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/goldeneggg/lsec2/cmd/lsec2/version" 7 | ) 8 | 9 | func TestVersion(t *testing.T) { 10 | exp := "0.2.11" 11 | 12 | if VERSION != exp { 13 | t.Errorf("expected: %#v, but actual: %#v", exp, VERSION) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/goldeneggg/lsec2 2 | 3 | require ( 4 | github.com/aws/aws-sdk-go v1.29.29 5 | github.com/davecgh/go-spew v1.1.1 // indirect 6 | github.com/fatih/color v1.7.0 7 | github.com/mattn/go-colorable v0.1.1 // indirect 8 | github.com/mattn/go-isatty v0.0.6 // indirect 9 | github.com/urfave/cli v1.22.3 10 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b 11 | ) 12 | 13 | go 1.13 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/aws/aws-sdk-go v1.29.29 h1:4TdSYzXL8bHKu80tzPjO4c0ALw4Fd8qZGqf1aozUcBU= 3 | github.com/aws/aws-sdk-go v1.29.29/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 6 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 11 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 12 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 13 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 14 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 15 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 16 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 17 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 18 | github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= 19 | github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 24 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 25 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 26 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 29 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 30 | github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU= 31 | github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 32 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 34 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 35 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= 36 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 37 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 38 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 39 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 40 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 41 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 42 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 44 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 46 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 49 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY= 50 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 51 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 55 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 56 | -------------------------------------------------------------------------------- /printer/instance_info.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/ec2" 10 | "github.com/fatih/color" 11 | ) 12 | 13 | const ( 14 | parsedTagSeparator = "," 15 | undefinedItem = "UNDEFINED" 16 | ) 17 | 18 | // InstanceInfo is attributes of aws ec2 instance 19 | type InstanceInfo struct { 20 | InstanceID string `header:"INSTANCE_ID"` 21 | PrivateIPAddress string `header:"PRIVATE_IP"` 22 | PublicIPAddress string `header:"PUBLIC_IP"` 23 | InstanceType string `header:"TYPE"` 24 | StateName string `header:"STATE"` 25 | Tags map[string]string `header:"TAGS"` 26 | } 27 | 28 | // NewInstanceInfo creates a new InstanceInfo 29 | func NewInstanceInfo(instance *ec2.Instance) *InstanceInfo { 30 | if instance == nil { 31 | return &InstanceInfo{} 32 | } 33 | 34 | i := &InstanceInfo{ 35 | InstanceID: fetchItem(instance.InstanceId), 36 | PrivateIPAddress: fetchItem(instance.PrivateIpAddress), 37 | PublicIPAddress: fetchItem(instance.PublicIpAddress), 38 | InstanceType: fetchItem(instance.InstanceType), 39 | StateName: fetchItem(instance.State.Name), 40 | } 41 | 42 | tags := make(map[string]string) 43 | for _, tag := range instance.Tags { 44 | tags[*tag.Key] = *tag.Value 45 | } 46 | 47 | if len(tags) > 0 { 48 | i.Tags = tags 49 | } 50 | 51 | return i 52 | } 53 | 54 | // Headers gets header string array 55 | func (info *InstanceInfo) Headers() []string { 56 | var headers []string 57 | 58 | rt := reflect.TypeOf(*info) 59 | for i := 0; i < rt.NumField(); i++ { 60 | field := rt.Field(i) 61 | headers = append(headers, field.Tag.Get("header")) 62 | } 63 | 64 | return headers 65 | } 66 | 67 | // Values gets value string array 68 | func (info *InstanceInfo) Values(withColor bool) []string { 69 | var values []string 70 | 71 | values = append(values, info.InstanceID) 72 | values = append(values, info.PrivateIPAddress) 73 | values = append(values, info.PublicIPAddress) 74 | values = append(values, info.InstanceType) 75 | values = append(values, info.decorateStateName(withColor)) 76 | 77 | values = append(values, strings.Join(info.parseTags(), parsedTagSeparator)) 78 | 79 | return values 80 | } 81 | 82 | func (info *InstanceInfo) decorateStateName(withColor bool) string { 83 | if !withColor { 84 | return info.StateName 85 | } 86 | 87 | var colorAttr color.Attribute 88 | switch info.StateName { 89 | case "running": 90 | colorAttr = color.FgGreen 91 | case "stopped": 92 | colorAttr = color.FgRed 93 | default: 94 | colorAttr = color.FgYellow 95 | } 96 | 97 | return color.New(colorAttr).SprintFunc()(info.StateName) 98 | } 99 | 100 | func (info *InstanceInfo) parseTags() []string { 101 | var tagNames []string 102 | for name := range info.Tags { 103 | tagNames = append(tagNames, name) 104 | } 105 | sort.Strings(tagNames) 106 | 107 | var tags []string 108 | for _, name := range tagNames { 109 | tags = append(tags, name+"="+info.Tags[name]) 110 | } 111 | 112 | return tags 113 | } 114 | 115 | func fetchItem(instanceItem *string) string { 116 | if len(aws.StringValue(instanceItem)) == 0 { 117 | return undefinedItem 118 | } 119 | 120 | return *instanceItem 121 | } 122 | -------------------------------------------------------------------------------- /printer/instance_info_test.go: -------------------------------------------------------------------------------- 1 | package printer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | 9 | . "github.com/goldeneggg/lsec2/printer" 10 | ) 11 | 12 | const ( 13 | dummyInstanceID = "i-xxxxxxxx" 14 | dummyPrivateIPAddress = "10.0.0.1" 15 | dummyPublicIPAddress = "1.2.3.4" 16 | dummyInstanceType = "t2.micro" 17 | dummyStateCode = 16 18 | dummyStateName = "running" 19 | ) 20 | 21 | var ( 22 | dummyTagKeys = []string{"Name", "Role"} 23 | dummyTagValues = []string{"test-name", "test-role"} 24 | ) 25 | 26 | type infoTest struct { 27 | in *ec2.Instance 28 | expected *InstanceInfo 29 | expectedValues []string 30 | } 31 | 32 | var infoTests = []infoTest{ 33 | // normal case 34 | { 35 | in: &ec2.Instance{ 36 | InstanceId: aws.String(dummyInstanceID), 37 | PrivateIpAddress: aws.String(dummyPrivateIPAddress), 38 | PublicIpAddress: aws.String(dummyPublicIPAddress), 39 | InstanceType: aws.String(dummyInstanceType), 40 | State: &ec2.InstanceState{ 41 | Code: aws.Int64(dummyStateCode), 42 | Name: aws.String(dummyStateName), 43 | }, 44 | Tags: []*ec2.Tag{ 45 | { 46 | Key: aws.String(dummyTagKeys[0]), 47 | Value: aws.String(dummyTagValues[0]), 48 | }, 49 | { 50 | Key: aws.String(dummyTagKeys[1]), 51 | Value: aws.String(dummyTagValues[1]), 52 | }, 53 | }, 54 | }, 55 | expected: &InstanceInfo{ 56 | InstanceID: dummyInstanceID, 57 | PrivateIPAddress: dummyPrivateIPAddress, 58 | PublicIPAddress: dummyPublicIPAddress, 59 | InstanceType: dummyInstanceType, 60 | StateName: dummyStateName, 61 | Tags: map[string]string{ 62 | dummyTagKeys[0]: dummyTagValues[0], 63 | dummyTagKeys[1]: dummyTagValues[1], 64 | }, 65 | }, 66 | expectedValues: []string{ 67 | dummyInstanceID, 68 | dummyPrivateIPAddress, 69 | dummyPublicIPAddress, 70 | dummyInstanceType, 71 | dummyStateName, 72 | dummyTagKeys[0] + "=" + dummyTagValues[0] + "," + dummyTagKeys[1] + "=" + dummyTagValues[1], 73 | }, 74 | }, 75 | { 76 | // public ip is nil 77 | in: &ec2.Instance{ 78 | InstanceId: aws.String(dummyInstanceID), 79 | PrivateIpAddress: aws.String(dummyPrivateIPAddress), 80 | PublicIpAddress: nil, 81 | InstanceType: aws.String(dummyInstanceType), 82 | State: &ec2.InstanceState{ 83 | Code: aws.Int64(dummyStateCode), 84 | Name: aws.String(dummyStateName), 85 | }, 86 | Tags: []*ec2.Tag{ 87 | { 88 | Key: aws.String(dummyTagKeys[0]), 89 | Value: aws.String(dummyTagValues[0]), 90 | }, 91 | }, 92 | }, 93 | expected: &InstanceInfo{ 94 | InstanceID: dummyInstanceID, 95 | PrivateIPAddress: dummyPrivateIPAddress, 96 | PublicIPAddress: "UNDEFINED", 97 | InstanceType: dummyInstanceType, 98 | StateName: dummyStateName, 99 | Tags: map[string]string{ 100 | dummyTagKeys[0]: dummyTagValues[0], 101 | }, 102 | }, 103 | expectedValues: []string{ 104 | dummyInstanceID, 105 | dummyPrivateIPAddress, 106 | dummyPublicIPAddress, 107 | dummyInstanceType, 108 | dummyStateName, 109 | dummyTagKeys[0] + "=" + dummyTagValues[0], 110 | }, 111 | }, 112 | { 113 | // tags are empty 114 | in: &ec2.Instance{ 115 | InstanceId: aws.String(dummyInstanceID), 116 | PrivateIpAddress: aws.String(dummyPrivateIPAddress), 117 | PublicIpAddress: aws.String(dummyPublicIPAddress), 118 | InstanceType: aws.String(dummyInstanceType), 119 | State: &ec2.InstanceState{ 120 | Code: aws.Int64(dummyStateCode), 121 | Name: aws.String(dummyStateName), 122 | }, 123 | Tags: []*ec2.Tag{}, 124 | }, 125 | expected: &InstanceInfo{ 126 | InstanceID: dummyInstanceID, 127 | PrivateIPAddress: dummyPrivateIPAddress, 128 | PublicIPAddress: dummyPublicIPAddress, 129 | InstanceType: dummyInstanceType, 130 | StateName: dummyStateName, 131 | Tags: map[string]string{}, 132 | }, 133 | expectedValues: []string{ 134 | dummyInstanceID, 135 | dummyPrivateIPAddress, 136 | dummyPublicIPAddress, 137 | dummyInstanceType, 138 | dummyStateName, 139 | "", 140 | }, 141 | }, 142 | } 143 | 144 | func TestNewInstanceInfo(t *testing.T) { 145 | for _, it := range infoTests { 146 | out := NewInstanceInfo(it.in) 147 | 148 | if !compare(out, it.expected) { 149 | t.Errorf("expected: %#v, but out: %#v", it.expected, out) 150 | } 151 | } 152 | } 153 | 154 | func TestValues(t *testing.T) { 155 | for _, it := range infoTests { 156 | out := it.expected.Values(false) 157 | if len(out) != len(it.expectedValues) { 158 | t.Errorf("expected: [%#v], but out: [%#v]", it.expectedValues, out) 159 | } 160 | } 161 | } 162 | 163 | func compare(out *InstanceInfo, expected *InstanceInfo) bool { 164 | if out.InstanceID != expected.InstanceID { 165 | return false 166 | } 167 | if out.PrivateIPAddress != expected.PrivateIPAddress { 168 | return false 169 | } 170 | if out.PublicIPAddress != expected.PublicIPAddress { 171 | return false 172 | } 173 | if out.InstanceType != expected.InstanceType { 174 | return false 175 | } 176 | if out.StateName != expected.StateName { 177 | return false 178 | } 179 | 180 | if len(out.Tags) != len(expected.Tags) { 181 | return false 182 | } 183 | if len(out.Tags) > 0 { 184 | for k, v := range out.Tags { 185 | if v != expected.Tags[k] { 186 | return false 187 | } 188 | } 189 | } 190 | 191 | return true 192 | } 193 | -------------------------------------------------------------------------------- /printer/printer.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | "text/tabwriter" 9 | 10 | "github.com/aws/aws-sdk-go/service/ec2" 11 | 12 | "github.com/goldeneggg/lsec2/awsec2" 13 | ) 14 | 15 | const ( 16 | defaultDelimiter = "\t" 17 | 18 | minWidth = 0 19 | tabWidth = 4 20 | padding = 4 21 | padChar = ' ' 22 | flags = 0 23 | ) 24 | 25 | type flushableWriter interface { 26 | io.Writer 27 | Flush() error 28 | } 29 | 30 | // Printer is options definition of print 31 | type Printer struct { 32 | io.Writer 33 | Delimiter string 34 | PrintHeader bool 35 | OnlyPrivateIP bool 36 | WithColor bool 37 | } 38 | 39 | // NewPrinter returns a new Printer 40 | func NewPrinter(delim string, header, onlyPvtIP, withColor bool, w interface{}) *Printer { 41 | pr := new(Printer) 42 | 43 | if delim == "" { 44 | delim = defaultDelimiter 45 | } 46 | pr.Delimiter = delim 47 | pr.PrintHeader = header 48 | pr.OnlyPrivateIP = onlyPvtIP 49 | pr.WithColor = withColor 50 | 51 | if writer, ok := w.(io.Writer); ok { 52 | pr.Writer = writer 53 | } else { 54 | pr.Writer = defaultWriter(delim) 55 | } 56 | 57 | return pr 58 | } 59 | 60 | func defaultWriter(delim string) io.Writer { 61 | org := os.Stdout 62 | 63 | if delim == defaultDelimiter { 64 | // pretty print customize is here 65 | tw := tabwriter.NewWriter( 66 | org, 67 | minWidth, 68 | tabWidth, 69 | padding, 70 | padChar, 71 | flags, 72 | ) 73 | return tw 74 | } 75 | 76 | return org 77 | } 78 | 79 | // PrintAll prints information all of aws ec2 instances 80 | func (pr *Printer) PrintAll(client *awsec2.Client) error { 81 | instances, err := client.EC2Instances() 82 | if err != nil { 83 | return fmt.Errorf("get EC2 Instances error: %v", err) 84 | } 85 | 86 | defer pr.flushIfFlushable() 87 | 88 | if pr.PrintHeader { 89 | if err := pr.printHeader(); err != nil { 90 | return fmt.Errorf("print header error: %v", err) 91 | } 92 | } 93 | 94 | for _, inst := range instances { 95 | if err := pr.printInstance(client, inst); err != nil { 96 | return fmt.Errorf("print instance error: %v", err) 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (pr *Printer) flushIfFlushable() { 104 | if fw, ok := pr.Writer.(flushableWriter); ok { 105 | fw.Flush() 106 | } 107 | } 108 | 109 | func (pr *Printer) printHeader() error { 110 | return pr.printArray(NewInstanceInfo(nil).Headers()) 111 | } 112 | 113 | func (pr *Printer) printInstance(client *awsec2.Client, inst *ec2.Instance) error { 114 | var ( 115 | ii *InstanceInfo 116 | err error 117 | ) 118 | 119 | ii = NewInstanceInfo(inst) 120 | if len(client.StateName) == 0 || client.StateName == ii.StateName { 121 | if pr.OnlyPrivateIP { 122 | err = pr.printArray([]string{ii.PrivateIPAddress}) 123 | } else { 124 | err = pr.printArray(ii.Values(pr.WithColor)) 125 | } 126 | } 127 | 128 | return err 129 | } 130 | 131 | func (pr *Printer) printArray(sArr []string) error { 132 | if _, err := fmt.Fprintln(pr.Writer, strings.Join(sArr, pr.Delimiter)); err != nil { 133 | return err 134 | } 135 | 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /printer/printer_test.go: -------------------------------------------------------------------------------- 1 | package printer_test 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | "text/tabwriter" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/service/ec2" 12 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 13 | 14 | "github.com/goldeneggg/lsec2/awsec2" 15 | . "github.com/goldeneggg/lsec2/printer" 16 | ) 17 | 18 | var ( 19 | dummyInstances = []*ec2.Instance{ 20 | &ec2.Instance{ 21 | InstanceId: aws.String("i-xxxxxxxx91"), 22 | PrivateIpAddress: aws.String("192.168.56.101"), 23 | PublicIpAddress: aws.String("54.0.0.1"), 24 | InstanceType: aws.String("t3.large"), 25 | State: &ec2.InstanceState{ 26 | Name: aws.String("running"), 27 | }, 28 | }, 29 | &ec2.Instance{ 30 | InstanceId: aws.String("i-xxxxxxxx99"), 31 | PrivateIpAddress: aws.String("192.168.56.199"), 32 | PublicIpAddress: aws.String("54.0.0.9"), 33 | InstanceType: aws.String("t4.large"), 34 | State: &ec2.InstanceState{ 35 | Name: aws.String("stopped"), 36 | }, 37 | }, 38 | } 39 | ) 40 | 41 | type mockEC2API struct { 42 | ec2iface.EC2API 43 | } 44 | 45 | // override for mock 46 | func (mock *mockEC2API) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { 47 | instancesOutput := &ec2.DescribeInstancesOutput{ 48 | Reservations: []*ec2.Reservation{ 49 | &ec2.Reservation{ 50 | Instances: []*ec2.Instance{dummyInstances[0]}, 51 | }, 52 | &ec2.Reservation{ 53 | Instances: []*ec2.Instance{dummyInstances[1]}, 54 | }, 55 | }, 56 | } 57 | 58 | return instancesOutput, nil 59 | } 60 | 61 | func TestNewPrinter(t *testing.T) { 62 | dummyFile := os.NewFile(uintptr(3), "printer_test.go.out") 63 | cases := []struct { 64 | delim string 65 | header bool 66 | onlyPvtIP bool 67 | withColor bool 68 | w io.Writer 69 | expected *Printer 70 | }{ 71 | { 72 | delim: "", 73 | header: true, 74 | onlyPvtIP: false, 75 | withColor: true, 76 | w: nil, 77 | expected: &Printer{ 78 | Delimiter: "\t", 79 | PrintHeader: true, 80 | OnlyPrivateIP: false, 81 | WithColor: true, 82 | }, 83 | }, 84 | { 85 | delim: ",", 86 | header: false, 87 | onlyPvtIP: true, 88 | withColor: false, 89 | w: dummyFile, 90 | expected: &Printer{ 91 | Writer: dummyFile, 92 | Delimiter: ",", 93 | PrintHeader: false, 94 | OnlyPrivateIP: true, 95 | WithColor: false, 96 | }, 97 | }, 98 | } 99 | 100 | for _, c := range cases { 101 | pr := NewPrinter(c.delim, c.header, c.onlyPvtIP, c.withColor, c.w) 102 | 103 | if pr.Delimiter != c.expected.Delimiter { 104 | t.Errorf("expected: %#v, but actual: %#v", c.expected.Delimiter, pr.Delimiter) 105 | } 106 | if pr.PrintHeader != c.expected.PrintHeader { 107 | t.Errorf("expected: %#v, but actual: %#v", c.expected.PrintHeader, pr.PrintHeader) 108 | } 109 | if pr.OnlyPrivateIP != c.expected.OnlyPrivateIP { 110 | t.Errorf("expected: %#v, but actual: %#v", c.expected.OnlyPrivateIP, pr.OnlyPrivateIP) 111 | } 112 | if pr.WithColor != c.expected.WithColor { 113 | t.Errorf("expected: %#v, but actual: %#v", c.expected.WithColor, pr.WithColor) 114 | } 115 | if c.w == nil { 116 | if _, ok := pr.Writer.(*tabwriter.Writer); !ok { 117 | t.Errorf("expected: *tabwriter.Writer, but actual: %#v", pr.Writer) 118 | } 119 | } else { 120 | if pr.Writer != c.expected.Writer { 121 | t.Errorf("expected: %#v, but actual: %#v", c.expected.Writer, pr.Writer) 122 | } 123 | } 124 | } 125 | } 126 | 127 | func TestPrintAll(t *testing.T) { 128 | tmp, err := ioutil.TempFile("", "test_lsec2_printer") 129 | if err != nil { 130 | t.Errorf("failed to ioutil.TempFile. err: %#v", err) 131 | } 132 | defer os.Remove(tmp.Name()) 133 | 134 | client := &awsec2.Client{ 135 | EC2API: &mockEC2API{}, 136 | StateName: "running", 137 | } 138 | cases := []struct { 139 | pr *Printer 140 | }{ 141 | { 142 | pr: &Printer{ 143 | Writer: os.Stdout, 144 | PrintHeader: true, 145 | OnlyPrivateIP: false, 146 | WithColor: true, 147 | Delimiter: "\t", 148 | }, 149 | }, 150 | { 151 | pr: &Printer{ 152 | Writer: tmp, 153 | PrintHeader: false, 154 | OnlyPrivateIP: false, 155 | WithColor: false, 156 | Delimiter: ",", 157 | }, 158 | }, 159 | { 160 | pr: &Printer{ 161 | Writer: tmp, 162 | PrintHeader: false, 163 | OnlyPrivateIP: true, 164 | WithColor: false, 165 | }, 166 | }, 167 | } 168 | 169 | for _, c := range cases { 170 | if err := c.pr.PrintAll(client); err != nil { 171 | t.Errorf("error occured. err: %#v", err) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /scripts/_ldflags.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | source scripts/_prepare.sh 5 | 6 | CUR_DATE=$(date "+%Y-%m-%d %H:%M:%S") 7 | COMMIT=$(git log --format=%H -n1) 8 | GO_VERSION=$(go version) 9 | 10 | LF="-w -s -extldflags \"-static\"" 11 | LF="${LF} -X \"${FULL_PACKAGE}/cmd/lsec2/cli.BuildDate=${CUR_DATE}\"" 12 | LF="${LF} -X \"${FULL_PACKAGE}/cmd/lsec2/cli.BuildCommit=${COMMIT}\"" 13 | LF="${LF} -X \"${FULL_PACKAGE}/cmd/lsec2/cli.GoVersion=${GO_VERSION}\"" 14 | 15 | echo "${LF}" 16 | -------------------------------------------------------------------------------- /scripts/_packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | echo "$(go list ./... | \grep -v 'vendor')" 5 | -------------------------------------------------------------------------------- /scripts/_prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | MYDIR=$(cd $(dirname $0) && pwd) 3 | BASE_DIR=${MYDIR}/.. 4 | CMD_GO_DIR=${BASE_DIR}/cmd/lsec2 5 | FULL_PACKAGE=github.com/goldeneggg/lsec2 6 | -------------------------------------------------------------------------------- /scripts/_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | source scripts/_prepare.sh 5 | 6 | GREP=$(which grep) 7 | SED=$(which sed) 8 | 9 | ${GREP} "const VERSION" ${CMD_GO_DIR}/version/version.go | ${SED} -e 's/const VERSION = //g' | ${SED} -e 's/\"//g' 10 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source scripts/_prepare.sh 6 | 7 | [ $# -lt 1 ] && { echo 'need to assign output target'; exit 1; } 8 | OUTPUT=${1} 9 | shift 10 | 11 | OTHER_OPTS="" 12 | if [ $# -ge 1 ] 13 | then 14 | OTHER_OPTS="$@" 15 | echo "OTHER_OPTS = ${OTHER_OPTS}" 16 | fi 17 | 18 | LDFLAGS=$(${MYDIR}/_ldflags.sh) 19 | 20 | echo "LDFLAGS=${LDFLAGS}, GOOS=${GOOS}, GOARCH=${GOARCH}, OTHER_OPTS=${OTHER_OPTS}" 21 | go build -a -tags netgo -installsuffix netgo -ldflags="${LDFLAGS}" ${OTHER_OPTS} -o ${OUTPUT} ${CMD_GO_DIR} 22 | -------------------------------------------------------------------------------- /scripts/ci-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | source scripts/_prepare.sh 5 | 6 | echo "" > coverage.txt 7 | for d in $(${MYDIR}/_packages.sh); do 8 | go test -race -coverprofile=profile.out -covermode=atomic $d 9 | if [ -f profile.out ]; then 10 | cat profile.out >> coverage.txt 11 | rm profile.out 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source scripts/_prepare.sh 6 | 7 | OTHER_OPTS="" 8 | if [ $# -ge 1 ] 9 | then 10 | OTHER_OPTS="$@" 11 | echo "OTHER_OPTS = ${OTHER_OPTS}" 12 | fi 13 | 14 | LDFLAGS=$(${MYDIR}/_ldflags.sh) 15 | 16 | echo "LDFLAGS=${LDFLAGS}, GOOS=${GOOS}, GOARCH=${GOARCH}, OTHER_OPTS=${OTHER_OPTS}" 17 | go install -v -ldflags="${LDFLAGS}" ${OTHER_OPTS} ${CMD_GO_DIR} 18 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "golang.org/x/lint/golint" 7 | ) 8 | --------------------------------------------------------------------------------