├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── cmd ├── root.go ├── run.go ├── scheduled_task.go ├── service.go ├── task.go ├── task_definition.go ├── update.go └── version.go ├── deploy ├── config.go ├── deploy.go ├── deploy_test.go ├── scheduled_task.go ├── service.go ├── service_test.go ├── task.go ├── task_definition.go ├── task_definition_test.go └── task_test.go ├── go.mod ├── go.sum ├── main.go └── packages └── .gitkeep /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 20 8 | ignore: 9 | - dependency-name: github.com/aws/aws-sdk-go 10 | versions: 11 | - 1.37.0 12 | - 1.37.1 13 | - 1.37.10 14 | - 1.37.11 15 | - 1.37.12 16 | - 1.37.13 17 | - 1.37.14 18 | - 1.37.15 19 | - 1.37.16 20 | - 1.37.17 21 | - 1.37.18 22 | - 1.37.19 23 | - 1.37.2 24 | - 1.37.20 25 | - 1.37.21 26 | - 1.37.22 27 | - 1.37.23 28 | - 1.37.24 29 | - 1.37.25 30 | - 1.37.26 31 | - 1.37.27 32 | - 1.37.28 33 | - 1.37.29 34 | - 1.37.3 35 | - 1.37.30 36 | - 1.37.31 37 | - 1.37.5 38 | - 1.37.6 39 | - 1.37.7 40 | - 1.37.8 41 | - 1.37.9 42 | - dependency-name: github.com/sirupsen/logrus 43 | versions: 44 | - 1.7.0 45 | - 1.7.1 46 | - 1.8.0 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | packages -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | env: 4 | - GO111MODULE=on 5 | 6 | go: 7 | - "1.12" 8 | - "1.13" 9 | 10 | install: 11 | - go mod download 12 | 13 | script: 14 | - go build 15 | - cd deploy && go test 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 h3poteto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all mod build 2 | 3 | OUTPUT = ecs-goploy 4 | BUILD_CMD = go build -a -tags netgo -installsuffix netgo --ldflags '-extldflags "-static"' 5 | VERSION = v1.0.0 6 | 7 | all: mac linux windows 8 | 9 | mod: go.mod 10 | go mod download 11 | 12 | mac: mod 13 | GOOS=darwin GOARCH=amd64 $(BUILD_CMD) -o $(OUTPUT) 14 | zip packages/ecs-goploy_${VERSION}_darwin_amd64.zip $(OUTPUT) 15 | 16 | linux: mod 17 | GOOS=linux GOARCH=amd64 $(BUILD_CMD) -o $(OUTPUT) 18 | zip packages/ecs-goploy_${VERSION}_linux_amd64.zip $(OUTPUT) 19 | 20 | windows: mod 21 | GOOS=windows GOARCH=amd64 $(BUILD_CMD) -o $(OUTPUT).exe 22 | zip packages/ecs-goploy_${VERSION}_windows_amd64.zip $(OUTPUT).exe 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecs-goploy 2 | [![Build Status](https://travis-ci.com/h3poteto/ecs-goploy.svg?branch=master)](https://travis-ci.com/h3poteto/ecs-goploy) 3 | [![GitHub release](http://img.shields.io/github/release/h3poteto/ecs-goploy.svg?style=flat-square)](https://github.com/h3poteto/ecs-goploy/releases) 4 | [![GoDoc](https://godoc.org/github.com/h3poteto/ecs-goploy/deploy?status.svg)](https://godoc.org/github.com/h3poteto/ecs-goploy/deploy) 5 | 6 | `ecs-goploy` is a re-implementation of [ecs-deploy](https://github.com/silinternational/ecs-deploy) in Golang. 7 | 8 | 9 | This is a command line tool, but you can use `deploy` as a package. 10 | So when you write own deploy script for AWS ECS, you can embed `deploy` package in your golang source code and customize deploy recipe. 11 | Please check [godoc](https://godoc.org/github.com/h3poteto/ecs-goploy/deploy). 12 | 13 | 14 | # Install 15 | 16 | Get binary from github: 17 | 18 | ``` 19 | $ wget https://github.com/h3poteto/ecs-goploy/releases/download/v0.5.0/ecs-goploy_v0.5.0_linux_amd64.zip 20 | $ unzip ecs-goploy_v0.5.0_linux_amd64.zip 21 | $ ./ecs-goploy --help 22 | ``` 23 | 24 | # Usage 25 | 26 | ``` 27 | $ ./ecs-goploy --help 28 | Deploy commands for ECS 29 | 30 | Usage: 31 | ecs-goploy [command] 32 | 33 | Available Commands: 34 | help Help about any command 35 | run Run command 36 | update Update some ECS resource 37 | version Print the version number 38 | 39 | Flags: 40 | -h, --help help for ecs-goploy 41 | --profile string AWS profile (detault is none, and use environment variables) 42 | --region string AWS region (default is none, and use AWS_DEFAULT_REGION) 43 | -v, --verbose Enable verbose mode 44 | 45 | Use "ecs-goploy [command] --help" for more information about a command. 46 | ``` 47 | 48 | ## Deploy an ECS Service 49 | 50 | Please specify cluser, service name and image(family:revision). 51 | 52 | ``` 53 | $ ./ecs-goploy update service --cluster my-cluster --service-name my-service --image nginx:stable --skip-check-deployments --enable-rollback 54 | ``` 55 | 56 | If you specify `--base-task-definition`, ecs-goploy updates the task definition with the image and deploy ecs service. 57 | If you does not specify `--base-task-definition`, ecs-goploy get current task definition of the service, and update with the image, and deploy ecs service. 58 | 59 | ## Run Task 60 | 61 | At first, you must update the task definition which is used to run ecs task. 62 | After that, you can run ecs task. 63 | 64 | ``` 65 | $ NEW_TASK_DEFINITION=`./ecs-goploy update task-definition --base-task-definition my-task-definition:1 --image nginx:stable` 66 | $ ./ecs-goploy run task --cluster my-cluster --container-name web --task-definition $NEW_TASK_DEFINITION --command "some commands" 67 | ``` 68 | 69 | ## Update Scheduled Task 70 | 71 | At first, you must update the task definition which is used to run scheduled task. 72 | After that, you can update the scheduled task. 73 | 74 | ``` 75 | $ NEW_TASK_DEFINITION=`./ecs-goploy update task-definition --base-task-definition my-task-definition:1 --image nginx:stable` 76 | $ ./ecs-goploy update scheduled-task --count 1 --name schedule-name --task-definition $NEW_TASK_DEFINITION 77 | ``` 78 | 79 | # Configuration 80 | ## AWS Configuration 81 | 82 | `ecs-goploy` calls AWS API via aws-skd-go, so you need export environment variables: 83 | 84 | ``` 85 | $ export AWS_ACCESS_KEY_ID=XXXXX 86 | $ export AWS_SECRET_ACCESS_KEY=XXXXX 87 | $ export AWS_DEFAULT_REGION=XXXXX 88 | ``` 89 | 90 | or set your credentials in `$HOME/.aws/credentials`: 91 | 92 | ``` 93 | [default] 94 | aws_access_key_id = XXXXX 95 | aws_secret_access_key = XXXXX 96 | ``` 97 | 98 | or prepare IAM Role or IAM Task Role. 99 | 100 | AWS region can be set command argument: `--region`. 101 | 102 | ## AWS IAM Policy 103 | 104 | Below is a basic IAM Policy required for ecs-goploy. 105 | 106 | ``` 107 | { 108 | "Version": "2012-10-17", 109 | "Statement": [ 110 | { 111 | "Sid": "AllowUserToECSDeploy", 112 | "Effect": "Allow", 113 | "Action": [ 114 | "ecr:DescribeRepositories", 115 | "ecr:DescribeImages", 116 | "ecs:DescribeServices", 117 | "ecs:DescribeTaskDefinition", 118 | "ecs:RegisterTaskDefinition", 119 | "ecs:UpdateService", 120 | "ecs:RunTask", 121 | "ecs:DescribeTasks", 122 | "ecs:ListTasks", 123 | "events:DescribeRule", 124 | "events:ListTargetsByRule", 125 | "events:PutTargets", 126 | "iam:PassRole" 127 | ], 128 | "Resource": "*" 129 | } 130 | ] 131 | } 132 | ``` 133 | 134 | # License 135 | 136 | The package is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 137 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // RootCmd root comand for cobra 9 | var RootCmd = &cobra.Command{ 10 | Use: "ecs-goploy", 11 | Short: "Deploy commands for ECS", 12 | SilenceErrors: true, 13 | SilenceUsage: true, 14 | } 15 | 16 | func init() { 17 | cobra.OnInitialize() 18 | RootCmd.PersistentFlags().StringP("profile", "", "", "AWS profile (detault is none, and use environment variables)") 19 | RootCmd.PersistentFlags().StringP("region", "", "", "AWS region (default is none, and use AWS_DEFAULT_REGION)") 20 | RootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose mode") 21 | viper.BindPFlag("profile", RootCmd.PersistentFlags().Lookup("profile")) 22 | viper.BindPFlag("region", RootCmd.PersistentFlags().Lookup("region")) 23 | viper.BindPFlag("verbose", RootCmd.PersistentFlags().Lookup("verbose")) 24 | 25 | RootCmd.AddCommand( 26 | versionCmd(), 27 | runCmd(), 28 | updateCmd(), 29 | ) 30 | } 31 | 32 | // generalConfig returns profile, and region. 33 | func generalConfig() (string, string, bool) { 34 | return viper.GetString("profile"), viper.GetString("region"), viper.GetBool("verbose") 35 | } 36 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | func runCmd() *cobra.Command { 6 | command := &cobra.Command{ 7 | Use: "run", 8 | Short: "Run command", 9 | Run: func(c *cobra.Command, arg []string) { 10 | c.Help() 11 | }, 12 | } 13 | command.AddCommand( 14 | runTaskCmd(), 15 | ) 16 | 17 | return command 18 | } 19 | -------------------------------------------------------------------------------- /cmd/scheduled_task.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | ecsdeploy "github.com/h3poteto/ecs-goploy/deploy" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type updateScheduledTask struct { 12 | name string 13 | taskDefinition string 14 | count int64 15 | } 16 | 17 | func updateScheduledTaskCmd() *cobra.Command { 18 | t := &updateScheduledTask{} 19 | command := &cobra.Command{ 20 | Use: "scheduled-task", 21 | Short: "Update ECS Scheduled Task", 22 | RunE: t.update, 23 | } 24 | 25 | flags := command.Flags() 26 | flags.StringVarP(&t.name, "name", "n", "", "Name of scheduled task") 27 | flags.StringVarP(&t.taskDefinition, "task-definition", "d", "", "Name of task definition to update scheduled task. Family and revision (family:revision) or full ARN") 28 | flags.Int64VarP(&t.count, "count", "c", 1, "Count of the task") 29 | 30 | return command 31 | } 32 | 33 | func (s *updateScheduledTask) update(cmd *cobra.Command, args []string) error { 34 | var baseTaskDefinition *string 35 | if len(s.taskDefinition) > 0 { 36 | baseTaskDefinition = &s.taskDefinition 37 | } 38 | profile, region, verbose := generalConfig() 39 | if !verbose { 40 | log.SetLevel(log.ErrorLevel) 41 | } 42 | scheduledTask := ecsdeploy.NewScheduledTask(profile, region, verbose) 43 | err := scheduledTask.Update(s.name, baseTaskDefinition, s.count) 44 | if err != nil { 45 | log.Fatal(err) 46 | return err 47 | } 48 | fmt.Println("Success to update the schedule") 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /cmd/service.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | ecsdeploy "github.com/h3poteto/ecs-goploy/deploy" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type updateService struct { 13 | cluster string 14 | name string 15 | baseTaskDefinition string 16 | imageWithTag string 17 | timeout int 18 | enableRollback bool 19 | skipCheckDeployments bool 20 | } 21 | 22 | func updateServiceCmd() *cobra.Command { 23 | s := &updateService{} 24 | cmd := &cobra.Command{ 25 | Use: "service", 26 | Short: "Deploy an ECS Service", 27 | Run: s.update, 28 | } 29 | 30 | flags := cmd.Flags() 31 | flags.StringVarP(&s.cluster, "cluster", "c", "", "Name of ECS cluster") 32 | flags.StringVarP(&s.name, "service-name", "n", "", "Name of service to deploy") 33 | flags.StringVarP(&s.baseTaskDefinition, "base-task-definition", "d", "", "Name of base task definition to deploy. Family and revision (family:revision) or full ARN. Default is none, and use current service's task definition") 34 | flags.StringVarP(&s.imageWithTag, "image", "i", "", "Name of Docker image to run, ex: repo/image:latest") 35 | flags.IntVarP(&s.timeout, "timeout", "t", 300, "Timeout seconds. Script monitors ECS Service for new task definition to be running") 36 | flags.BoolVar(&s.enableRollback, "enable-rollback", false, "Rollback task definition if new version is not running before TIMEOUT") 37 | flags.BoolVar(&s.skipCheckDeployments, "skip-check-deployments", false, "Skip checking deployments when detect whether deploy completed") 38 | 39 | return cmd 40 | } 41 | 42 | func (s *updateService) update(cmd *cobra.Command, args []string) { 43 | var baseTaskDefinition *string 44 | if len(s.baseTaskDefinition) > 0 { 45 | baseTaskDefinition = &s.baseTaskDefinition 46 | } 47 | profile, region, verbose := generalConfig() 48 | if !verbose { 49 | log.SetLevel(log.ErrorLevel) 50 | } 51 | service, err := ecsdeploy.NewService(s.cluster, s.name, s.imageWithTag, baseTaskDefinition, (time.Duration(s.timeout) * time.Second), s.enableRollback, s.skipCheckDeployments, profile, region, verbose) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | if err := service.Deploy(); err != nil { 56 | log.Fatal(err) 57 | } 58 | fmt.Println("Deploy success") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/task.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | ecsdeploy "github.com/h3poteto/ecs-goploy/deploy" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type runTask struct { 13 | cluster string 14 | name string 15 | taskDefinition string 16 | imageWithTag string 17 | command string 18 | subnets string 19 | securityGroups string 20 | fargate bool 21 | timeout int 22 | } 23 | 24 | func runTaskCmd() *cobra.Command { 25 | t := &runTask{} 26 | cmd := &cobra.Command{ 27 | Use: "task", 28 | Short: "Run task on ECS", 29 | Run: t.run, 30 | } 31 | 32 | flags := cmd.Flags() 33 | flags.StringVarP(&t.cluster, "cluster", "c", "", "Name of ECS cluster") 34 | flags.StringVarP(&t.name, "container-name", "n", "", "Name of the container for override task definition") 35 | flags.StringVarP(&t.taskDefinition, "task-definition", "d", "", "Name of task definition to run task. Family and revision (family:revision) or full ARN") 36 | flags.StringVar(&t.command, "command", "", "Task command which run on ECS") 37 | flags.StringVarP(&t.subnets, "subnets", "s", "", "Provide subnet IDs with comma-separated string (subnet-12abcde,subnet-34abcde). This param is necessary, if you set farage flag.") 38 | flags.StringVarP(&t.securityGroups, "security-groups", "g", "", "Provide security group IDs with comma-separated string (sg-0123asdb,sg-2345asdf), if you want to attach the security groups to ENI of the task.") 39 | flags.BoolVarP(&t.fargate, "fargate", "f", false, "Whether run task with FARGATE") 40 | flags.IntVarP(&t.timeout, "timeout", "t", 0, "Timeout seconds") 41 | 42 | return cmd 43 | } 44 | 45 | func (t *runTask) run(cmd *cobra.Command, args []string) { 46 | profile, region, verbose := generalConfig() 47 | if !verbose { 48 | log.SetLevel(log.ErrorLevel) 49 | } 50 | task, err := ecsdeploy.NewTask(t.cluster, t.name, t.command, t.taskDefinition, t.fargate, t.subnets, t.securityGroups, (time.Duration(t.timeout) * time.Second), profile, region, verbose) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | if _, err := task.Run(); err != nil { 55 | log.Fatal(err) 56 | } 57 | fmt.Println("Success to run task") 58 | } 59 | -------------------------------------------------------------------------------- /cmd/task_definition.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | ecsdeploy "github.com/h3poteto/ecs-goploy/deploy" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type updateTaskDefinition struct { 12 | baseTaskDefinition string 13 | imageWithTag string 14 | } 15 | 16 | func updateTaskDefinitionCmd() *cobra.Command { 17 | n := &updateTaskDefinition{} 18 | cmd := &cobra.Command{ 19 | Use: "task-definition", 20 | Short: "Create a new revision of the task definiition", 21 | RunE: n.update, 22 | } 23 | 24 | flags := cmd.Flags() 25 | flags.StringVarP(&n.baseTaskDefinition, "base-task-definition", "d", "", "Nmae of base task definition to create a new revision. Family and revision (family:revision) or full ARN") 26 | flags.StringVarP(&n.imageWithTag, "image", "i", "", "Name of Docker image to update, ex: repo/image:latest") 27 | 28 | return cmd 29 | } 30 | 31 | func (n *updateTaskDefinition) update(cmd *cobra.Command, args []string) error { 32 | var baseTaskDefinition *string 33 | if len(n.baseTaskDefinition) > 0 { 34 | baseTaskDefinition = &n.baseTaskDefinition 35 | } 36 | profile, region, verbose := generalConfig() 37 | if !verbose { 38 | log.SetLevel(log.ErrorLevel) 39 | } 40 | taskDefinition := ecsdeploy.NewTaskDefinition(profile, region, verbose) 41 | t, err := taskDefinition.Create(baseTaskDefinition, n.imageWithTag) 42 | if err != nil { 43 | log.Fatal(err) 44 | return err 45 | } 46 | fmt.Println(*t.TaskDefinitionArn) 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | func updateCmd() *cobra.Command { 6 | command := &cobra.Command{ 7 | Use: "update", 8 | Short: "Update some ECS resource", 9 | Run: func(c *cobra.Command, arg []string) { 10 | c.Help() 11 | }, 12 | } 13 | 14 | command.AddCommand( 15 | updateServiceCmd(), 16 | updateTaskDefinitionCmd(), 17 | updateScheduledTaskCmd(), 18 | ) 19 | 20 | return command 21 | } 22 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func versionCmd() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "version", 12 | Short: "Print the version number", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | fmt.Println("ecs-goploy v0.5.1") 15 | }, 16 | } 17 | 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /deploy/config.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/credentials" 8 | "github.com/aws/aws-sdk-go/aws/defaults" 9 | ) 10 | 11 | // newConfig returns a new aws config 12 | func newConfig(profile string, region string) *aws.Config { 13 | defaultConfig := defaults.Get().Config 14 | cred := newCredentials(getenv(profile, "AWS_DEFAULT_PROFILE"), getenv(region, "AWS_DEFAULT_REGION")) 15 | return defaultConfig.WithCredentials(cred).WithRegion(getenv(region, "AWS_DEFAULT_REGION")) 16 | } 17 | 18 | func newCredentials(profile string, region string) *credentials.Credentials { 19 | // temporary config to resolve RemoteCredProvider 20 | tmpConfig := defaults.Get().Config.WithRegion(region) 21 | tmpHandlers := defaults.Handlers() 22 | 23 | return credentials.NewChainCredentials( 24 | []credentials.Provider{ 25 | // Read profile before environment variables 26 | &credentials.SharedCredentialsProvider{ 27 | Profile: profile, 28 | }, 29 | &credentials.EnvProvider{}, 30 | // for IAM Task Role (ECS) and IAM Role 31 | defaults.RemoteCredProvider(*tmpConfig, tmpHandlers), 32 | }) 33 | } 34 | 35 | func getenv(value, key string) string { 36 | if len(value) == 0 { 37 | return os.Getenv(key) 38 | } 39 | return value 40 | } 41 | -------------------------------------------------------------------------------- /deploy/deploy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package deploy provides simple functions for deploy ECS. 3 | 4 | Usage: 5 | 6 | import "github.com/h3poteto/ecs-goploy/deploy" 7 | 8 | Service update 9 | 10 | When you want to update service in ECS, please use this package as follows. 11 | 12 | Construct a new Service, then use deploy functions. 13 | 14 | s, err := deploy.NewService("cluster", "service-name", "nginx:stable", nil, 5 * time.Minute, true, "", "", true) 15 | if err != nil { 16 | log.Fatalf("[ERROR] %v", err) 17 | } 18 | 19 | // deploy new image 20 | if err := s.Deploy(); err != nil { 21 | log.Fatalf("[ERROR] %v", err) 22 | } 23 | 24 | Or you can write a custom deploy recipe as you like. 25 | 26 | For example: 27 | 28 | s, err := deploy.NewService("cluster", "service-name", "nginx:stable", nil, 5 * time.Minute, true, "", "", true) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | // get the current service 34 | service, err := s.DescribeService() 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | currentTaskDefinition, err := s.TaskDefinition.DescribeTaskDefinition(service) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | newTaskDefinition, err := s.RegisterTaskDefinition(currentTaskDefinition, s.NewImage) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | // Do something 49 | 50 | err = s.UpdateService(service, newTaskDefinition) 51 | if err != nil { 52 | // Do something 53 | } 54 | log.Println("[INFO] Deploy success") 55 | 56 | TaskDefinition update 57 | 58 | You can create a new revision of the task definition. Please use this task definition at `Task` and `ScheduledTask`. 59 | 60 | For example: 61 | 62 | taskDefinition := ecsdeploy.NewTaskDefinition("", "", true) 63 | t, err := taskDefinition.Create("sample-task-definition:revision", "nginx:stable") 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | log.Println(*t.TaskDefinitionArn) 68 | 69 | 70 | Run task 71 | 72 | When you want to run task on ECS at once, plese use this package as follows. 73 | 74 | For example: 75 | 76 | task, err := ecsdeploy.NewTask("cluster", "container-name", "echo hoge", "sample-task-definition:2", (5 * time.Minute), "", "", true) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | if _, err := task.Run(); err != nil { 81 | log.Fatal(err) 82 | } 83 | log.Println("[INFO] Task success") 84 | 85 | 86 | ScheduledTask update 87 | 88 | When you update the ECS Scheduled Task, please use this package. 89 | 90 | For example: 91 | 92 | scheduledTask := ecsdeploy.NewScheduledTask("", "", true) 93 | scheduledTask("schedule-name", "sample-task-definition:2", 1) 94 | 95 | */ 96 | package deploy 97 | 98 | import ( 99 | "fmt" 100 | "strings" 101 | 102 | "github.com/aws/aws-sdk-go/service/ecs" 103 | "github.com/pkg/errors" 104 | log "github.com/sirupsen/logrus" 105 | ) 106 | 107 | // Image has repository and tag string of docker image. 108 | type Image struct { 109 | 110 | // Docker image repository. 111 | Repository string 112 | 113 | // Docker image tag. 114 | Tag string 115 | } 116 | 117 | // Deploy runs deploy commands and handle errors. 118 | func (s *Service) Deploy() error { 119 | service, err := s.DescribeService() 120 | if err != nil { 121 | return errors.Wrap(err, "Can not get current service: ") 122 | } 123 | 124 | // get running task definition 125 | currentTaskDefinition, err := s.TaskDefinition.DescribeTaskDefinition(*service.TaskDefinition) 126 | if err != nil { 127 | return errors.Wrap(err, "Can not get task definition: ") 128 | } 129 | 130 | // get base task definition if needed 131 | baseTaskDefinition := currentTaskDefinition 132 | if s.BaseTaskDefinition != nil { 133 | var err error 134 | baseTaskDefinition, err = s.TaskDefinition.DescribeTaskDefinition(*s.BaseTaskDefinition) 135 | if err != nil { 136 | return errors.Wrap(err, "Can not get task definition: ") 137 | } 138 | } 139 | 140 | newTaskDefinition, err := s.TaskDefinition.RegisterTaskDefinition(baseTaskDefinition, s.NewImage) 141 | if err != nil { 142 | return errors.Wrap(err, "Can not regist new task definition: ") 143 | } 144 | log.Infof("New task definition: %+v", newTaskDefinition) 145 | 146 | err = s.UpdateService(service, newTaskDefinition) 147 | if err != nil { 148 | log.Info("update failed") 149 | updateError := errors.Wrap(err, "Can not update service: ") 150 | if !s.EnableRollback { 151 | return updateError 152 | } 153 | 154 | // rollback to the current task definition which have been running to the end 155 | log.Infof("Rolling back to: %+v", currentTaskDefinition) 156 | if err := s.Rollback(service, currentTaskDefinition); err != nil { 157 | return errors.Wrap(updateError, err.Error()) 158 | } 159 | return updateError 160 | } 161 | return nil 162 | } 163 | 164 | // divideImageAndTag separates imageWithTag into repository and tag. 165 | func divideImageAndTag(imageWithTag string) (*string, *string, error) { 166 | res := strings.Split(imageWithTag, ":") 167 | if len(res) != 2 { 168 | return nil, nil, fmt.Errorf("image format is wrong: %s", imageWithTag) 169 | } 170 | return &res[0], &res[1], nil 171 | 172 | } 173 | 174 | //Run run task on ECS based on provided task definition. 175 | func (t *Task) Run() ([]*ecs.Task, error) { 176 | if t.BaseTaskDefinition == "" { 177 | return nil, errors.New("task definition is required") 178 | } 179 | // get a task definition 180 | baseTaskDefinition, err := t.TaskDefinition.DescribeTaskDefinition(t.BaseTaskDefinition) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | return t.RunTask(baseTaskDefinition) 186 | } 187 | 188 | // Create creates a new revision of the task definition. 189 | func (n *TaskDefinition) Create(base *string, dockerImage string) (*ecs.TaskDefinition, error) { 190 | repository, revision, err := divideImageAndTag(dockerImage) 191 | if err != nil { 192 | return nil, err 193 | } 194 | image := &Image{ 195 | Repository: *repository, 196 | Tag: *revision, 197 | } 198 | if base == nil { 199 | return nil, errors.New("task definition is required") 200 | } 201 | baseTaskDefinition, err := n.DescribeTaskDefinition(*base) 202 | if err != nil { 203 | return nil, err 204 | } 205 | newTaskDefinition, err := n.RegisterTaskDefinition(baseTaskDefinition, image) 206 | if err != nil { 207 | return nil, err 208 | } 209 | log.Infof("New task definition: %+v", newTaskDefinition) 210 | 211 | return newTaskDefinition, nil 212 | } 213 | 214 | // Update update the cloudwatch event with provided task definition. 215 | func (s *ScheduledTask) Update(name string, taskDefinition *string, count int64) error { 216 | if taskDefinition == nil { 217 | return errors.New("task definition is required") 218 | } 219 | // get a task definition 220 | t, err := s.TaskDefinition.DescribeTaskDefinition(*taskDefinition) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | return s.UpdateTargets(count, t, name) 226 | } 227 | -------------------------------------------------------------------------------- /deploy/deploy_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDivideImageAndTag(t *testing.T) { 8 | imageWithTag := "nginx:latest" 9 | image, tag, err := divideImageAndTag(imageWithTag) 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | if *image != "nginx" { 14 | t.Errorf("image is invalid: %s", *image) 15 | } 16 | if *tag != "latest" { 17 | t.Errorf("tag is invalid: %s", *tag) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /deploy/scheduled_task.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | events "github.com/aws/aws-sdk-go/service/cloudwatchevents" 7 | eventsiface "github.com/aws/aws-sdk-go/service/cloudwatchevents/cloudwatcheventsiface" 8 | "github.com/aws/aws-sdk-go/service/ecs" 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // ScheduledTask has target task definition information and client of aws-sdk-go. 14 | type ScheduledTask struct { 15 | awsCloudWatchEvents eventsiface.CloudWatchEventsAPI 16 | 17 | // TaskDefinition struct to call aws API. 18 | TaskDefinition *TaskDefinition 19 | 20 | verbose bool 21 | } 22 | 23 | // NewScheduledTask returns a nwe ScheduledTask struct, and initialize aws cloudwatchevents API client. 24 | func NewScheduledTask(profile, region string, verbose bool) *ScheduledTask { 25 | awsCloudWatchEvents := events.New(session.New(), newConfig(profile, region)) 26 | taskDefinition := NewTaskDefinition(profile, region, verbose) 27 | if !verbose { 28 | log.SetLevel(log.ErrorLevel) 29 | } 30 | return &ScheduledTask{ 31 | awsCloudWatchEvents, 32 | taskDefinition, 33 | verbose, 34 | } 35 | } 36 | 37 | // ListsEventTargets list up event targets based on rule name. 38 | func (s *ScheduledTask) ListsEventTargets(ruleName *string) ([]*events.Target, error) { 39 | params := &events.ListTargetsByRuleInput{ 40 | Rule: ruleName, 41 | } 42 | resp, err := s.awsCloudWatchEvents.ListTargetsByRule(params) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return resp.Targets, nil 47 | } 48 | 49 | // DescribeRule finds an event rule. 50 | func (s *ScheduledTask) DescribeRule(name string) (*events.DescribeRuleOutput, error) { 51 | params := &events.DescribeRuleInput{ 52 | Name: aws.String(name), 53 | } 54 | resp, err := s.awsCloudWatchEvents.DescribeRule(params) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return resp, nil 59 | } 60 | 61 | // update updates an event target. 62 | func (s *ScheduledTask) update(taskCount int64, taskDefinition *ecs.TaskDefinition, baseTarget *events.Target, ruleName *string) error { 63 | ecsParameter := &events.EcsParameters{ 64 | TaskCount: aws.Int64(taskCount), 65 | TaskDefinitionArn: taskDefinition.TaskDefinitionArn, 66 | } 67 | target := baseTarget.SetEcsParameters(ecsParameter) 68 | params := &events.PutTargetsInput{ 69 | Rule: ruleName, 70 | Targets: []*events.Target{ 71 | target, 72 | }, 73 | } 74 | resp, err := s.awsCloudWatchEvents.PutTargets(params) 75 | if err != nil { 76 | return err 77 | } 78 | if *resp.FailedEntryCount > 0 { 79 | for _, e := range resp.FailedEntries { 80 | log.Errorf("Failed to update the entry: %+v", *e) 81 | } 82 | return errors.New("Failed to update entries") 83 | } 84 | return nil 85 | } 86 | 87 | // UpdateTargets updates all event targets related the rule. 88 | func (s *ScheduledTask) UpdateTargets(taskCount int64, taskDefinition *ecs.TaskDefinition, name string) error { 89 | rule, err := s.DescribeRule(name) 90 | if err != nil { 91 | return err 92 | } 93 | targets, err := s.ListsEventTargets(rule.Name) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | for _, target := range targets { 99 | log.Infof("Event target: %s", *target.Arn) 100 | err := s.update(taskCount, taskDefinition, target, rule.Name) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /deploy/service.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/ecs" 10 | "github.com/aws/aws-sdk-go/service/ecs/ecsiface" 11 | "github.com/pkg/errors" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // Service has target ECS information, client of aws-sdk-go, tasks information and timeout seconds. 16 | type Service struct { 17 | awsECS ecsiface.ECSAPI 18 | 19 | // Name of ECS cluster. 20 | Cluster string 21 | 22 | // Name of ECS service. 23 | Name string 24 | 25 | // Name of base task definition of deploy. 26 | BaseTaskDefinition *string 27 | 28 | // TaskDefinition struct to call aws API. 29 | TaskDefinition *TaskDefinition 30 | 31 | // New image for deploy. 32 | NewImage *Image 33 | 34 | // Wait time when update service. 35 | // This script monitors ECS service for new task definition to be running after call update service API. 36 | Timeout time.Duration 37 | 38 | // If deploy failed, rollback to current task definition. 39 | EnableRollback bool 40 | 41 | // When check whether deploy completed, confirm only new task status. 42 | // If this flag is true, confirm service deployments status. 43 | SkipCheckDeployments bool 44 | 45 | verbose bool 46 | } 47 | 48 | // NewService returns a new Service struct, and initialize aws ecs API client. 49 | // Separates imageWithTag into repository and tag, then sets a NewImage for deploy. 50 | func NewService(cluster, name, imageWithTag string, baseTaskDefinition *string, timeout time.Duration, enableRollback bool, skipCheckDeployments bool, profile, region string, verbose bool) (*Service, error) { 51 | awsECS := ecs.New(session.New(), newConfig(profile, region)) 52 | taskDefinition := NewTaskDefinition(profile, region, verbose) 53 | if !verbose { 54 | log.SetLevel(log.ErrorLevel) 55 | } 56 | var newImage *Image 57 | if len(imageWithTag) > 0 { 58 | var err error 59 | repository, tag, err := divideImageAndTag(imageWithTag) 60 | if err != nil { 61 | return nil, err 62 | } 63 | newImage = &Image{ 64 | *repository, 65 | *tag, 66 | } 67 | } 68 | return &Service{ 69 | awsECS, 70 | cluster, 71 | name, 72 | baseTaskDefinition, 73 | taskDefinition, 74 | newImage, 75 | timeout, 76 | enableRollback, 77 | skipCheckDeployments, 78 | verbose, 79 | }, nil 80 | } 81 | 82 | // DescribeService gets a current service in the cluster. 83 | func (s *Service) DescribeService() (*ecs.Service, error) { 84 | params := &ecs.DescribeServicesInput{ 85 | Services: []*string{ 86 | aws.String(s.Name), 87 | }, 88 | Cluster: aws.String(s.Cluster), 89 | } 90 | resp, err := s.awsECS.DescribeServices(params) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return resp.Services[0], nil 96 | } 97 | 98 | // UpdateService updates the service with a new task definition, and wait during update action. 99 | func (s *Service) UpdateService(service *ecs.Service, taskDefinition *ecs.TaskDefinition) error { 100 | params := &ecs.UpdateServiceInput{} 101 | if *service.SchedulingStrategy == "DAEMON" { 102 | // If the service type is DAEMON, we can not specify desired count. 103 | params = &ecs.UpdateServiceInput{ 104 | Service: aws.String(s.Name), 105 | Cluster: aws.String(s.Cluster), 106 | DeploymentConfiguration: service.DeploymentConfiguration, 107 | TaskDefinition: taskDefinition.TaskDefinitionArn, 108 | } 109 | } else { 110 | params = &ecs.UpdateServiceInput{ 111 | Service: aws.String(s.Name), 112 | Cluster: aws.String(s.Cluster), 113 | DeploymentConfiguration: service.DeploymentConfiguration, 114 | DesiredCount: service.DesiredCount, 115 | TaskDefinition: taskDefinition.TaskDefinitionArn, 116 | } 117 | } 118 | resp, err := s.awsECS.UpdateService(params) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | newService := resp.Service 124 | if *newService.DesiredCount <= 0 { 125 | return nil 126 | } 127 | ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) 128 | defer cancel() 129 | return s.waitUpdating(ctx, taskDefinition) 130 | } 131 | 132 | // waitUpdating waits the new task definition is deployed. 133 | func (s *Service) waitUpdating(ctx context.Context, newTaskDefinition *ecs.TaskDefinition) error { 134 | log.Info("Waiting for new task running...") 135 | errCh := make(chan error, 1) 136 | done := make(chan struct{}, 1) 137 | go func() { 138 | err := s.waitSwitchTask(newTaskDefinition) 139 | if err != nil { 140 | errCh <- err 141 | } 142 | close(done) 143 | }() 144 | select { 145 | case err := <-errCh: 146 | if err != nil { 147 | return err 148 | } 149 | case <-done: 150 | log.Info("New task is running") 151 | case <-ctx.Done(): 152 | return errors.New("process timeout") 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func (s *Service) waitSwitchTask(newTaskDefinition *ecs.TaskDefinition) error { 159 | for { 160 | time.Sleep(5 * time.Second) 161 | 162 | service, err := s.DescribeService() 163 | if err != nil { 164 | return err 165 | } 166 | if s.checkCompleteDeploy(service, newTaskDefinition) { 167 | return nil 168 | } 169 | } 170 | } 171 | 172 | func (s *Service) checkCompleteDeploy(service *ecs.Service, newTaskDefinition *ecs.TaskDefinition) bool { 173 | if s.SkipCheckDeployments { 174 | return s.checkNewTaskRunning(service, newTaskDefinition) 175 | } 176 | return s.checkDeployments(service.Deployments, newTaskDefinition) 177 | } 178 | 179 | func (s *Service) checkDeployments(deployments []*ecs.Deployment, newTaskDefinition *ecs.TaskDefinition) bool { 180 | if len(deployments) != 1 { 181 | return false 182 | } 183 | for _, deploy := range deployments { 184 | if *deploy.TaskDefinition == *newTaskDefinition.TaskDefinitionArn && *deploy.Status == "PRIMARY" && *deploy.DesiredCount == *deploy.RunningCount { 185 | return true 186 | } 187 | } 188 | return false 189 | } 190 | 191 | func (s *Service) checkNewTaskRunning(service *ecs.Service, newTaskDefinition *ecs.TaskDefinition) bool { 192 | input := &ecs.ListTasksInput{ 193 | Cluster: service.ClusterArn, 194 | ServiceName: service.ServiceName, 195 | DesiredStatus: aws.String("RUNNING"), 196 | } 197 | runningTasks, err := s.awsECS.ListTasks(input) 198 | if err != nil { 199 | log.Error(err) 200 | return false 201 | } 202 | params := &ecs.DescribeTasksInput{ 203 | Cluster: service.ClusterArn, 204 | Tasks: runningTasks.TaskArns, 205 | } 206 | resp, err := s.awsECS.DescribeTasks(params) 207 | if err != nil { 208 | log.Error(err) 209 | return false 210 | } 211 | for _, task := range resp.Tasks { 212 | if *task.LastStatus == "RUNNING" && *task.TaskDefinitionArn == *newTaskDefinition.TaskDefinitionArn { 213 | return true 214 | } 215 | } 216 | return false 217 | 218 | } 219 | 220 | // Rollback updates the service with current task definition. 221 | // This method call update-service API and does not wait for execution to end. 222 | func (s *Service) Rollback(service *ecs.Service, currentTaskDefinition *ecs.TaskDefinition) error { 223 | if currentTaskDefinition == nil { 224 | return errors.New("old task definition is not exist") 225 | } 226 | params := &ecs.UpdateServiceInput{ 227 | Service: aws.String(s.Name), 228 | Cluster: aws.String(s.Cluster), 229 | DeploymentConfiguration: service.DeploymentConfiguration, 230 | DesiredCount: service.DesiredCount, 231 | TaskDefinition: currentTaskDefinition.TaskDefinitionArn, 232 | } 233 | _, err := s.awsECS.UpdateService(params) 234 | if err != nil { 235 | return err 236 | } 237 | log.Info("Rolled back") 238 | return nil 239 | } 240 | -------------------------------------------------------------------------------- /deploy/service_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/ecs" 9 | "github.com/aws/aws-sdk-go/service/ecs/ecsiface" 10 | ) 11 | 12 | type mockedDescribeServices struct { 13 | ecsiface.ECSAPI 14 | Resp ecs.DescribeServicesOutput 15 | } 16 | 17 | func (m mockedDescribeServices) DescribeServices(in *ecs.DescribeServicesInput) (*ecs.DescribeServicesOutput, error) { 18 | return &m.Resp, nil 19 | } 20 | 21 | func TestDescribeService(t *testing.T) { 22 | s := &ecs.Service{ 23 | ServiceName: aws.String("dummy-service"), 24 | } 25 | resp := ecs.DescribeServicesOutput{ 26 | Services: []*ecs.Service{ 27 | s, 28 | }, 29 | } 30 | service := &Service{ 31 | awsECS: mockedDescribeServices{Resp: resp}, 32 | } 33 | output, err := service.DescribeService() 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | 38 | if *output.ServiceName != "dummy-service" { 39 | t.Error("ServiceName is invalid") 40 | } 41 | 42 | } 43 | 44 | type mockedUpdateService struct { 45 | ecsiface.ECSAPI 46 | Update ecs.UpdateServiceOutput 47 | Describe ecs.DescribeServicesOutput 48 | } 49 | 50 | func (m mockedUpdateService) UpdateService(in *ecs.UpdateServiceInput) (*ecs.UpdateServiceOutput, error) { 51 | return &m.Update, nil 52 | } 53 | 54 | func (m mockedUpdateService) DescribeServices(in *ecs.DescribeServicesInput) (*ecs.DescribeServicesOutput, error) { 55 | return &m.Describe, nil 56 | } 57 | 58 | func TestUpdateService(t *testing.T) { 59 | s := &ecs.Service{ 60 | ServiceName: aws.String("dummy-service"), 61 | SchedulingStrategy: aws.String("REPLICA"), 62 | } 63 | newTaskDefinition := &ecs.TaskDefinition{ 64 | TaskDefinitionArn: aws.String("task-definition-arn"), 65 | } 66 | describe := ecs.DescribeServicesOutput{ 67 | Services: []*ecs.Service{ 68 | &ecs.Service{ 69 | ServiceName: aws.String("dummy-service"), 70 | Deployments: []*ecs.Deployment{ 71 | &ecs.Deployment{ 72 | TaskDefinition: aws.String("task-definition-arn"), 73 | Status: aws.String("PRIMARY"), 74 | DesiredCount: aws.Int64(1), 75 | RunningCount: aws.Int64(1), 76 | }, 77 | }, 78 | }, 79 | }, 80 | } 81 | update := ecs.UpdateServiceOutput{ 82 | Service: &ecs.Service{ 83 | ServiceName: aws.String("dummy-service"), 84 | DesiredCount: aws.Int64(1), 85 | }, 86 | } 87 | 88 | service := &Service{ 89 | awsECS: mockedUpdateService{ 90 | Update: update, 91 | Describe: describe, 92 | }, 93 | Timeout: 10 * time.Second, 94 | } 95 | 96 | err := service.UpdateService(s, newTaskDefinition) 97 | if err != nil { 98 | t.Error(err) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /deploy/task.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/ecs" 11 | "github.com/aws/aws-sdk-go/service/ecs/ecsiface" 12 | shellwords "github.com/mattn/go-shellwords" 13 | "github.com/pkg/errors" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // Task has target ECS information, client of aws-sdk-go, command and timeout seconds. 18 | type Task struct { 19 | awsECS ecsiface.ECSAPI 20 | 21 | // Name of ECS cluster. 22 | Cluster string 23 | 24 | // Name of the container for override task definition. 25 | Name string 26 | 27 | // Name of base task definition for run task. 28 | BaseTaskDefinition string 29 | 30 | // TaskDefinition struct to call aws API. 31 | TaskDefinition *TaskDefinition 32 | 33 | // Task command which run on ECS. 34 | Command []*string 35 | 36 | // Wait time when run task. 37 | // This script monitors ECS task for new task definition to be running after call run task API. 38 | Timeout time.Duration 39 | // EC2 or Fargate 40 | LaunchType string 41 | // If you set Fargate as launch type, you have to set your subnet IDs. 42 | // Because Fargate demands awsvpc as network configuration, so subnet IDs are required. 43 | Subnets []*string 44 | // If you want to attach the security groups to ENI of the task, please set this. 45 | SecurityGroups []*string 46 | // If you don't enable this flag, the task access the internet throguth NAT gateway. 47 | // Please read more information: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html 48 | AssignPublicIP string 49 | verbose bool 50 | } 51 | 52 | // NewTask returns a new Task struct, and initialize aws ecs API client. 53 | // If you want to run the task as Fargate, please provide fargate flag to true, and your subnet IDs for awsvpc. 54 | // If you don't want to run the task as Fargate, please provide empty string for subnetIDs. 55 | func NewTask(cluster, name, command, baseTaskDefinition string, fargate bool, subnetIDs, securityGroupIDs string, timeout time.Duration, profile, region string, verbose bool) (*Task, error) { 56 | if baseTaskDefinition == "" { 57 | return nil, errors.New("task definition is required") 58 | } 59 | awsECS := ecs.New(session.New(), newConfig(profile, region)) 60 | taskDefinition := NewTaskDefinition(profile, region, verbose) 61 | if !verbose { 62 | log.SetLevel(log.ErrorLevel) 63 | } 64 | p := shellwords.NewParser() 65 | commands, err := p.Parse(command) 66 | if err != nil { 67 | return nil, errors.Wrap(err, "Parse error in a task command") 68 | } 69 | var cmd []*string 70 | for _, c := range commands { 71 | cmd = append(cmd, aws.String(c)) 72 | } 73 | launchType := "EC2" 74 | assignPublicIP := "DISABLED" 75 | if fargate { 76 | launchType = "FARGATE" 77 | assignPublicIP = "ENABLED" 78 | } 79 | subnets := []*string{} 80 | for _, s := range strings.Split(subnetIDs, ",") { 81 | if len(s) > 0 { 82 | subnets = append(subnets, aws.String(s)) 83 | } 84 | } 85 | securityGroups := []*string{} 86 | for _, g := range strings.Split(securityGroupIDs, ",") { 87 | if len(g) > 0 { 88 | securityGroups = append(securityGroups, aws.String(g)) 89 | } 90 | } 91 | 92 | return &Task{ 93 | awsECS: awsECS, 94 | Cluster: cluster, 95 | Name: name, 96 | BaseTaskDefinition: baseTaskDefinition, 97 | TaskDefinition: taskDefinition, 98 | Command: cmd, 99 | Timeout: timeout, 100 | LaunchType: launchType, 101 | Subnets: subnets, 102 | SecurityGroups: securityGroups, 103 | AssignPublicIP: assignPublicIP, 104 | verbose: verbose, 105 | }, nil 106 | } 107 | 108 | // RunTask calls run-task API. 109 | func (t *Task) RunTask(taskDefinition *ecs.TaskDefinition) ([]*ecs.Task, error) { 110 | ctx, cancel := context.WithCancel(context.Background()) 111 | if t.Timeout != 0 { 112 | ctx, cancel = context.WithTimeout(context.Background(), t.Timeout) 113 | } 114 | defer cancel() 115 | 116 | containerOverride := &ecs.ContainerOverride{ 117 | Command: t.Command, 118 | Name: aws.String(t.Name), 119 | } 120 | 121 | override := &ecs.TaskOverride{ 122 | ContainerOverrides: []*ecs.ContainerOverride{ 123 | containerOverride, 124 | }, 125 | } 126 | 127 | var params *ecs.RunTaskInput 128 | if len(t.Subnets) > 0 { 129 | vpcConfiguration := &ecs.AwsVpcConfiguration{ 130 | AssignPublicIp: aws.String(t.AssignPublicIP), 131 | Subnets: t.Subnets, 132 | SecurityGroups: t.SecurityGroups, 133 | } 134 | network := &ecs.NetworkConfiguration{ 135 | AwsvpcConfiguration: vpcConfiguration, 136 | } 137 | params = &ecs.RunTaskInput{ 138 | Cluster: aws.String(t.Cluster), 139 | TaskDefinition: taskDefinition.TaskDefinitionArn, 140 | Overrides: override, 141 | NetworkConfiguration: network, 142 | LaunchType: aws.String(t.LaunchType), 143 | } 144 | } else { 145 | params = &ecs.RunTaskInput{ 146 | Cluster: aws.String(t.Cluster), 147 | TaskDefinition: taskDefinition.TaskDefinitionArn, 148 | Overrides: override, 149 | LaunchType: aws.String(t.LaunchType), 150 | } 151 | } 152 | 153 | resp, err := t.awsECS.RunTaskWithContext(ctx, params) 154 | if err != nil { 155 | return nil, err 156 | } 157 | if len(resp.Failures) > 0 { 158 | log.Errorf("Run task error: %+v", resp.Failures) 159 | return nil, errors.New(*resp.Failures[0].Reason) 160 | } 161 | log.Infof("Running tasks: %+v", resp.Tasks) 162 | 163 | err = t.waitRunning(ctx, resp.Tasks) 164 | if err != nil { 165 | return resp.Tasks, err 166 | } 167 | return resp.Tasks, nil 168 | } 169 | 170 | // waitRunning waits a task running. 171 | func (t *Task) waitRunning(ctx context.Context, tasks []*ecs.Task) error { 172 | log.Info("Waiting for running task...") 173 | 174 | taskArns := []*string{} 175 | for _, task := range tasks { 176 | taskArns = append(taskArns, task.TaskArn) 177 | } 178 | errCh := make(chan error, 1) 179 | done := make(chan struct{}, 1) 180 | go func() { 181 | err := t.waitExitTasks(taskArns) 182 | if err != nil { 183 | errCh <- err 184 | } 185 | close(done) 186 | }() 187 | select { 188 | case err := <-errCh: 189 | if err != nil { 190 | return err 191 | } 192 | case <-done: 193 | log.Info("Run task is success") 194 | case <-ctx.Done(): 195 | return errors.New("process timeout") 196 | } 197 | 198 | return nil 199 | } 200 | 201 | func (t *Task) waitExitTasks(taskArns []*string) error { 202 | retry: 203 | for { 204 | time.Sleep(5 * time.Second) 205 | 206 | params := &ecs.DescribeTasksInput{ 207 | Cluster: aws.String(t.Cluster), 208 | Tasks: taskArns, 209 | } 210 | resp, err := t.awsECS.DescribeTasks(params) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | for _, task := range resp.Tasks { 216 | if !t.checkTaskStopped(task) { 217 | continue retry 218 | } 219 | } 220 | 221 | for _, task := range resp.Tasks { 222 | code, result, err := t.checkTaskSucceeded(task) 223 | if err != nil { 224 | continue retry 225 | } 226 | if !result { 227 | return errors.Errorf("exit code: %v", code) 228 | } 229 | } 230 | return nil 231 | } 232 | } 233 | 234 | func (t *Task) checkTaskStopped(task *ecs.Task) bool { 235 | if *task.LastStatus != "STOPPED" { 236 | return false 237 | } 238 | return true 239 | } 240 | 241 | func (t *Task) checkTaskSucceeded(task *ecs.Task) (int64, bool, error) { 242 | for _, c := range task.Containers { 243 | if c.ExitCode == nil { 244 | return 1, false, errors.New("can not read exit code") 245 | } 246 | if *c.ExitCode != int64(0) { 247 | return *c.ExitCode, false, nil 248 | } 249 | } 250 | return int64(0), true, nil 251 | } 252 | -------------------------------------------------------------------------------- /deploy/task_definition.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/ecs" 7 | "github.com/aws/aws-sdk-go/service/ecs/ecsiface" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // TaskDefinition has image and task definition information. 12 | type TaskDefinition struct { 13 | awsECS ecsiface.ECSAPI 14 | 15 | verbose bool 16 | } 17 | 18 | // NewTaskDefinition initializes aws ecs API client, and returns a task definition struct. 19 | func NewTaskDefinition(profile, region string, verbose bool) *TaskDefinition { 20 | awsECS := ecs.New(session.New(), newConfig(profile, region)) 21 | return &TaskDefinition{ 22 | awsECS, 23 | verbose, 24 | } 25 | } 26 | 27 | // DescribeTaskDefinition gets a task definition. 28 | // The family for the latest ACTIVE revision, family and revision (family:revision) 29 | // for a specific revision in the family, or full Amazon Resource Name (ARN) 30 | // of the task definition to describe. 31 | func (d *TaskDefinition) DescribeTaskDefinition(taskDefinitionName string) (*ecs.TaskDefinition, error) { 32 | params := &ecs.DescribeTaskDefinitionInput{ 33 | TaskDefinition: aws.String(taskDefinitionName), 34 | } 35 | resp, err := d.awsECS.DescribeTaskDefinition(params) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return resp.TaskDefinition, nil 41 | } 42 | 43 | // RegisterTaskDefinition registers new task definition if needed. 44 | // If newTask is not set, returns a task definition which same as the given task definition. 45 | func (d *TaskDefinition) RegisterTaskDefinition(baseDefinition *ecs.TaskDefinition, newImage *Image) (*ecs.TaskDefinition, error) { 46 | var containerDefinitions []*ecs.ContainerDefinition 47 | for _, c := range baseDefinition.ContainerDefinitions { 48 | newDefinition, err := d.NewContainerDefinition(c, newImage) 49 | if err != nil { 50 | return nil, err 51 | } 52 | containerDefinitions = append(containerDefinitions, newDefinition) 53 | } 54 | params := &ecs.RegisterTaskDefinitionInput{ 55 | ContainerDefinitions: containerDefinitions, 56 | Cpu: baseDefinition.Cpu, 57 | ExecutionRoleArn: baseDefinition.ExecutionRoleArn, 58 | Family: baseDefinition.Family, 59 | IpcMode: baseDefinition.IpcMode, 60 | Memory: baseDefinition.Memory, 61 | NetworkMode: baseDefinition.NetworkMode, 62 | PidMode: baseDefinition.PidMode, 63 | PlacementConstraints: baseDefinition.PlacementConstraints, 64 | RequiresCompatibilities: baseDefinition.RequiresCompatibilities, 65 | TaskRoleArn: baseDefinition.TaskRoleArn, 66 | Volumes: baseDefinition.Volumes, 67 | } 68 | 69 | resp, err := d.awsECS.RegisterTaskDefinition(params) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return resp.TaskDefinition, nil 75 | } 76 | 77 | // NewContainerDefinition updates image tag in the given container definition. 78 | // If the container definition is not target container, returns the givien definition. 79 | func (d *TaskDefinition) NewContainerDefinition(baseDefinition *ecs.ContainerDefinition, newImage *Image) (*ecs.ContainerDefinition, error) { 80 | if newImage == nil { 81 | return baseDefinition, nil 82 | } 83 | baseRepository, _, err := divideImageAndTag(*baseDefinition.Image) 84 | if err != nil { 85 | return nil, errors.Wrap(err, "Task definition format is incorrect in base task definition") 86 | } 87 | if newImage.Repository != *baseRepository { 88 | return baseDefinition, nil 89 | } 90 | imageWithTag := (newImage.Repository) + ":" + (newImage.Tag) 91 | baseDefinition.Image = &imageWithTag 92 | return baseDefinition, nil 93 | } 94 | -------------------------------------------------------------------------------- /deploy/task_definition_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/ecs" 8 | "github.com/aws/aws-sdk-go/service/ecs/ecsiface" 9 | ) 10 | 11 | type mockedDescribeTaskDefinition struct { 12 | ecsiface.ECSAPI 13 | Resp ecs.DescribeTaskDefinitionOutput 14 | } 15 | 16 | func (m mockedDescribeTaskDefinition) DescribeTaskDefinition(in *ecs.DescribeTaskDefinitionInput) (*ecs.DescribeTaskDefinitionOutput, error) { 17 | return &m.Resp, nil 18 | } 19 | 20 | func TestDescribeTaskDefinition(t *testing.T) { 21 | resp := ecs.DescribeTaskDefinitionOutput{ 22 | TaskDefinition: &ecs.TaskDefinition{ 23 | Family: aws.String("dummy"), 24 | }, 25 | } 26 | taskDefinition := &TaskDefinition{ 27 | awsECS: mockedDescribeTaskDefinition{Resp: resp}, 28 | } 29 | output, err := taskDefinition.DescribeTaskDefinition("dummy") 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | 34 | if *output.Family != "dummy" { 35 | t.Error("Task definition is invalid") 36 | } 37 | 38 | } 39 | 40 | type mockedRegisterTaskDefinition struct { 41 | ecsiface.ECSAPI 42 | Resp ecs.RegisterTaskDefinitionOutput 43 | } 44 | 45 | func (m mockedRegisterTaskDefinition) RegisterTaskDefinition(in *ecs.RegisterTaskDefinitionInput) (*ecs.RegisterTaskDefinitionOutput, error) { 46 | return &m.Resp, nil 47 | } 48 | 49 | func TestRegisterTaskDefinition(t *testing.T) { 50 | resp := ecs.RegisterTaskDefinitionOutput{ 51 | TaskDefinition: &ecs.TaskDefinition{ 52 | Family: aws.String("dummy"), 53 | }, 54 | } 55 | 56 | taskDefinition := &TaskDefinition{ 57 | awsECS: mockedRegisterTaskDefinition{Resp: resp}, 58 | } 59 | output, err := taskDefinition.RegisterTaskDefinition( 60 | &ecs.TaskDefinition{ 61 | Family: aws.String("dummy"), 62 | }, 63 | &Image{ 64 | Repository: "nginx", 65 | Tag: "latest", 66 | }, 67 | ) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | if *output.Family != "dummy" { 72 | t.Error("Task definition is invalid") 73 | } 74 | } 75 | 76 | type mockedECS struct { 77 | ecsiface.ECSAPI 78 | } 79 | 80 | func TestNewContainerDefinition(t *testing.T) { 81 | baseDefinition := &ecs.ContainerDefinition{ 82 | Image: aws.String("nginx:latest"), 83 | } 84 | newImage := &Image{ 85 | Repository: "nginx", 86 | Tag: "master", 87 | } 88 | taskDefinition := &TaskDefinition{ 89 | awsECS: mockedECS{}, 90 | } 91 | newContainer, err := taskDefinition.NewContainerDefinition(baseDefinition, newImage) 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | if *newContainer.Image != "nginx:master" { 96 | t.Error("Container definition is invalid") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /deploy/task_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/request" 9 | "github.com/aws/aws-sdk-go/service/ecs" 10 | "github.com/aws/aws-sdk-go/service/ecs/ecsiface" 11 | ) 12 | 13 | type mockedRunTask struct { 14 | ecsiface.ECSAPI 15 | Run ecs.RunTaskOutput 16 | Describe ecs.DescribeTasksOutput 17 | } 18 | 19 | func (m mockedRunTask) RunTaskWithContext(ctx aws.Context, in *ecs.RunTaskInput, opts ...request.Option) (*ecs.RunTaskOutput, error) { 20 | return &m.Run, nil 21 | } 22 | 23 | func (m mockedRunTask) DescribeTasks(in *ecs.DescribeTasksInput) (*ecs.DescribeTasksOutput, error) { 24 | return &m.Describe, nil 25 | } 26 | 27 | func TestRunTask(t *testing.T) { 28 | runTask := ecs.RunTaskOutput{ 29 | Tasks: []*ecs.Task{ 30 | &ecs.Task{ 31 | ClusterArn: aws.String("dummy-cluster"), 32 | TaskDefinitionArn: aws.String("task-definition-arn"), 33 | Overrides: &ecs.TaskOverride{ 34 | ContainerOverrides: []*ecs.ContainerOverride{ 35 | &ecs.ContainerOverride{ 36 | Command: []*string{ 37 | aws.String("echo"), 38 | }, 39 | Name: aws.String("dummy"), 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | } 46 | describe := ecs.DescribeTasksOutput{ 47 | Tasks: []*ecs.Task{ 48 | &ecs.Task{ 49 | LastStatus: aws.String("STOPPED"), 50 | Containers: []*ecs.Container{ 51 | &ecs.Container{ 52 | ExitCode: aws.Int64(0), 53 | }, 54 | }, 55 | }, 56 | }, 57 | } 58 | task := &Task{ 59 | awsECS: mockedRunTask{Run: runTask, Describe: describe}, 60 | Command: []*string{ 61 | aws.String("echo"), 62 | }, 63 | Timeout: 10 * time.Second, 64 | } 65 | _, err := task.RunTask(&ecs.TaskDefinition{ 66 | TaskDefinitionArn: aws.String("task-definition-arn"), 67 | }) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/h3poteto/ecs-goploy 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.25.25 7 | github.com/mattn/go-shellwords v1.0.3 8 | github.com/pkg/errors v0.9.1 9 | github.com/sirupsen/logrus v1.4.2 10 | github.com/spf13/cobra v0.0.5 11 | github.com/spf13/viper v1.6.2 12 | golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/aws/aws-sdk-go v1.25.25 h1:j3HLOqcDWjNox1DyvJRs+kVQF42Ghtv6oL6cVBfXS3U= 9 | github.com/aws/aws-sdk-go v1.25.25/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 12 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 13 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 14 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 15 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 16 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 17 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 18 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 19 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 20 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 24 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 25 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 26 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 27 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 28 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 29 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 30 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 31 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 32 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 33 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 36 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 37 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 40 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 41 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 42 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 43 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 44 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 45 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 46 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 47 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 48 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 49 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 50 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 51 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 52 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 53 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 54 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 55 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 56 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 57 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 58 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 59 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 60 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 61 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 62 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 63 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 64 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 65 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 66 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 67 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 68 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 69 | github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk= 70 | github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= 71 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 72 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 73 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 74 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 75 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 76 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 77 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 78 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 79 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 80 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 81 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 82 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 83 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 84 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 85 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 86 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 88 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 89 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 90 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 91 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 92 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 93 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 94 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 95 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 96 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 97 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 98 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 99 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 100 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 101 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 102 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 103 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 104 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 105 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 106 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 107 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 108 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 109 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 110 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 111 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 112 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 113 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 114 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 115 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 116 | github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4= 117 | github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4= 118 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 119 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 120 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 121 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 122 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 123 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 124 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 125 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 126 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 127 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 128 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 129 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 130 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 131 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 132 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 133 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 134 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 135 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= 136 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 137 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 138 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 139 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 140 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 141 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 142 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 143 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 144 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 145 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 146 | golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9 h1:DPz9iiH3YoKiKhX/ijjoZvT0VFwK2c6CWYWQ7Zyr8TU= 147 | golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 148 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 149 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 150 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 151 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 152 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 153 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 154 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 155 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 156 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 157 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 158 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 159 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 160 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 162 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 163 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 164 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 165 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 166 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 167 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 168 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 169 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 170 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 171 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 172 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 173 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 174 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 175 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 176 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 177 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 178 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 179 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 180 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 181 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 183 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 185 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 186 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 187 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/h3poteto/ecs-goploy/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.RootCmd.Execute(); err != nil { 12 | fmt.Println(err) 13 | os.Exit(-1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h3poteto/ecs-goploy/26e7c3cf5694b65547be06180cf5ab97fa41e075/packages/.gitkeep --------------------------------------------------------------------------------