├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd ├── main.go └── version.go ├── go.mod ├── go.sum ├── internal ├── app.go ├── app_test.go ├── aws.go ├── aws_test.go ├── command.go ├── command_test.go ├── forward.go ├── forward_test.go └── select.go └── test.tf /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | jobs: 9 | Release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.18 21 | 22 | - name: Install dependencies 23 | run: | 24 | go mod tidy 25 | # reset go.sum and go.mod so goreleaser won't complain about dirty git state 26 | git checkout HEAD -- go.sum go.mod 27 | 28 | - name: Run tests 29 | run: go test -v ./internal 30 | 31 | - name: Build 32 | uses: goreleaser/goreleaser-action@v6 33 | with: 34 | version: latest 35 | args: build 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Release 40 | uses: goreleaser/goreleaser-action@v6 41 | with: 42 | version: latest 43 | args: release --clean 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | paths: 8 | - "**.go" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | Test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | 20 | - name: Install Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.18 24 | 25 | - name: Install dependencies 26 | run: | 27 | go mod tidy 28 | # reset go.sum and go.mod so goreleaser won't complain about dirty git state 29 | git checkout HEAD -- go.sum go.mod 30 | 31 | - name: Run tests 32 | run: go test -v ./internal 33 | 34 | - name: Build 35 | uses: goreleaser/goreleaser-action@v6 36 | with: 37 | version: latest 38 | args: --snapshot --skip=publish --clean 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | ecsgo 18 | 19 | # For goreleaser otherwise build fails with git dirty error 20 | dist 21 | 22 | # Exclude .vscode folder 23 | .vscode/ 24 | 25 | # Exclude terraform state files from test folder 26 | .terraform* 27 | **.tfstate* 28 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - dir: cmd 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - darwin 12 | goarch: 13 | - amd64 14 | - arm64 15 | mod_timestamp: "{{ .CommitTimestamp }}" 16 | flags: 17 | - -trimpath 18 | ldflags: 19 | - -s -w 20 | - -X main.version={{.Version}} 21 | - -X main.commit={{.Commit}} 22 | - -X main.date={{ .CommitDate }} 23 | - -X main.builtBy=goreleaser 24 | changelog: 25 | sort: asc 26 | filters: 27 | exclude: 28 | - "^docs:" 29 | - "^test:" 30 | - Merge pull request 31 | - Merge branch 32 | - go mod tidy 33 | archives: 34 | - name_template: >- 35 | {{- .ProjectName }}_ 36 | {{- title .Os }}_ 37 | {{- if eq .Arch "amd64" }}x86_64 38 | {{- else if eq .Arch "386" }}i386 39 | {{- else }}{{ .Arch }}{{ end }} 40 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 41 | format_overrides: 42 | - goos: windows 43 | format: zip 44 | files: 45 | - README.md 46 | - LICENSE 47 | release: 48 | github: 49 | owner: tedsmitt 50 | name: ecsgo 51 | brews: 52 | - name: ecsgo 53 | repository: 54 | owner: tedsmitt 55 | name: homebrew-ecsgo 56 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 57 | commit_author: 58 | name: tedsmitt 59 | email: ed@edintheclouds.io 60 | description: "Interactive CLI tool which acts as a wrapper around the ECS ExecuteCommand API." 61 | license: Apache2 62 | test: | 63 | system "#{bin}/goreleaser -v" 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecsgo 2 | 3 | Inspired by the incredibly useful [gossm](https://github.com/gjbae1212/gossm), this tool makes use of the [ECS ExecuteCommand API](https://aws.amazon.com/blogs/containers/new-using-amazon-ecs-exec-access-your-containers-fargate-ec2/) to connect to running ECS tasks. 4 | 5 | It provides an interactive prompt to select your cluster, task and container (if only one container in the task it will default to this), and opens a connection to it. You can also use it to port-forward to containers within your tasks. 6 | 7 | That's it! Nothing fancy. 8 | 9 | ## Installation 10 | 11 | ### MacOS/Homebrew 12 | 13 | ```bash 14 | brew tap tedsmitt/ecsgo 15 | brew install ecsgo 16 | ``` 17 | 18 | ### Linux 19 | 20 | ```bash 21 | wget https://github.com/tedsmitt/ecsgo/releases/latest/download/ecsgo_Linux_x86_64.tar.gz 22 | tar xzf ecsgo_*.tar.gz 23 | ``` 24 | 25 | Move the `ecsgo` binary into your `$PATH` 26 | 27 | ## Pre-requisites 28 | 29 | ### session-manager-plugin 30 | 31 | This tool makes use of the [session-manager-plugin](https://github.com/aws/session-manager-plugin). For instructions on how to install, please check out [this user guide](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html). 32 | 33 | MacOS users can install this via Homebrew if desired 34 | `brew install --cask session-manager-plugin` 35 | 36 | ### Infrastructure 37 | 38 | Use [ecs-exec-checker](https://github.com/aws-containers/amazon-ecs-exec-checker) to check for the pre-requisites to use ECS exec. 39 | 40 | ## Usage 41 | 42 | ### CLI Args 43 | 44 | By default, the tool will prompt you to interactively select which cluster, service, task and container to connect to. You can change the behaviour using the flags detailed below: 45 | 46 | | Long | Short | Description | Default Value | 47 | | -------------------- | ----- | --------------------------------------------------------------------------------------------------------- | -------------------------- | 48 | | `--cluster` | `-n` | Specify the ECS cluster name | N/A | 49 | | `--service` | `-s` | Specify the ECS service name | N/A | 50 | | `--task` | `-t` | Specify the ECS Task ID | N/A | 51 | | `--container` | `-u` | Specify the container name in the ECS Task (if task only has one container this will selected by default) | N/A | 52 | | `--cmd` | `-c` | Specify the command to be run on the container (default will change depending on OS family). | `/bin/sh`,`powershell.exe` | 53 | | `--forward` | `-f` | Port-forward to the container (Remote port will be taken from task/container definitions) | `false` | 54 | | `--local-port` | `-l` | Specify local port to forward (will prompt if not specified) | N/A | 55 | | `--profile` | `-p` | Specify the profile to load the credentials | `default` | 56 | | `--region` | `-r` | Specify the AWS region to run in | N/A | 57 | | `--quiet` | `-q` | Disable output detailing the Cluster/Service/Task information | `false` | 58 | | `--aws-endpoint-url` | `-e` | Specify the AWS endpoint used for all service requests | N/A | 59 | 60 | ### Environment variables 61 | 62 | The above options can also be configured via environment variables. Simply export environment variables in the form `ECSGO_`. For example, if you want to set the `--cluster` value, it would be `ECSGO_CLUSTER`, or for the `--aws-endpoint-url` option it would be `ECSGO_AWS_ENDPOINT_URL`. 63 | 64 | The tool supports Standard AWS Environment Variables for AWS Client configuration. If you aren't familiar with working on AWS via the CLI, you can read more about how to configure your environment [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). 65 | 66 | ## Example 67 | 68 | See it in action below 69 | 70 | ![ecsgo0 2 0](https://user-images.githubusercontent.com/25430401/114218136-ef8f7b00-9960-11eb-9c3f-b353ae0ff7ca.gif) 71 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 Ed Smith ed@edintheclouds.io 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | "strings" 22 | 23 | "github.com/spf13/cobra" 24 | "github.com/spf13/viper" 25 | app "github.com/tedsmitt/ecsgo/internal" 26 | ) 27 | 28 | func main() { 29 | rootCmd.Execute() 30 | } 31 | 32 | var cfgFile string 33 | 34 | // rootCmd represents the base command when called without any subcommands 35 | var rootCmd = &cobra.Command{ 36 | Use: "ecsgo", 37 | Short: "Tool to list and connect to your ECS tasks", 38 | Long: ` 39 | ########################################################## 40 | ___ ___ ___ __ _ ___ 41 | / _ \/ __/ __|/ _ |/ _ \ 42 | | __/ (__\__ \ (_| | (_) | 43 | \___|\___|___/\__, |\___/ 44 | |___/ 45 | ########################################################## 46 | 47 | Lists your ECS Clusters/tasks/containers and allows you to interactively select which to connect to. Makes use 48 | of the ECS ExecuteCommand API under the hood. 49 | 50 | Requires pre-existing installation of the session-manager-plugin 51 | (https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) 52 | ------------`, 53 | // Validate args 54 | PreRunE: func(cmd *cobra.Command, args []string) error { 55 | cluster := cmd.PersistentFlags().Lookup("cluster") 56 | service := cmd.PersistentFlags().Lookup("service") 57 | task := cmd.PersistentFlags().Lookup("task") 58 | 59 | if cluster.Value.String() == "" { 60 | if task.Value.String() != "" { 61 | return fmt.Errorf(app.Red("Cluster name must be specified when specifying task")) 62 | } 63 | if service.Value.String() != "" { 64 | return fmt.Errorf(app.Red("Cluster name must be specified when specifying service")) 65 | } 66 | } 67 | if task.Value.String() != "" && service.Value.String() != "" { 68 | fmt.Printf(fmt.Sprintf("%s\n", app.Yellow("The service argument will be ignored when task is specified"))) 69 | viper.Set("service", "") 70 | } 71 | 72 | viper.SetEnvPrefix("ECSGO") 73 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 74 | viper.AutomaticEnv() 75 | 76 | return nil 77 | }, 78 | Run: func(cmd *cobra.Command, args []string) { 79 | a := app.CreateApp() 80 | if err := a.Start(); err != nil { 81 | fmt.Printf("\n%s\n", app.Red(err)) 82 | os.Exit(1) 83 | } 84 | }, 85 | Version: getVersion(), 86 | } 87 | 88 | func init() { 89 | // Here you will define your flags and configuration settings. 90 | 91 | // Cobra supports persistent flags, which, if defined here, 92 | // will be global for your application. 93 | rootCmd.PersistentFlags().StringP("cmd", "c", "", "Command to run on the container") 94 | rootCmd.PersistentFlags().StringP("profile", "p", "", "AWS Profile") 95 | rootCmd.PersistentFlags().StringP("region", "r", "", "AWS Region") 96 | rootCmd.PersistentFlags().StringP("cluster", "n", "", "Cluster Name") 97 | rootCmd.PersistentFlags().StringP("service", "s", "", "Service Name") 98 | rootCmd.PersistentFlags().StringP("task", "t", "", "Task ID") 99 | rootCmd.PersistentFlags().StringP("container", "u", "", "Container name") 100 | rootCmd.PersistentFlags().BoolP("forward", "f", false, "Port Forward") 101 | rootCmd.PersistentFlags().StringP("local-port", "l", "", "Local port for use with port forwarding") 102 | rootCmd.PersistentFlags().BoolP("quiet", "q", false, "Do not print cluster and container information") 103 | rootCmd.PersistentFlags().StringP("aws-endpoint-url", "e", "", "AWS Endpoint Url") 104 | 105 | viper.BindPFlag("cmd", rootCmd.PersistentFlags().Lookup("cmd")) 106 | viper.BindPFlag("profile", rootCmd.PersistentFlags().Lookup("profile")) 107 | viper.BindPFlag("region", rootCmd.PersistentFlags().Lookup("region")) 108 | viper.BindPFlag("cluster", rootCmd.PersistentFlags().Lookup("cluster")) 109 | viper.BindPFlag("service", rootCmd.PersistentFlags().Lookup("service")) 110 | viper.BindPFlag("task", rootCmd.PersistentFlags().Lookup("task")) 111 | viper.BindPFlag("container", rootCmd.PersistentFlags().Lookup("container")) 112 | viper.BindPFlag("forward", rootCmd.PersistentFlags().Lookup("forward")) 113 | viper.BindPFlag("local-port", rootCmd.PersistentFlags().Lookup("local-port")) 114 | viper.BindPFlag("quiet", rootCmd.PersistentFlags().Lookup("quiet")) 115 | viper.BindPFlag("aws-endpoint-url", rootCmd.PersistentFlags().Lookup("aws-endpoint-url")) 116 | } 117 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | var ( 6 | version = "unset" 7 | commit = "unset" 8 | date = "unset" 9 | builtBy = "unset" 10 | ) 11 | 12 | // getVersion returns version information 13 | func getVersion() string { 14 | return fmt.Sprintf("Version: %s, Commit: %s, Built date: %s, Built by: %s", version, commit, date, builtBy) 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tedsmitt/ecsgo 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.2.9 7 | github.com/aws/aws-sdk-go-v2 v1.24.0 8 | github.com/aws/aws-sdk-go-v2/config v1.26.2 9 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.0 10 | github.com/aws/aws-sdk-go-v2/service/ecs v1.35.6 11 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.6 12 | github.com/fatih/color v1.10.0 13 | github.com/spf13/cobra v1.1.3 14 | github.com/spf13/viper v1.7.1 15 | github.com/stretchr/testify v1.3.0 16 | ) 17 | 18 | require ( 19 | github.com/aws/aws-sdk-go-v2/credentials v1.16.13 // indirect 20 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect 21 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect 22 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.6 // indirect 29 | github.com/aws/smithy-go v1.19.0 // indirect 30 | github.com/davecgh/go-spew v1.1.1 // indirect 31 | github.com/fsnotify/fsnotify v1.4.7 // indirect 32 | github.com/hashicorp/hcl v1.0.0 // indirect 33 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 34 | github.com/jmespath/go-jmespath v0.4.0 // indirect 35 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 36 | github.com/magiconair/properties v1.8.1 // indirect 37 | github.com/mattn/go-colorable v0.1.8 // indirect 38 | github.com/mattn/go-isatty v0.0.12 // indirect 39 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 40 | github.com/mitchellh/mapstructure v1.1.2 // indirect 41 | github.com/pelletier/go-toml v1.2.0 // indirect 42 | github.com/pmezard/go-difflib v1.0.0 // indirect 43 | github.com/spf13/afero v1.1.2 // indirect 44 | github.com/spf13/cast v1.3.0 // indirect 45 | github.com/spf13/jwalterweatherman v1.0.0 // indirect 46 | github.com/spf13/pflag v1.0.5 // indirect 47 | github.com/subosito/gotenv v1.2.0 // indirect 48 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 49 | golang.org/x/sys v0.5.0 // indirect 50 | golang.org/x/text v0.13.0 // indirect 51 | gopkg.in/ini.v1 v1.51.0 // indirect 52 | gopkg.in/yaml.v2 v2.4.0 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/AlecAivazis/survey/v2 v2.2.9 h1:LWvJtUswz/W9/zVVXELrmlvdwWcKE60ZAw0FWV9vssk= 15 | github.com/AlecAivazis/survey/v2 v2.2.9/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk= 16 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 17 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 18 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 19 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= 20 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 21 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 22 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 23 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 24 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 25 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 26 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 27 | github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= 28 | github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= 29 | github.com/aws/aws-sdk-go-v2/config v1.26.2 h1:+RWLEIWQIGgrz2pBPAUoGgNGs1TOyF4Hml7hCnYj2jc= 30 | github.com/aws/aws-sdk-go-v2/config v1.26.2/go.mod h1:l6xqvUxt0Oj7PI/SUXYLNyZ9T/yBPn3YTQcJLLOdtR8= 31 | github.com/aws/aws-sdk-go-v2/credentials v1.16.13 h1:WLABQ4Cp4vXtXfOWOS3MEZKr6AAYUpMczLhgKtAjQ/8= 32 | github.com/aws/aws-sdk-go-v2/credentials v1.16.13/go.mod h1:Qg6x82FXwW0sJHzYruxGiuApNo31UEtJvXVSZAXeWiw= 33 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= 34 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= 35 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= 36 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= 37 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= 38 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= 39 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= 40 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= 41 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.0 h1:VrFC1uEZjX4ghkm/et8ATVGb1mT75Iv8aPKPjUE+F8A= 42 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.0/go.mod h1:qjhtI9zjpUHRc6khtrIM9fb48+ii6+UikL3/b+MKYn0= 43 | github.com/aws/aws-sdk-go-v2/service/ecs v1.35.6 h1:Sc2mLjyA1R8z2l705AN7Wr7QOlnUxVnGPJeDIVyUSrs= 44 | github.com/aws/aws-sdk-go-v2/service/ecs v1.35.6/go.mod h1:LzHcyOEvaLjbc5e+fP/KmPWBr+h/Ef+EHvnf1Pzo368= 45 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= 46 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= 47 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= 48 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= 49 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.6 h1:EZw+TRx/4qlfp6VJ0P1sx04Txd9yGNK+NiO1upaXmh4= 50 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.6/go.mod h1:uXndCJoDO9gpuK24rNWVCnrGNUydKFEAYAZ7UU9S0rQ= 51 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= 52 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= 53 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= 54 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= 55 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.6 h1:HJeiuZ2fldpd0WqngyMR6KW7ofkXNLyOaHwEIGm39Cs= 56 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.6/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= 57 | github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= 58 | github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= 59 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 60 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 61 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 62 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 63 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 64 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 65 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 66 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 67 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 68 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 69 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 70 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 71 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 72 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 73 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 74 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 75 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 76 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 77 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 78 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 79 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 80 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 81 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 82 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 83 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 84 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 85 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 86 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 87 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 88 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 89 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 90 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 91 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 92 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 93 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 94 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 95 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 96 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 97 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 98 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 99 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 100 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 101 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 102 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 103 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 104 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 105 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 106 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 107 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 108 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 109 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 110 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 111 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 112 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 113 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 114 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 115 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 116 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 117 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 118 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 119 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 120 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 121 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 122 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 123 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 124 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 125 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 126 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 127 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 128 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 129 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 130 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 131 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 132 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 133 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 134 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 135 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= 136 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 137 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 138 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 139 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 140 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 141 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 142 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 143 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 144 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 145 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 146 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 147 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 148 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 149 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 150 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 151 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 152 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 153 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 154 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 155 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 156 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 157 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 158 | github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= 159 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 160 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 161 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 162 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 163 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 164 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 165 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 166 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 167 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 168 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 169 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 170 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 171 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 172 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 173 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 174 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 175 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 176 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 177 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 178 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 179 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 180 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 181 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 182 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 183 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 184 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 185 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 186 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 187 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 188 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 189 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 190 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 191 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 192 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 193 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 194 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 195 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 196 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 197 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 198 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 199 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 200 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 201 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 202 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 203 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 204 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 205 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 206 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 207 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 208 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 209 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 210 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 211 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 212 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 213 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 214 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 215 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 216 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 217 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 218 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 219 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 220 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 221 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 222 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 223 | github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= 224 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 225 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 226 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 227 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 228 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 229 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 230 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 231 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 232 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 233 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 234 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 235 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 236 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 237 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 238 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 239 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 240 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 241 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 242 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 243 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 244 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 245 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 246 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 247 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 248 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 249 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 250 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 251 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 252 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 253 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 254 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 255 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 256 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 257 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 258 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 259 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 260 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 261 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 262 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 263 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 264 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 265 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 266 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 267 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 268 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 269 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 270 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 271 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 272 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 273 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 274 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 275 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 276 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 277 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 278 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 279 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 280 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 281 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 282 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 283 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 284 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 285 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 286 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 287 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 288 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 289 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 290 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 291 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 292 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 293 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 294 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 295 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 296 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 297 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 298 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 299 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 300 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 301 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 302 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 303 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 304 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 305 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 306 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 307 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 308 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 309 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 310 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 311 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 312 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 313 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 314 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 315 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 316 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 317 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 318 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 319 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 320 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 321 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 322 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 323 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 324 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 325 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 326 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 327 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 328 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 329 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 330 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 331 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 332 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 333 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 334 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 335 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 336 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 337 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 338 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 339 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 340 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 341 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 342 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 343 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 344 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 345 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 346 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 347 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 348 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 349 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 350 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 351 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 352 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 353 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 354 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 355 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 356 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 357 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 358 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 359 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 360 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 361 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 362 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 363 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 364 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 365 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 366 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 367 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 368 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 369 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 370 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 371 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 372 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 373 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 374 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 375 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 376 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 377 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 378 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 379 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 380 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 381 | -------------------------------------------------------------------------------- /internal/app.go: -------------------------------------------------------------------------------- 1 | /* app.go contains the main logic for running the app */ 2 | 3 | package app 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "flag" 9 | "fmt" 10 | "os" 11 | "os/exec" 12 | "os/signal" 13 | "sort" 14 | "strings" 15 | "syscall" 16 | 17 | "github.com/aws/aws-sdk-go-v2/aws" 18 | "github.com/aws/aws-sdk-go-v2/service/ecs" 19 | ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" 20 | "github.com/fatih/color" 21 | "github.com/spf13/viper" 22 | ) 23 | 24 | var ( 25 | region string 26 | 27 | Red = color.New(color.FgRed).SprintFunc() 28 | Magenta = color.New(color.FgMagenta).SprintFunc() 29 | Cyan = color.New(color.FgCyan).SprintFunc() 30 | Green = color.New(color.FgGreen).SprintFunc() 31 | Yellow = color.New(color.FgYellow).SprintFunc() 32 | 33 | pageSize = 15 34 | backOpt = "⏎ Back" // backOpt is used to allow the user to navigate backwards in the selection prompt 35 | awsMaxResults = aws.Int32(int32(100)) 36 | ) 37 | 38 | // runCommand executes a command in the current shell and returns an error if the command fails 39 | func runCommand(process string, args ...string) error { 40 | if flag.Lookup("test.v") != nil { 41 | // emulate successful return for testing purposes 42 | return nil 43 | } 44 | 45 | // Capture any SIGINTs and discard them 46 | sigs := make(chan os.Signal, 1) 47 | signal.Notify(sigs, os.Interrupt, syscall.SIGINT) 48 | go func() { 49 | for { 50 | select { 51 | case <-sigs: 52 | } 53 | } 54 | }() 55 | defer close(sigs) 56 | 57 | cmd := exec.Command(process, args...) 58 | cmd.Stderr = os.Stderr 59 | cmd.Stdout = os.Stdout 60 | cmd.Stdin = os.Stdin 61 | 62 | if err := cmd.Run(); err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // App is the main struct for the application which holds the state and methods for the application 70 | type App struct { 71 | input chan string 72 | err chan error 73 | exit chan error 74 | client ECSClient 75 | region string 76 | endpoint string 77 | cluster string 78 | service string 79 | task *ecsTypes.Task 80 | tasks map[string]*ecsTypes.Task 81 | container *ecsTypes.Container 82 | } 83 | 84 | // CreateApp initialises a new App struct with the required initial values 85 | func CreateApp() *App { 86 | client := createEcsClient() 87 | e := &App{ 88 | input: make(chan string, 1), 89 | err: make(chan error, 1), 90 | exit: make(chan error, 1), 91 | client: client, 92 | region: client.Options().Region, 93 | } 94 | 95 | return e 96 | } 97 | 98 | // Start begins a goroutine that listens on the input channel for instructions 99 | func (e *App) Start() error { 100 | // Before we do anything make sure that the session-manager-plugin is available in $PATH, exit if it isn't 101 | _, err := exec.LookPath("session-manager-plugin") 102 | if err != nil { 103 | fmt.Println(Red("session-manager-plugin isn't installed or wasn't found in $PATH - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html")) 104 | return err 105 | } 106 | 107 | go func() { 108 | for { 109 | select { 110 | case input := <-e.input: 111 | switch input { 112 | case "getCluster": 113 | e.getCluster() 114 | case "getService": 115 | e.getService() 116 | case "getTask": 117 | e.getTask() 118 | case "getContainer": 119 | e.getContainer() 120 | case "execute": 121 | if viper.GetBool("forward") { 122 | e.executeForward() 123 | } else { 124 | e.executeCommand() 125 | } 126 | default: 127 | e.getCluster() 128 | } 129 | case err := <-e.err: 130 | e.exit <- err 131 | } 132 | } 133 | }() 134 | 135 | // Initiate the workflow 136 | e.input <- "getCluster" 137 | 138 | // Block until we receive a message on the exit channel 139 | err = <-e.exit 140 | if err != nil { 141 | return err 142 | } 143 | 144 | return nil 145 | } 146 | 147 | // Lists available clusters and prompts the user to select one 148 | func (e *App) getCluster() { 149 | var clusters []string 150 | var nextToken *string 151 | 152 | if cluster := viper.GetString("cluster"); cluster != "" { 153 | e.cluster = cluster 154 | viper.Set("cluster", "") // Reset the cli arg so user can navigate 155 | 156 | // if task is set, skip ahead to getTask 157 | if taskId := viper.GetString("task"); taskId != "" { 158 | e.input <- "getTask" 159 | return 160 | } 161 | 162 | e.input <- "getService" 163 | return 164 | } 165 | 166 | if e.client == nil { 167 | panic("oh no") 168 | } 169 | list, err := e.client.ListClusters(context.TODO(), &ecs.ListClustersInput{ 170 | MaxResults: awsMaxResults, 171 | }) 172 | if err != nil { 173 | e.err <- err 174 | return 175 | } 176 | clusters = append(clusters, list.ClusterArns...) 177 | nextToken = list.NextToken 178 | 179 | if nextToken != nil { 180 | for { 181 | list, err := e.client.ListClusters(context.TODO(), &ecs.ListClustersInput{ 182 | MaxResults: awsMaxResults, 183 | NextToken: nextToken, 184 | }) 185 | if err != nil { 186 | e.err <- err 187 | return 188 | } 189 | clusters = append(clusters, list.ClusterArns...) 190 | if list.NextToken == nil { 191 | break 192 | } else { 193 | nextToken = list.NextToken 194 | } 195 | } 196 | } 197 | 198 | // Sort the list of clusters alphabetically 199 | sort.Slice(clusters, func(i, j int) bool { 200 | return clusters[i] < clusters[j] 201 | }) 202 | 203 | if len(clusters) > 0 { 204 | var clusterNames []string 205 | for _, c := range clusters { 206 | arnSplit := strings.Split(c, "/") 207 | name := arnSplit[len(arnSplit)-1] 208 | clusterNames = append(clusterNames, name) 209 | } 210 | 211 | selection, err := selectCluster(clusterNames) 212 | if err != nil { 213 | e.err <- err 214 | return 215 | } 216 | 217 | e.cluster = selection 218 | e.input <- "getService" 219 | return 220 | 221 | } else { 222 | err := errors.New("no clusters found in account or region") 223 | e.err <- err 224 | return 225 | } 226 | } 227 | 228 | // Lists available services and prompts the user to select one 229 | func (e *App) getService() { 230 | var services []string 231 | var nextToken *string 232 | 233 | cliArg := viper.GetString("service") 234 | if cliArg != "" { 235 | e.service = cliArg 236 | e.input <- "getTask" 237 | viper.Set("service", "") // Reset the cli arg so user can navigate 238 | return 239 | } 240 | 241 | list, err := e.client.ListServices(context.TODO(), &ecs.ListServicesInput{ 242 | Cluster: aws.String(e.cluster), 243 | MaxResults: awsMaxResults, 244 | }) 245 | if err != nil { 246 | e.err <- err 247 | return 248 | } 249 | services = append(services, list.ServiceArns...) 250 | nextToken = list.NextToken 251 | 252 | if nextToken != nil { 253 | for { 254 | list, err := e.client.ListServices(context.TODO(), &ecs.ListServicesInput{ 255 | Cluster: aws.String(e.cluster), 256 | MaxResults: awsMaxResults, 257 | NextToken: nextToken, 258 | }) 259 | if err != nil { 260 | e.err <- err 261 | return 262 | } 263 | services = append(services, list.ServiceArns...) 264 | if list.NextToken == nil { 265 | break 266 | } else { 267 | nextToken = list.NextToken 268 | } 269 | } 270 | } 271 | 272 | // Sort the list of services alphabetically 273 | sort.Slice(services, func(i, j int) bool { 274 | return services[i] < services[j] 275 | }) 276 | 277 | if len(services) > 0 { 278 | var serviceNames []string 279 | 280 | for _, s := range services { 281 | arnSplit := strings.Split(s, "/") 282 | name := arnSplit[len(arnSplit)-1] 283 | serviceNames = append(serviceNames, name) 284 | } 285 | 286 | selection, err := selectService(serviceNames) 287 | if err != nil { 288 | e.err <- err 289 | return 290 | } 291 | 292 | if selection == backOpt { 293 | e.service = "" 294 | e.input <- "getCluster" 295 | return 296 | } 297 | 298 | e.service = selection 299 | e.input <- "getTask" 300 | return 301 | 302 | } else { 303 | // Continue without setting a service if no services are found in the cluster 304 | fmt.Printf(Yellow("\n%s"), "No services found in the cluster, returning all running tasks...\n") 305 | e.input <- "getTask" 306 | return 307 | } 308 | } 309 | 310 | // Lists tasks in a cluster and prompts the user to select one 311 | func (e *App) getTask() { 312 | var taskArns []string 313 | var nextToken *string 314 | 315 | var input *ecs.ListTasksInput 316 | 317 | cliArg := viper.GetString("task") 318 | if cliArg != "" { 319 | describe, err := e.client.DescribeTasks(context.TODO(), &ecs.DescribeTasksInput{ 320 | Cluster: aws.String(e.cluster), 321 | Tasks: []string{*aws.String(cliArg)}, 322 | }) 323 | if err != nil { 324 | e.err <- err 325 | return 326 | } 327 | if len(describe.Tasks) > 0 { 328 | e.task = &describe.Tasks[0] 329 | e.getContainerOS() 330 | e.input <- "getContainer" 331 | viper.Set("task", "") // Reset the cli arg so user can navigate 332 | return 333 | } else { 334 | fmt.Println(Red(fmt.Sprintf("\nTask with ID %s not found in cluster %s\n", cliArg, e.cluster))) 335 | e.input <- "getService" 336 | return 337 | } 338 | } 339 | 340 | // If no service has been set, or if ALL (*) services have been selected 341 | // then we don't need to specify a ServiceName 342 | if e.service == "" || e.service == "*" { 343 | input = &ecs.ListTasksInput{ 344 | Cluster: aws.String(e.cluster), 345 | MaxResults: awsMaxResults, 346 | } 347 | } else { 348 | input = &ecs.ListTasksInput{ 349 | Cluster: aws.String(e.cluster), 350 | ServiceName: aws.String(e.service), 351 | MaxResults: awsMaxResults, 352 | } 353 | } 354 | 355 | list, err := e.client.ListTasks(context.TODO(), input) 356 | if err != nil { 357 | e.err <- err 358 | return 359 | } 360 | 361 | taskArns = append(taskArns, list.TaskArns...) 362 | nextToken = list.NextToken 363 | 364 | if nextToken != nil { 365 | for { 366 | list, err := e.client.ListTasks(context.TODO(), &ecs.ListTasksInput{ 367 | Cluster: aws.String(e.cluster), 368 | MaxResults: awsMaxResults, 369 | NextToken: nextToken, 370 | }) 371 | if err != nil { 372 | e.err <- err 373 | return 374 | } 375 | taskArns = append(taskArns, list.TaskArns...) 376 | if list.NextToken == nil { 377 | break 378 | } else { 379 | nextToken = list.NextToken 380 | } 381 | } 382 | } 383 | 384 | e.tasks = make(map[string]*ecsTypes.Task) 385 | if len(taskArns) > 0 { 386 | describe, err := e.client.DescribeTasks(context.TODO(), &ecs.DescribeTasksInput{ 387 | Cluster: aws.String(e.cluster), 388 | Tasks: taskArns, 389 | }) 390 | if err != nil { 391 | e.err <- err 392 | return 393 | } 394 | 395 | for _, t := range describe.Tasks { 396 | task := t 397 | taskId := strings.Split(*t.TaskArn, "/")[2] 398 | e.tasks[taskId] = &task 399 | } 400 | 401 | selection, err := selectTask(e.tasks) 402 | if err != nil { 403 | e.err <- err 404 | return 405 | } 406 | 407 | if *selection.TaskArn == backOpt { 408 | // e.task = nil 409 | if e.service == "" { 410 | e.input <- "getCluster" 411 | return 412 | } 413 | e.input <- "getService" 414 | return 415 | } 416 | e.task = selection 417 | e.getContainerOS() 418 | e.input <- "getContainer" 419 | return 420 | 421 | } else { 422 | if e.service == "" { 423 | fmt.Println(Red(fmt.Sprintf("There are no running tasks in the cluster %s\n", e.cluster))) 424 | e.input <- "getCluster" 425 | return 426 | } else { 427 | fmt.Println(Red(fmt.Sprintf("\nThere are no running tasks for the service %s in cluster %s\n", e.service, e.cluster))) 428 | e.input <- "getService" 429 | return 430 | } 431 | } 432 | } 433 | 434 | // Lists containers in a task and prompts the user to select one (if there is more than 1 container) 435 | // otherwise returns the the only container in the task 436 | func (e *App) getContainer() { 437 | cliArg := viper.GetString("container") 438 | if cliArg != "" { 439 | for _, c := range e.task.Containers { 440 | container := c 441 | if *container.Name == cliArg { 442 | e.container = &container 443 | e.input <- "execute" 444 | return 445 | } 446 | } 447 | fmt.Println(Red(fmt.Sprintf("\nSupplied container with name %s not found in task %s, cluster %s\n", cliArg, *e.task.TaskArn, e.cluster))) 448 | } 449 | 450 | if len(e.task.Containers) > 1 { 451 | selection, err := selectContainer(&e.task.Containers) 452 | if err != nil { 453 | e.err <- err 454 | return 455 | } 456 | 457 | if *selection.Name == backOpt { 458 | e.input <- "getTask" 459 | return 460 | } 461 | 462 | e.container = selection 463 | e.input <- "execute" 464 | return 465 | 466 | } else { 467 | // There is only one container in the task, return it 468 | e.container = &e.task.Containers[0] 469 | e.input <- "execute" 470 | return 471 | } 472 | } 473 | 474 | // Determines the OS family of the container instance the task is running on 475 | func (e *App) getContainerOS() { 476 | // Get associated task definition and determine OS family if EC2 launch-type 477 | if e.task.LaunchType == "EC2" { 478 | family, err := getPlatformFamily(e.client, e.task) 479 | if err != nil { 480 | e.err <- err 481 | return 482 | } 483 | // if the OperatingSystemFamily has not been specified in the task definition 484 | // then we refer to the container instance to determine the OS 485 | if family == "" { 486 | ec2Client := createEC2Client() 487 | family, err = getContainerInstanceOS(e.client, ec2Client, e.cluster, *e.task.ContainerInstanceArn) 488 | if err != nil { 489 | e.err <- err 490 | return 491 | } 492 | } 493 | // Add our own PlatformFamily value for the task struct 494 | e.task.PlatformFamily = &family 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /internal/app_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/ecs" 11 | ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func init() { 16 | os.Setenv("AWS_DEFAULT_REGION", "eu-west-1") 17 | } 18 | 19 | type ECSClientMock struct { 20 | ListClustersMock func(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) 21 | ListServicesMock func(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) 22 | ListTasksMock func(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) 23 | DescribeTasksMock func(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) 24 | DescribeTaskDefinitionMock func(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) 25 | DescribeContainerInstancesMock func(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) 26 | ExecuteCommandMock func(ctx context.Context, params *ecs.ExecuteCommandInput, optFns ...func(*ecs.Options)) (*ecs.ExecuteCommandOutput, error) 27 | } 28 | 29 | func (m ECSClientMock) ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) { 30 | return m.ListClustersMock(ctx, params, optFns...) 31 | } 32 | 33 | func (m ECSClientMock) ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) { 34 | return m.ListServicesMock(ctx, params, optFns...) 35 | } 36 | 37 | func (m ECSClientMock) ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) { 38 | return m.ListTasksMock(ctx, params, optFns...) 39 | } 40 | 41 | func (m ECSClientMock) DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { 42 | return m.DescribeTasksMock(ctx, params, optFns...) 43 | } 44 | 45 | func (m ECSClientMock) DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { 46 | return m.DescribeTaskDefinitionMock(ctx, params, optFns...) 47 | } 48 | 49 | func (m ECSClientMock) DescribeContainerInstances(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) { 50 | return m.DescribeContainerInstancesMock(ctx, params, optFns...) 51 | } 52 | func (m ECSClientMock) ExecuteCommand(ctx context.Context, params *ecs.ExecuteCommandInput, optFns ...func(*ecs.Options)) (*ecs.ExecuteCommandOutput, error) { 53 | return m.ExecuteCommandMock(ctx, params, optFns...) 54 | } 55 | 56 | // CreateMockApp initialises a new App struct and takes a MockClient as an argument - only used in tests 57 | func CreateMockApp(c ECSClient) *App { 58 | e := &App{ 59 | input: make(chan string, 1), 60 | err: make(chan error, 1), 61 | exit: make(chan error, 1), 62 | client: c, 63 | region: "eu-west-1", 64 | } 65 | 66 | return e 67 | } 68 | 69 | func TestGetCluster(t *testing.T) { 70 | paginationCall := 0 71 | cases := []struct { 72 | name string 73 | client func(t *testing.T) ECSClient 74 | expected string 75 | }{ 76 | { 77 | name: "TestGetClusterWithResultsPaginated", 78 | client: func(t *testing.T) ECSClient { 79 | return ECSClientMock{ 80 | ListClustersMock: func(ctx context.Context, input *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) { 81 | var clusters []string 82 | for i := paginationCall; i < (paginationCall * 100); i++ { 83 | clusters = append(clusters, *aws.String(fmt.Sprintf("arn:aws:ecs:eu-west-1:1111111111:cluster/test-cluster-%d", i))) 84 | } 85 | paginationCall = paginationCall + 1 86 | if paginationCall > 2 { 87 | return &ecs.ListClustersOutput{ 88 | ClusterArns: clusters, 89 | NextToken: nil, 90 | }, nil 91 | } 92 | return &ecs.ListClustersOutput{ 93 | ClusterArns: clusters, 94 | NextToken: aws.String("test-token"), 95 | }, nil 96 | }, 97 | } 98 | }, 99 | expected: "test-cluster-101", 100 | }, 101 | } 102 | 103 | for _, c := range cases { 104 | client := c.client(t) 105 | input := CreateMockApp(client) 106 | input.getCluster() 107 | if ok := assert.Equal(t, c.expected, input.cluster); ok != true { 108 | fmt.Printf("%s FAILED\n", c.name) 109 | } 110 | fmt.Printf("%s PASSED\n", c.name) 111 | } 112 | } 113 | 114 | func TestGetService(t *testing.T) { 115 | paginationCall := 1 116 | cases := []struct { 117 | name string 118 | client func(t *testing.T) ECSClient 119 | expected string 120 | }{ 121 | { 122 | name: "TestGetServiceWithResults", 123 | client: func(t *testing.T) ECSClient { 124 | return ECSClientMock{ 125 | ListServicesMock: func(ctx context.Context, input *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) { 126 | return &ecs.ListServicesOutput{ 127 | ServiceArns: []string{ 128 | *aws.String("arn:aws:ecs:eu-west-1:1111111111:cluster/App/test-service-1"), 129 | *aws.String("arn:aws:ecs:eu-west-1:1111111111:cluster/blueGreen/test-service-2"), 130 | }, 131 | }, nil 132 | }, 133 | } 134 | }, 135 | expected: "test-service-1", 136 | }, 137 | { 138 | name: "TestGetServiceWithResultsPaginated", 139 | client: func(t *testing.T) ECSClient { 140 | return ECSClientMock{ 141 | ListServicesMock: func(ctx context.Context, input *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) { 142 | var services []string 143 | for i := paginationCall; i < (paginationCall * 100); i++ { 144 | services = append(services, *aws.String(fmt.Sprintf("arn:aws:ecs:eu-west-1:1111111111:cluster/App/test-service-%d", i))) 145 | } 146 | paginationCall = paginationCall + 1 147 | if paginationCall > 2 { 148 | return &ecs.ListServicesOutput{ 149 | ServiceArns: services, 150 | NextToken: nil, 151 | }, nil 152 | } 153 | return &ecs.ListServicesOutput{ 154 | ServiceArns: services, 155 | NextToken: aws.String("test-token"), 156 | }, nil 157 | }, 158 | } 159 | }, 160 | expected: "test-service-101", 161 | }, 162 | { 163 | name: "TestGetServiceWithoutResults", 164 | client: func(t *testing.T) ECSClient { 165 | return ECSClientMock{ 166 | ListServicesMock: func(ctx context.Context, input *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) { 167 | return &ecs.ListServicesOutput{ 168 | ServiceArns: []string{}, 169 | }, nil 170 | }, 171 | } 172 | }, 173 | expected: "", 174 | }, 175 | } 176 | 177 | for _, c := range cases { 178 | client := c.client(t) 179 | input := CreateMockApp(client) 180 | input.cluster = "App" 181 | input.getService() 182 | if ok := assert.Equal(t, c.expected, input.service); ok != true { 183 | fmt.Printf("%s FAILED\n", c.name) 184 | } 185 | fmt.Printf("%s PASSED\n", c.name) 186 | } 187 | } 188 | 189 | func TestGetTask(t *testing.T) { 190 | paginationCall := 1 191 | cases := []struct { 192 | name string 193 | client func(t *testing.T) ECSClient 194 | expected *ecsTypes.Task 195 | }{ 196 | { 197 | name: "TestGetTaskWithResults", 198 | client: func(t *testing.T) ECSClient { 199 | return ECSClientMock{ 200 | ListTasksMock: func(ctx context.Context, input *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) { 201 | return &ecs.ListTasksOutput{ 202 | TaskArns: []string{ 203 | *aws.String("arn:aws:ecs:eu-west-1:111111111111:task/App/8a58117dac38436ba5547e9da5d3ac3d"), 204 | }, 205 | }, nil 206 | }, 207 | DescribeTasksMock: func(ctx context.Context, input *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { 208 | var tasks []ecsTypes.Task 209 | for _, taskArn := range input.Tasks { 210 | tasks = append(tasks, ecsTypes.Task{TaskArn: &taskArn, LaunchType: ecsTypes.LaunchTypeFargate}) 211 | } 212 | return &ecs.DescribeTasksOutput{ 213 | Tasks: tasks, 214 | }, nil 215 | }, 216 | } 217 | }, 218 | expected: &ecsTypes.Task{ 219 | TaskArn: aws.String("arn:aws:ecs:eu-west-1:111111111111:task/App/8a58117dac38436ba5547e9da5d3ac3d"), 220 | LaunchType: ecsTypes.LaunchTypeFargate, 221 | }, 222 | }, 223 | { 224 | name: "TestGetTaskWithResultsPaginated", 225 | client: func(t *testing.T) ECSClient { 226 | return ECSClientMock{ 227 | ListTasksMock: func(ctx context.Context, input *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) { 228 | var taskArns []string 229 | var tasks []*ecsTypes.Task 230 | for i := paginationCall; i < (paginationCall * 100); i++ { 231 | taskArn := *aws.String(fmt.Sprintf("arn:aws:ecs:eu-west-1:111111111111:task/App/%d", i)) 232 | taskArns = append(taskArns, taskArn) 233 | tasks = append(tasks, &ecsTypes.Task{TaskArn: &taskArn, LaunchType: ecsTypes.LaunchTypeFargate}) 234 | } 235 | paginationCall = paginationCall + 1 236 | if paginationCall > 2 { 237 | return &ecs.ListTasksOutput{ 238 | TaskArns: taskArns, 239 | NextToken: nil, 240 | }, nil 241 | } 242 | return &ecs.ListTasksOutput{ 243 | TaskArns: taskArns, 244 | NextToken: aws.String("test-token"), 245 | }, nil 246 | }, 247 | DescribeTasksMock: func(ctx context.Context, input *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { 248 | var tasks []ecsTypes.Task 249 | for _, taskArn := range input.Tasks { 250 | tasks = append(tasks, ecsTypes.Task{TaskArn: &taskArn, LaunchType: ecsTypes.LaunchTypeFargate}) 251 | } 252 | return &ecs.DescribeTasksOutput{ 253 | Tasks: tasks, 254 | }, nil 255 | }, 256 | } 257 | }, 258 | expected: &ecsTypes.Task{ 259 | TaskArn: aws.String("arn:aws:ecs:eu-west-1:111111111111:task/App/199"), 260 | LaunchType: ecsTypes.LaunchTypeFargate, 261 | }, 262 | }, 263 | { 264 | name: "TestGetTaskWithoutResults", 265 | client: func(t *testing.T) ECSClient { 266 | return ECSClientMock{ 267 | ListTasksMock: func(ctx context.Context, input *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) { 268 | return &ecs.ListTasksOutput{ 269 | TaskArns: []string{}, 270 | }, nil 271 | }, 272 | } 273 | }, 274 | expected: nil, 275 | }, 276 | } 277 | 278 | for _, c := range cases { 279 | client := c.client(t) 280 | input := CreateMockApp(client) 281 | input.cluster = "App" 282 | input.service = "test-service-1" 283 | input.getTask() 284 | if ok := assert.Equal(t, c.expected, input.task); ok != true { 285 | fmt.Printf("%s FAILED\n", c.name) 286 | } 287 | fmt.Printf("%s PASSED\n", c.name) 288 | } 289 | } 290 | 291 | func TestGetContainer(t *testing.T) { 292 | cases := []struct { 293 | name string 294 | client func(t *testing.T) ECSClient 295 | task *ecsTypes.Task 296 | expected *ecsTypes.Container 297 | }{ 298 | { 299 | name: "TestGetContainerWithMultipleContainers", 300 | client: func(t *testing.T) ECSClient { 301 | return ECSClientMock{} 302 | }, 303 | task: &ecsTypes.Task{ 304 | Containers: []ecsTypes.Container{ 305 | { 306 | Name: aws.String("echo-server"), 307 | }, 308 | { 309 | Name: aws.String("redis"), 310 | }, 311 | }, 312 | }, 313 | expected: &ecsTypes.Container{ 314 | Name: aws.String("echo-server"), 315 | }, 316 | }, 317 | { 318 | name: "TestGetContainerWithSingleContainer", 319 | client: func(t *testing.T) ECSClient { 320 | return ECSClientMock{} 321 | }, 322 | task: &ecsTypes.Task{ 323 | Containers: []ecsTypes.Container{ 324 | { 325 | Name: aws.String("nginx"), 326 | }, 327 | }, 328 | }, 329 | expected: &ecsTypes.Container{ 330 | Name: aws.String("nginx"), 331 | }, 332 | }, 333 | } 334 | 335 | for _, c := range cases { 336 | client := c.client(t) 337 | input := CreateMockApp(client) 338 | input.task = c.task 339 | input.getContainer() 340 | if ok := assert.Equal(t, c.expected, input.container); ok != true { 341 | fmt.Printf("%s FAILED\n", c.name) 342 | } 343 | fmt.Printf("%s PASSED\n", c.name) 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /internal/aws.go: -------------------------------------------------------------------------------- 1 | /* aws.go contains AWS Client creation funcs and other helpers used by the main app */ 2 | 3 | package app 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/aws/retry" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/service/ec2" 13 | "github.com/aws/aws-sdk-go-v2/service/ecs" 14 | ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | type EC2Client interface { 19 | DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) 20 | } 21 | 22 | type ECSClient interface { 23 | ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) 24 | ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) 25 | ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) 26 | DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) 27 | DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) 28 | DescribeContainerInstances(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) 29 | ExecuteCommand(ctx context.Context, params *ecs.ExecuteCommandInput, optFns ...func(*ecs.Options)) (*ecs.ExecuteCommandOutput, error) 30 | } 31 | 32 | func createEcsClient() *ecs.Client { 33 | region := viper.GetString("region") 34 | getCustomAWSEndpoint := func(o *ecs.Options) { 35 | endpointUrl := viper.GetString("aws-endpoint-url") 36 | if endpointUrl != "" { 37 | o.BaseEndpoint = aws.String(endpointUrl) 38 | } 39 | } 40 | cfg, err := config.LoadDefaultConfig(context.Background(), 41 | config.WithSharedConfigProfile(viper.GetString("profile")), 42 | config.WithRegion(region), 43 | config.WithRetryer(func() aws.Retryer { 44 | return retry.AddWithMaxBackoffDelay(retry.NewStandard(), time.Second*1) 45 | }), 46 | ) 47 | if err != nil { 48 | panic(err) 49 | } 50 | client := ecs.NewFromConfig(cfg, getCustomAWSEndpoint) 51 | 52 | return client 53 | } 54 | 55 | func createEC2Client() *ec2.Client { 56 | region := viper.GetString("region") 57 | getCustomAWSEndpoint := func(o *ec2.Options) { 58 | endpointUrl := viper.GetString("aws-endpoint-url") 59 | if endpointUrl != "" { 60 | o.BaseEndpoint = aws.String(endpointUrl) 61 | } 62 | } 63 | cfg, err := config.LoadDefaultConfig(context.Background(), 64 | config.WithSharedConfigProfile(viper.GetString("profile")), 65 | config.WithRegion(region), 66 | config.WithRetryer(func() aws.Retryer { 67 | return retry.AddWithMaxBackoffDelay(retry.NewStandard(), time.Second*1) 68 | }), 69 | ) 70 | if err != nil { 71 | panic(err) 72 | } 73 | client := ec2.NewFromConfig(cfg, getCustomAWSEndpoint) 74 | 75 | return client 76 | } 77 | 78 | // getPlatformFamily checks an ECS tasks properties to see if the OS can be derived from its properties, otherwise 79 | // it will check the container instance itself to determine the OS. 80 | func getPlatformFamily(client ECSClient, task *ecsTypes.Task) (string, error) { 81 | taskDefinition, err := client.DescribeTaskDefinition(context.TODO(), &ecs.DescribeTaskDefinitionInput{ 82 | TaskDefinition: task.TaskDefinitionArn, 83 | }) 84 | if err != nil { 85 | return "", err 86 | } 87 | if taskDefinition.TaskDefinition.RuntimePlatform != nil { 88 | return string(taskDefinition.TaskDefinition.RuntimePlatform.OperatingSystemFamily), nil 89 | } 90 | return "", nil 91 | } 92 | 93 | // getContainerInstanceOS describes the specified container instance and checks against the backing EC2 instance 94 | // to determine the platform. 95 | func getContainerInstanceOS(ecsClient ECSClient, ec2Client EC2Client, cluster string, containerInstanceArn string) (string, error) { 96 | res, err := ecsClient.DescribeContainerInstances(context.TODO(), &ecs.DescribeContainerInstancesInput{ 97 | Cluster: aws.String(cluster), 98 | ContainerInstances: []string{ 99 | *aws.String(containerInstanceArn), 100 | }, 101 | }) 102 | if err != nil { 103 | return "", err 104 | } 105 | instanceId := res.ContainerInstances[0].Ec2InstanceId 106 | instance, _ := ec2Client.DescribeInstances(context.TODO(), &ec2.DescribeInstancesInput{ 107 | InstanceIds: []string{ 108 | *instanceId, 109 | }, 110 | }) 111 | operatingSystem := *instance.Reservations[0].Instances[0].PlatformDetails 112 | return operatingSystem, nil 113 | } 114 | 115 | func getContainerPort(client ECSClient, taskDefinitionArn string, containerName string) (*int32, error) { 116 | res, err := client.DescribeTaskDefinition(context.TODO(), &ecs.DescribeTaskDefinitionInput{ 117 | TaskDefinition: aws.String(taskDefinitionArn), 118 | }) 119 | if err != nil { 120 | return nil, err 121 | } 122 | var container ecsTypes.ContainerDefinition 123 | for _, c := range res.TaskDefinition.ContainerDefinitions { 124 | if *c.Name == containerName { 125 | container = c 126 | } 127 | } 128 | return container.PortMappings[0].ContainerPort, nil 129 | } 130 | -------------------------------------------------------------------------------- /internal/aws_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/ec2" 10 | ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 11 | "github.com/aws/aws-sdk-go-v2/service/ecs" 12 | ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | type EC2ClientMock struct { 17 | DescribeInstancesMock func(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) 18 | } 19 | 20 | func (m EC2ClientMock) DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { 21 | return m.DescribeInstancesMock(ctx, params, optFns...) 22 | } 23 | 24 | func TestGetPlatformFamily(t *testing.T) { 25 | cases := []struct { 26 | name string 27 | expected string 28 | client func(t *testing.T) ECSClient 29 | cluster string 30 | task *ecsTypes.Task 31 | }{ 32 | { 33 | name: "TestGetPlatformFamilyWithFargateTask", 34 | cluster: "test", 35 | task: &ecsTypes.Task{ 36 | TaskArn: aws.String("arn:aws:ecs:eu-west-1:111111111111:task/App/8a58117dac38436ba5547e9da5d3ac3d"), 37 | LaunchType: ecsTypes.LaunchTypeFargate, 38 | PlatformFamily: aws.String("Linux"), 39 | }, 40 | client: func(t *testing.T) ECSClient { 41 | return ECSClientMock{ 42 | DescribeTaskDefinitionMock: func(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { 43 | return &ecs.DescribeTaskDefinitionOutput{ 44 | TaskDefinition: &ecsTypes.TaskDefinition{ 45 | RuntimePlatform: &ecsTypes.RuntimePlatform{ 46 | OperatingSystemFamily: ecsTypes.OSFamilyLinux, 47 | }, 48 | }, 49 | }, nil 50 | }, 51 | } 52 | }, 53 | expected: "LINUX", 54 | }, 55 | { 56 | name: "TestGetPlatformFamilyWithEC2LaunchTaskNoRuntimePlatformFail", 57 | cluster: "test", 58 | task: &ecsTypes.Task{ 59 | TaskArn: aws.String("arn:aws:ecs:eu-west-1:111111111111:task/App/8a58117dac38436ba5547e9da5d3ac3d"), 60 | LaunchType: ecsTypes.LaunchTypeEc2, 61 | ContainerInstanceArn: aws.String("abcdefghij1234567890"), 62 | }, 63 | client: func(t *testing.T) ECSClient { 64 | return ECSClientMock{ 65 | DescribeTaskDefinitionMock: func(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { 66 | return &ecs.DescribeTaskDefinitionOutput{ 67 | TaskDefinition: &ecsTypes.TaskDefinition{}, 68 | }, nil 69 | }, 70 | } 71 | }, 72 | expected: "", 73 | }, 74 | } 75 | 76 | for _, c := range cases { 77 | client := c.client(t) 78 | res, _ := getPlatformFamily(client, c.task) 79 | if ok := assert.Equal(t, c.expected, res); ok != true { 80 | fmt.Printf("%s FAILED\n", c.name) 81 | } 82 | fmt.Printf("%s PASSED\n", c.name) 83 | } 84 | } 85 | 86 | func TestGetContainerInstanceOS(t *testing.T) { 87 | cases := []struct { 88 | name string 89 | expected string 90 | ecsClient func(t *testing.T) ECSClient 91 | ec2Client func(t *testing.T) EC2Client 92 | cluster string 93 | containerInstanceArn string 94 | }{ 95 | { 96 | name: "TestGetContainerInstanceOS", 97 | cluster: "test", 98 | containerInstanceArn: "abcdef123456", 99 | ecsClient: func(t *testing.T) ECSClient { 100 | return ECSClientMock{ 101 | DescribeContainerInstancesMock: func(ctx context.Context, input *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) { 102 | return &ecs.DescribeContainerInstancesOutput{ 103 | ContainerInstances: []ecsTypes.ContainerInstance{ 104 | { 105 | Ec2InstanceId: aws.String("i-0063cc3b62343f4d1"), 106 | }, 107 | }, 108 | }, nil 109 | }, 110 | } 111 | }, 112 | ec2Client: func(t *testing.T) EC2Client { 113 | return EC2ClientMock{ 114 | DescribeInstancesMock: func(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { 115 | return &ec2.DescribeInstancesOutput{ 116 | Reservations: []ec2Types.Reservation{ 117 | { 118 | Instances: []ec2Types.Instance{ 119 | { 120 | InstanceId: aws.String("i-0063cc3b62343f4d1"), 121 | PlatformDetails: aws.String("Linux/UNIX"), 122 | }, 123 | }, 124 | }, 125 | }}, nil 126 | }, 127 | } 128 | }, 129 | expected: "Linux/UNIX", 130 | }, 131 | } 132 | 133 | for _, c := range cases { 134 | ecsClient := c.ecsClient(t) 135 | ec2Client := c.ec2Client(t) 136 | res, _ := getContainerInstanceOS(ecsClient, ec2Client, c.cluster, c.containerInstanceArn) 137 | if ok := assert.Equal(t, c.expected, res); ok != true { 138 | fmt.Printf("%s FAILED\n", c.name) 139 | } 140 | fmt.Printf("%s PASSED\n", c.name) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /internal/command.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/ecs" 11 | "github.com/aws/aws-sdk-go-v2/service/ssm" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | // executeCommand takes the app state and builds an execute-command session for us 16 | // which is then passed to the session-manager-plugin for execution 17 | func (e *App) executeCommand() error { 18 | var command string 19 | if viper.GetString("cmd") != "" { 20 | command = viper.GetString("cmd") 21 | } else { 22 | if strings.Contains(strings.ToLower(*e.task.PlatformFamily), "windows") { 23 | command = "powershell.exe" 24 | } else { 25 | command = "/bin/sh" 26 | } 27 | } 28 | App, err := e.client.ExecuteCommand(context.TODO(), &ecs.ExecuteCommandInput{ 29 | Cluster: aws.String(e.cluster), 30 | Interactive: *aws.Bool(true), 31 | Task: e.task.TaskArn, 32 | Command: aws.String(command), 33 | Container: e.container.Name, 34 | }) 35 | 36 | if err != nil { 37 | e.err <- err 38 | return err 39 | } 40 | 41 | execSess, err := json.MarshalIndent(App.Session, "", " ") 42 | if err != nil { 43 | e.err <- err 44 | return err 45 | } 46 | 47 | taskArnSplit := strings.Split(*e.task.TaskArn, "/") 48 | taskID := taskArnSplit[len(taskArnSplit)-1] 49 | target := ssm.StartSessionInput{ 50 | Target: aws.String(fmt.Sprintf("ecs:%s_%s_%s", e.cluster, taskID, *e.container.RuntimeId)), 51 | } 52 | 53 | targetJson, err := json.MarshalIndent(target, "", " ") 54 | if err != nil { 55 | e.err <- err 56 | return err 57 | } 58 | 59 | // Print Cluster/Service/Task information to the console 60 | if !viper.GetBool("quiet") { 61 | fmt.Printf("\nCluster: %v | Service: %v | Task: %s | Cmd: %s", Cyan(e.cluster), Magenta(e.service), Green(strings.Split(*e.task.TaskArn, "/")[2]), Yellow(command)) 62 | fmt.Printf("\nConnecting to container %v\n", Yellow(*e.container.Name)) 63 | } 64 | 65 | // Execute the session-manager-plugin with our task details 66 | err = runCommand("session-manager-plugin", string(execSess), e.region, "StartSession", "", string(targetJson)) 67 | e.err <- err 68 | 69 | return err 70 | } 71 | -------------------------------------------------------------------------------- /internal/command_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/ecs" 10 | ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestExecuteInput(t *testing.T) { 15 | cases := []struct { 16 | name string 17 | expected error 18 | client func(t *testing.T) ECSClient 19 | cluster string 20 | task *ecsTypes.Task 21 | }{ 22 | { 23 | name: "TestExecuteInput", 24 | cluster: "test", 25 | task: &ecsTypes.Task{ 26 | TaskArn: aws.String("arn:aws:ecs:eu-west-1:111111111111:task/App/8a58117dac38436ba5547e9da5d3ac3d"), 27 | Containers: []ecsTypes.Container{ 28 | { 29 | Name: aws.String("nginx"), 30 | RuntimeId: aws.String("544e08d919364be9926186b086c29868-2531612879"), 31 | }, 32 | }, 33 | PlatformFamily: aws.String("Linux"), 34 | }, 35 | client: func(t *testing.T) ECSClient { 36 | return ECSClientMock{ 37 | ExecuteCommandMock: func(ctx context.Context, input *ecs.ExecuteCommandInput, optFns ...func(*ecs.Options)) (*ecs.ExecuteCommandOutput, error) { 38 | return &ecs.ExecuteCommandOutput{ 39 | Session: &ecsTypes.Session{ 40 | SessionId: aws.String("ecs-execute-command-0e86561fddf625dc1"), 41 | StreamUrl: aws.String("wss://ssmmessages.eu-west-1.amazonaws.com/v1/data-channel/ecs-execute-command-blah"), 42 | TokenValue: aws.String("abc123"), 43 | }, 44 | }, nil 45 | }, 46 | } 47 | }, 48 | expected: nil, 49 | }, 50 | } 51 | 52 | for _, c := range cases { 53 | app := &App{ 54 | input: make(chan string, 1), 55 | err: make(chan error, 1), 56 | exit: make(chan error, 1), 57 | client: c.client(t), 58 | region: "eu-west-1", 59 | endpoint: "ecs.eu-west-1.amazonaws.com", 60 | cluster: c.cluster, 61 | task: c.task, 62 | } 63 | app.container = &c.task.Containers[0] 64 | err := app.executeCommand() 65 | if ok := assert.Equal(t, c.expected, err); ok != true { 66 | fmt.Printf("%s FAILED\n", c.name) 67 | } 68 | fmt.Printf("%s PASSED\n", c.name) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/forward.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/aws/aws-sdk-go-v2/service/ecs" 12 | "github.com/aws/aws-sdk-go-v2/service/ssm" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | // executeForward takes the app state and builds a port-forward session for us 17 | // which is then passed to the session-manager-plugin for execution 18 | func (e *App) executeForward() error { 19 | taskArnSplit := strings.Split(*e.task.TaskArn, "/") 20 | taskID := taskArnSplit[len(taskArnSplit)-1] 21 | target := ssm.StartSessionInput{ 22 | Target: aws.String(fmt.Sprintf("ecs:%s_%s_%s", e.cluster, taskID, *e.container.RuntimeId)), 23 | } 24 | 25 | cfg, err := config.LoadDefaultConfig(context.Background(), 26 | config.WithSharedConfigProfile(viper.GetString("profile")), 27 | config.WithRegion(region), 28 | ) 29 | if err != nil { 30 | panic(err) 31 | } 32 | client := ssm.NewFromConfig(cfg) // TODO: add region 33 | ecsClient := e.client.(*ecs.Client) 34 | containerPort, err := getContainerPort(ecsClient, *e.task.TaskDefinitionArn, *e.container.Name) 35 | if err != nil { 36 | e.err <- err 37 | return err 38 | } 39 | localPort := viper.GetString("local-port") 40 | if localPort == "" { 41 | localPort, err = inputLocalPort() 42 | if err != nil { 43 | e.err <- err 44 | return err 45 | } 46 | } 47 | portNumber := fmt.Sprint(*containerPort) 48 | input := &ssm.StartSessionInput{ 49 | DocumentName: aws.String("AWS-StartPortForwardingSession"), 50 | Parameters: map[string][]string{ 51 | "localPortNumber": {localPort}, 52 | "portNumber": {portNumber}, 53 | }, 54 | Target: aws.String(fmt.Sprintf("ecs:%s_%s_%s", e.cluster, taskID, *e.container.RuntimeId)), 55 | } 56 | sess, err := client.StartSession(context.TODO(), input) 57 | if err != nil { 58 | e.err <- err 59 | return err 60 | } 61 | sessJson, err := json.Marshal(sess) 62 | if err != nil { 63 | e.err <- err 64 | return err 65 | } 66 | paramsJson, err := json.Marshal(target) 67 | if err != nil { 68 | e.err <- err 69 | return err 70 | } 71 | 72 | // Print Cluster/Service/Task information to the console 73 | if !viper.GetBool("quiet") { 74 | fmt.Printf("\nCluster: %v | Service: %v | Task: %s", Cyan(e.cluster), Magenta(e.service), Green(strings.Split(*e.task.TaskArn, "/")[2])) 75 | fmt.Printf("\nPort-forwarding %s:%d -> container %v\n", localPort, *containerPort, Yellow(*e.container.Name)) 76 | } 77 | 78 | // Execute the session-manager-plugin with our task details 79 | err = runCommand("session-manager-plugin", string(sessJson), e.region, "StartSession", "", string(paramsJson)) 80 | e.err <- err 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /internal/forward_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | -------------------------------------------------------------------------------- /internal/select.go: -------------------------------------------------------------------------------- 1 | /* select.go contains the logic for the Select/Survey views in the TUI app */ 2 | 3 | package app 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/AlecAivazis/survey/v2" 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | 13 | ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" 14 | ) 15 | 16 | func init() { 17 | survey.SelectQuestionTemplate = ` 18 | {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} 19 | {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} 20 | {{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}} 21 | {{- if .ShowAnswer}}{{color "Cyan"}} {{""}}{{color "reset"}} 22 | {{- else}} 23 | {{- " "}}{{- color "Cyan"}}[Type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}{{- "\n"}} 24 | {{- range $ix, $choice := .PageEntries}} 25 | {{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}} 26 | {{- $choice.Value}} 27 | {{- color "reset"}}{{"\n"}} 28 | {{- end}} 29 | {{- end}}` 30 | } 31 | 32 | // createOpts builds the initial options for the survey prompts 33 | func createOpts(opts []string) []string { 34 | initialOpts := []string{backOpt} 35 | return append(initialOpts, opts...) 36 | } 37 | 38 | // selectCluster provides the prompt for choosing a cluster 39 | func selectCluster(clusterNames []string) (string, error) { 40 | if flag.Lookup("test.v") != nil { 41 | if len(clusterNames) > 100 { 42 | // For Pagination testing, after sorting alphabetically, the 101st cluster is at index 4, and proves 43 | // that the pagination is working correctly 44 | return clusterNames[4], nil 45 | } 46 | return clusterNames[0], nil 47 | } 48 | 49 | prompt := &survey.Select{ 50 | Message: "Select a cluster:", 51 | Options: clusterNames, 52 | PageSize: pageSize, 53 | } 54 | 55 | var selection string 56 | err := survey.AskOne(prompt, &selection, survey.WithIcons(func(icons *survey.IconSet) { 57 | icons.SelectFocus.Text = "➡" 58 | icons.SelectFocus.Format = "cyan" 59 | })) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | return selection, nil 65 | } 66 | 67 | // selectService provides the prompt for choosing a service 68 | func selectService(serviceNames []string) (string, error) { 69 | if flag.Lookup("test.v") != nil { 70 | if len(serviceNames) > int(*awsMaxResults) { 71 | // For Pagination testing, after sorting alphabetically, the 101st service is at index 4, and proves 72 | // that the pagination is working correctly 73 | return serviceNames[4], nil 74 | } 75 | return serviceNames[0], nil 76 | } 77 | 78 | serviceNames = append(serviceNames, "*") 79 | 80 | prompt := &survey.Select{ 81 | Message: fmt.Sprintf("Select a service: %s", Yellow("(choose * to display all tasks)")), 82 | Options: createOpts(serviceNames), 83 | PageSize: pageSize, 84 | } 85 | 86 | var selection string 87 | err := survey.AskOne(prompt, &selection, survey.WithIcons(func(icons *survey.IconSet) { 88 | icons.SelectFocus.Text = "➡" 89 | icons.SelectFocus.Format = "magenta" 90 | })) 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | return selection, nil 96 | } 97 | 98 | // selectTask provides the prompt for choosing a Task 99 | func selectTask(tasks map[string]*ecsTypes.Task) (*ecsTypes.Task, error) { 100 | if flag.Lookup("test.v") != nil { 101 | // When testing pagination, we want to return a task from the second set of results, 102 | // which will prove pagination is working correctly 103 | if len(tasks) > int(*awsMaxResults) { 104 | return tasks["199"], nil 105 | } 106 | for _, t := range tasks { 107 | return t, nil // return the first value from the map 108 | } 109 | } 110 | 111 | var taskOpts []string 112 | for id, t := range tasks { 113 | taskDefinition := strings.Split(*t.TaskDefinitionArn, "/")[1] 114 | var containers []string 115 | for _, c := range t.Containers { 116 | containers = append(containers, *c.Name) 117 | } 118 | taskOpts = append(taskOpts, fmt.Sprintf("%s | %s | (%s)", id, taskDefinition, strings.Join(containers, ","))) 119 | } 120 | 121 | prompt := &survey.Select{ 122 | Message: "Select a task:", 123 | Options: createOpts(taskOpts), 124 | PageSize: pageSize, 125 | } 126 | 127 | var selection string 128 | err := survey.AskOne(prompt, &selection, survey.WithIcons(func(icons *survey.IconSet) { 129 | icons.SelectFocus.Text = "➡" 130 | icons.SelectFocus.Format = "green" 131 | })) 132 | if err != nil { 133 | return &ecsTypes.Task{}, err 134 | } 135 | 136 | if selection == backOpt { 137 | return &ecsTypes.Task{TaskArn: aws.String(backOpt)}, nil 138 | } 139 | 140 | taskId := strings.Split(selection, " | ")[0] 141 | task := tasks[taskId] 142 | 143 | return task, nil 144 | } 145 | 146 | // selectContainer prompts the user to choose a container within a task 147 | func selectContainer(containers *[]ecsTypes.Container) (*ecsTypes.Container, error) { 148 | if flag.Lookup("test.v") != nil { 149 | container := *containers 150 | return &container[0], nil 151 | } 152 | 153 | var containerNames []string 154 | for _, c := range *containers { 155 | containerNames = append(containerNames, *c.Name) 156 | } 157 | 158 | var selection string 159 | var prompt = &survey.Select{ 160 | Message: "Multiple containers found, please select:", 161 | Options: createOpts(containerNames), 162 | PageSize: pageSize, 163 | } 164 | 165 | err := survey.AskOne(prompt, &selection, survey.WithIcons(func(icons *survey.IconSet) { 166 | icons.SelectFocus.Text = "➡" 167 | icons.SelectFocus.Format = "yellow" 168 | })) 169 | if err != nil { 170 | return &ecsTypes.Container{}, err 171 | } 172 | if selection == backOpt { 173 | return &ecsTypes.Container{Name: aws.String(backOpt)}, nil 174 | } 175 | 176 | var container *ecsTypes.Container 177 | for _, c := range *containers { 178 | cont := c 179 | if selection == *cont.Name { 180 | container = &cont 181 | } 182 | } 183 | 184 | return container, nil 185 | } 186 | 187 | // inputLocalPort prompts the user to enter a port number for port-forwarding 188 | func inputLocalPort() (string, error) { 189 | if flag.Lookup("test.v") != nil { 190 | return "42069", nil 191 | } 192 | 193 | port := "" 194 | prompt := &survey.Input{ 195 | Message: "Enter the local port to be used for forwarding\n", 196 | } 197 | survey.AskOne(prompt, &port) 198 | 199 | return port, nil 200 | } 201 | -------------------------------------------------------------------------------- /test.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4" 8 | } 9 | } 10 | } 11 | 12 | 13 | #################### 14 | # DATA SOURCES 15 | #################### 16 | 17 | # add id parameter to specify a VPC, in my case I'm just testing using the default vpc 18 | data "aws_vpc" "test" {} 19 | 20 | data "aws_subnets" "test" { 21 | filter { 22 | name = "vpc-id" 23 | values = [data.aws_vpc.test.id] 24 | } 25 | } 26 | 27 | data "aws_ami" "ecs_optimized" { 28 | most_recent = true 29 | 30 | filter { 31 | name = "name" 32 | values = ["amzn2-ami-ecs-hvm-*-arm64-ebs"] 33 | } 34 | 35 | owners = ["amazon"] 36 | } 37 | 38 | data "aws_ami" "windows_ecs_optimized" { 39 | most_recent = true 40 | 41 | filter { 42 | name = "name" 43 | values = ["Windows_Server-2022-English-Core-ECS_Optimized-*"] 44 | } 45 | 46 | owners = ["amazon"] 47 | } 48 | 49 | #################### 50 | # IAM 51 | #################### 52 | 53 | # EC2 Instance IAM resources 54 | data "aws_iam_policy_document" "ecs_instance" { 55 | statement { 56 | actions = [ 57 | "cloudwatch:PutMetricData", 58 | ] 59 | 60 | resources = [ 61 | "*", 62 | ] 63 | } 64 | 65 | statement { 66 | actions = [ 67 | "logs:CreateLogGroup", 68 | "logs:CreateLogStream", 69 | "logs:PutLogEvents", 70 | "logs:DescribeLogStreams", 71 | ] 72 | 73 | resources = [ 74 | "*", 75 | ] 76 | } 77 | 78 | statement { 79 | actions = [ 80 | "ec2:*", 81 | "ecs:*", 82 | "kms:*", 83 | ] 84 | 85 | resources = [ 86 | "*", 87 | ] 88 | } 89 | } 90 | 91 | resource "aws_iam_role" "ecs_instance" { 92 | name = "test-ecsgo-instance-role" 93 | assume_role_policy = jsonencode({ 94 | Version = "2012-10-17" 95 | Statement = [ 96 | { 97 | Action = "sts:AssumeRole" 98 | Effect = "Allow" 99 | Principal = { 100 | Service = "ec2.amazonaws.com" 101 | } 102 | }, 103 | ] 104 | }) 105 | 106 | inline_policy { 107 | name = "instance-policy" 108 | policy = data.aws_iam_policy_document.ecs_instance.json 109 | } 110 | } 111 | 112 | resource "aws_iam_role_policy_attachment" "instance_ssm" { 113 | role = aws_iam_role.ecs_instance.name 114 | policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" 115 | } 116 | 117 | resource "aws_iam_instance_profile" "ecs_instance" { 118 | name = "test-ecsgo-instance" 119 | role = aws_iam_role.ecs_instance.name 120 | } 121 | 122 | # ECS Task IAM resources 123 | resource "aws_iam_role" "ecs_task" { 124 | name = "test-ecsgo-task-role" 125 | assume_role_policy = jsonencode({ 126 | Version = "2012-10-17" 127 | Statement = [ 128 | { 129 | Action = "sts:AssumeRole" 130 | Effect = "Allow" 131 | Sid = "" 132 | Principal = { 133 | Service = "ecs-tasks.amazonaws.com" 134 | } 135 | }, 136 | ] 137 | }) 138 | } 139 | 140 | resource "aws_iam_role_policy_attachment" "task_ssm" { 141 | role = aws_iam_role.ecs_task.name 142 | policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" 143 | } 144 | 145 | resource "aws_iam_role_policy_attachment" "task_ecs" { 146 | role = aws_iam_role.ecs_task.name 147 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 148 | } 149 | 150 | #################### 151 | # CLOUDWATCH 152 | #################### 153 | resource "aws_cloudwatch_log_group" "test" { 154 | name = "/aws/ecs/test-ecsgo" 155 | } 156 | 157 | #################### 158 | # EC2 159 | #################### 160 | resource "aws_security_group" "ecs_instance" { 161 | name = "test-ecsgo-instance-sg" 162 | description = "Security group for ecsgo test environment" 163 | vpc_id = data.aws_vpc.test.id 164 | 165 | egress { 166 | from_port = 0 167 | to_port = 0 168 | protocol = "-1" 169 | cidr_blocks = ["0.0.0.0/0"] 170 | ipv6_cidr_blocks = ["::/0"] 171 | } 172 | } 173 | 174 | resource "aws_launch_template" "ecs_instance" { 175 | name = "test-ecsgo-instance-lt" 176 | image_id = data.aws_ami.ecs_optimized.image_id 177 | instance_type = "t4g.medium" 178 | ebs_optimized = true 179 | instance_initiated_shutdown_behavior = "terminate" 180 | 181 | tag_specifications { 182 | resource_type = "instance" 183 | 184 | tags = { 185 | Name = "test-ecsgo-instance" 186 | } 187 | } 188 | 189 | iam_instance_profile { 190 | name = aws_iam_instance_profile.ecs_instance.id 191 | } 192 | 193 | monitoring { 194 | enabled = true 195 | } 196 | 197 | network_interfaces { 198 | associate_public_ip_address = true 199 | security_groups = [aws_security_group.ecs_instance.id] 200 | } 201 | 202 | user_data = base64encode(<<-EOF 203 | #!/usr/bin/env bash 204 | 205 | cat <> /etc/ecs/ecs.config 206 | ECS_CLUSTER=${aws_ecs_cluster.test.name} 207 | ECS_ENABLE_CONTAINER_METADATA=true 208 | ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM=ec2_instance 209 | EOF 210 | ) 211 | 212 | instance_market_options { 213 | market_type = "spot" 214 | } 215 | } 216 | 217 | resource "aws_autoscaling_group" "ecs" { 218 | name = "test-ecsgo-instance-asg" 219 | vpc_zone_identifier = data.aws_subnets.test.ids 220 | max_size = 1 221 | min_size = 0 222 | desired_capacity = 1 223 | 224 | launch_template { 225 | id = aws_launch_template.ecs_instance.id 226 | version = "$Latest" 227 | } 228 | 229 | health_check_grace_period = 300 230 | health_check_type = "EC2" 231 | 232 | lifecycle { 233 | create_before_destroy = true 234 | } 235 | } 236 | 237 | resource "aws_launch_template" "windows_ecs_instance" { 238 | name = "test-ecsgo-windows-instance-lt" 239 | image_id = data.aws_ami.windows_ecs_optimized.image_id 240 | instance_type = "m5.large" 241 | ebs_optimized = true 242 | instance_initiated_shutdown_behavior = "terminate" 243 | 244 | block_device_mappings { 245 | device_name = "/dev/sda1" 246 | ebs { 247 | volume_size = 100 248 | } 249 | } 250 | 251 | tag_specifications { 252 | resource_type = "instance" 253 | 254 | tags = { 255 | Name = "test-ecsgo-windows-instance" 256 | } 257 | } 258 | 259 | iam_instance_profile { 260 | name = aws_iam_instance_profile.ecs_instance.id 261 | } 262 | 263 | monitoring { 264 | enabled = true 265 | } 266 | 267 | network_interfaces { 268 | associate_public_ip_address = true 269 | security_groups = [aws_security_group.ecs_instance.id] 270 | } 271 | 272 | user_data = base64encode(<<-EOF 273 | 274 | Initialize-ECSAgent -Cluster ${aws_ecs_cluster.windows_test.name} -EnableTaskIAMRole -AwsvpcBlockIMDS -EnableTaskENI -LoggingDrivers '["json-file","awslogs"]' 275 | [Environment]::SetEnvironmentVariable("ECS_ENABLE_AWSLOGS_EXECUTIONROLE_OVERRIDE",$TRUE, "Machine") 276 | 277 | EOF 278 | ) 279 | 280 | instance_market_options { 281 | market_type = "spot" 282 | } 283 | } 284 | 285 | resource "aws_autoscaling_group" "windows_ecs" { 286 | name = "test-ecsgo-windows-instance-asg" 287 | vpc_zone_identifier = data.aws_subnets.test.ids 288 | max_size = 1 289 | min_size = 0 290 | desired_capacity = 1 291 | 292 | launch_template { 293 | id = aws_launch_template.windows_ecs_instance.id 294 | version = "$Latest" 295 | } 296 | 297 | health_check_grace_period = 300 298 | health_check_type = "EC2" 299 | 300 | lifecycle { 301 | create_before_destroy = true 302 | } 303 | } 304 | 305 | #################### 306 | # ECS 307 | #################### 308 | resource "aws_ecs_cluster" "test" { 309 | name = "test-ecsgo" 310 | } 311 | 312 | resource "aws_ecs_cluster" "test_pagination" { 313 | for_each = toset([for i in range(1, 10) : tostring(i)]) 314 | name = "test-ecsgo-${each.key}" 315 | } 316 | 317 | resource "aws_ecs_cluster" "windows_test" { 318 | name = "test-windows-ecsgo" 319 | } 320 | 321 | resource "aws_ecs_cluster_capacity_providers" "test" { 322 | cluster_name = aws_ecs_cluster.test.name 323 | 324 | capacity_providers = ["FARGATE"] 325 | 326 | default_capacity_provider_strategy { 327 | base = 1 328 | weight = 100 329 | capacity_provider = "FARGATE" 330 | } 331 | } 332 | 333 | resource "aws_ecs_task_definition" "fargate" { 334 | family = "test-ecsgo-fargate" 335 | task_role_arn = aws_iam_role.ecs_task.arn 336 | execution_role_arn = aws_iam_role.ecs_task.arn 337 | requires_compatibilities = ["FARGATE"] 338 | network_mode = "awsvpc" 339 | cpu = 1024 340 | memory = 2048 341 | 342 | container_definitions = jsonencode([ 343 | { 344 | name = "nginx" 345 | image = "nginx:latest" 346 | cpu = 256 347 | memory = 512 348 | essential = true 349 | portMappings = [ 350 | { 351 | containerPort = 80 352 | hostPort = 80 353 | } 354 | ] 355 | logConfiguration = { 356 | logDriver = "awslogs" 357 | options = { 358 | "awslogs-group" : aws_cloudwatch_log_group.test.id 359 | "awslogs-region" : "eu-west-1" 360 | "awslogs-stream-prefix" : "ecs" 361 | } 362 | } 363 | }, 364 | { 365 | name = "redis" 366 | image = "redis:latest" 367 | cpu = 256 368 | memory = 512 369 | essential = true 370 | portMappings = [ 371 | { 372 | containerPort = 6379 373 | hostPort = 6379 374 | }] 375 | logConfiguration = { 376 | logDriver = "awslogs" 377 | options = { 378 | "awslogs-group" : aws_cloudwatch_log_group.test.id 379 | "awslogs-region" : "eu-west-1" 380 | "awslogs-stream-prefix" : "ecs" 381 | } 382 | } 383 | }, 384 | { 385 | name = "rabbitmq" 386 | image = "rabbitmq:latest" 387 | cpu = 256 388 | memory = 512 389 | essential = true 390 | portMappings = [ 391 | { 392 | containerPort = 5672 393 | hostPort = 5672 394 | }] 395 | logConfiguration = { 396 | logDriver = "awslogs" 397 | options = { 398 | "awslogs-group" : aws_cloudwatch_log_group.test.id 399 | "awslogs-region" : "eu-west-1" 400 | "awslogs-stream-prefix" : "ecs" 401 | } 402 | } 403 | } 404 | ]) 405 | 406 | } 407 | 408 | resource "aws_ecs_task_definition" "ec2_launch" { 409 | family = "test-ecsgo-ec2-launch" 410 | task_role_arn = aws_iam_role.ecs_task.arn 411 | execution_role_arn = aws_iam_role.ecs_task.arn 412 | requires_compatibilities = ["EC2"] 413 | 414 | runtime_platform { 415 | operating_system_family = "LINUX" 416 | } 417 | 418 | container_definitions = jsonencode([ 419 | { 420 | name = "nginx" 421 | image = "nginx:latest" 422 | cpu = 256 423 | memory = 512 424 | essential = true 425 | portMappings = [ 426 | { 427 | containerPort = 80 428 | } 429 | ] 430 | logConfiguration = { 431 | logDriver = "awslogs" 432 | options = { 433 | "awslogs-group" : aws_cloudwatch_log_group.test.id 434 | "awslogs-region" : "eu-west-1" 435 | "awslogs-stream-prefix" : "ecs" 436 | } 437 | } 438 | }, 439 | { 440 | name = "redis" 441 | image = "redis:latest" 442 | cpu = 256 443 | memory = 512 444 | essential = true 445 | portMappings = [ 446 | { 447 | containerPort = 6379 448 | }] 449 | logConfiguration = { 450 | logDriver = "awslogs" 451 | options = { 452 | "awslogs-group" : aws_cloudwatch_log_group.test.id 453 | "awslogs-region" : "eu-west-1" 454 | "awslogs-stream-prefix" : "ecs" 455 | } 456 | } 457 | }, 458 | { 459 | name = "rabbitmq" 460 | image = "rabbitmq:latest" 461 | cpu = 256 462 | memory = 512 463 | essential = true 464 | portMappings = [ 465 | { 466 | containerPort = 5672 467 | }] 468 | logConfiguration = { 469 | logDriver = "awslogs" 470 | options = { 471 | "awslogs-group" : aws_cloudwatch_log_group.test.id 472 | "awslogs-region" : "eu-west-1" 473 | "awslogs-stream-prefix" : "ecs" 474 | } 475 | } 476 | } 477 | ]) 478 | 479 | } 480 | 481 | resource "aws_ecs_task_definition" "windows_ec2_launch" { 482 | family = "test-ecsgo-windows-ec2-launch" 483 | task_role_arn = aws_iam_role.ecs_task.arn 484 | requires_compatibilities = ["EC2"] 485 | 486 | container_definitions = jsonencode([ 487 | { 488 | name = "iis" 489 | image = "mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2022" 490 | cpu = 1024 491 | memory = 2048 492 | essential = true 493 | portMappings = [ 494 | { 495 | containerPort = 80 496 | } 497 | ] 498 | logConfiguration = { 499 | logDriver = "awslogs" 500 | options = { 501 | "awslogs-group" : aws_cloudwatch_log_group.test.id 502 | "awslogs-region" : "eu-west-1" 503 | "awslogs-stream-prefix" : "ecs" 504 | } 505 | } 506 | } 507 | ]) 508 | 509 | } 510 | 511 | resource "aws_ecs_service" "fargate" { 512 | name = "fargate-test" 513 | cluster = aws_ecs_cluster.test.id 514 | task_definition = aws_ecs_task_definition.fargate.arn 515 | desired_count = 1 516 | enable_execute_command = true 517 | launch_type = "FARGATE" 518 | 519 | network_configuration { 520 | subnets = data.aws_subnets.test.ids 521 | assign_public_ip = true 522 | security_groups = [aws_security_group.ecs_instance.id] 523 | } 524 | 525 | depends_on = [ 526 | aws_ecs_cluster_capacity_providers.test 527 | ] 528 | } 529 | 530 | resource "aws_ecs_service" "ec2_launch" { 531 | name = "ec2-launch-test" 532 | cluster = aws_ecs_cluster.test.id 533 | task_definition = aws_ecs_task_definition.ec2_launch.arn 534 | desired_count = 3 535 | enable_execute_command = true 536 | launch_type = "EC2" 537 | } 538 | 539 | resource "aws_ecs_service" "ec2_launch_pagniation" { 540 | for_each = toset([for i in range(1, 10) : tostring(i)]) 541 | name = "ec2-launch-test-${each.value}" 542 | cluster = aws_ecs_cluster.test.id 543 | task_definition = aws_ecs_task_definition.ec2_launch.arn 544 | desired_count = 0 545 | enable_execute_command = true 546 | launch_type = "EC2" 547 | } 548 | 549 | resource "aws_ecs_service" "windows-ec2_launch" { 550 | name = "ec2-windows-launch-test" 551 | cluster = aws_ecs_cluster.windows_test.id 552 | task_definition = aws_ecs_task_definition.windows_ec2_launch.arn 553 | desired_count = 1 554 | enable_execute_command = true 555 | launch_type = "EC2" 556 | } 557 | 558 | --------------------------------------------------------------------------------