From 0793ff96d4967b008f33a21fb64054ee8865ef1a Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Tue, 4 Apr 2017 15:45:34 +0900 Subject: [PATCH 01/10] switch go dep to glide --- Makefile | 9 ++---- glide.lock | 44 ++++++++++++++++++++++++++ glide.yaml | 10 ++++++ lock.json | 86 --------------------------------------------------- manifest.json | 1 - 5 files changed, 56 insertions(+), 94 deletions(-) create mode 100644 glide.lock create mode 100644 glide.yaml delete mode 100644 lock.json delete mode 100644 manifest.json diff --git a/Makefile b/Makefile index 684a69b..e2cd917 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,8 @@ LDFLAGS := -ldflags="-s -w -X \"main.Version=$(VERSION)\" -X \"main.Revision=$(R bin/$(NAME): $(SRCS) @go build -a -tags netgo -installsuffix netgo $(LDFLAGS) -o bin/$(NAME) -dep: -ifeq ($(shell command -v dep 2> /dev/null),) - go get -u github.com/golang/dep/... -endif - -deps: dep - dep ensure +deps: + glide install install: go install $(LDFLAGS) diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..476ca0f --- /dev/null +++ b/glide.lock @@ -0,0 +1,44 @@ +hash: 11cb6f132da94e0cea3dd97da870c896ff31884001672b248c2e8c7ba7d291c5 +updated: 2017-04-04T15:45:10.700235649+09:00 +imports: +- name: github.com/aws/aws-sdk-go + version: 06d3a0c37ae95ae040548a13952501261cb5fd2b + subpackages: + - aws + - aws/awserr + - aws/awsutil + - aws/client + - aws/client/metadata + - aws/corehandlers + - aws/credentials + - aws/credentials/ec2rolecreds + - aws/credentials/endpointcreds + - aws/credentials/stscreds + - aws/defaults + - aws/ec2metadata + - aws/endpoints + - aws/request + - aws/session + - aws/signer/v4 + - private/protocol + - private/protocol/json/jsonutil + - private/protocol/jsonrpc + - private/protocol/query + - private/protocol/query/queryutil + - private/protocol/rest + - private/protocol/xml/xmlutil + - service/ecs + - service/sts +- name: github.com/go-ini/ini + version: e7fea39b01aea8d5671f6858f0532f56e8bff3a5 +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/jmespath/go-jmespath + version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d +- name: github.com/monochromegane/slack-incoming-webhooks + version: 86d1b9ab9a9450c03e86cbe9ee2d0f1bb2de3bbf +- name: github.com/spf13/cobra + version: 7aeaa2cce6ae7e18d2a6ee597b60ee137e999bd9 +- name: github.com/spf13/pflag + version: d16db1e50e33dff1b6cdf37596cef36742128670 +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..4e53899 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,10 @@ +package: github.com/SKAhack/ship +import: +- package: github.com/aws/aws-sdk-go + version: ^1.8.7 + subpackages: + - aws + - aws/session + - service/ecs +- package: github.com/monochromegane/slack-incoming-webhooks +- package: github.com/spf13/cobra diff --git a/lock.json b/lock.json deleted file mode 100644 index 01b37d5..0000000 --- a/lock.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "memo": "dd0152c74789679f82c8343cd4819ccbbcccf82834d50f6698ae88e3e46ac7f0", - "projects": [ - { - "name": "github.com/aws/aws-sdk-go", - "version": "v1.7.3", - "revision": "6669bce73b4e3bc922ff5ea3a3983ede26e02b39", - "packages": [ - "aws", - "aws/awserr", - "aws/awsutil", - "aws/client", - "aws/client/metadata", - "aws/corehandlers", - "aws/credentials", - "aws/credentials/ec2rolecreds", - "aws/credentials/endpointcreds", - "aws/credentials/stscreds", - "aws/defaults", - "aws/ec2metadata", - "aws/endpoints", - "aws/request", - "aws/session", - "aws/signer/v4", - "private/protocol", - "private/protocol/json/jsonutil", - "private/protocol/jsonrpc", - "private/protocol/query", - "private/protocol/query/queryutil", - "private/protocol/rest", - "private/protocol/xml/xmlutil", - "private/waiter", - "service/ecs", - "service/sts" - ] - }, - { - "name": "github.com/go-ini/ini", - "version": "v1.25.2", - "revision": "74bdc99692c3408cb103221e38675ce8fda0a718", - "packages": [ - "." - ] - }, - { - "name": "github.com/inconshreveable/mousetrap", - "branch": "master", - "revision": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75", - "packages": [ - "." - ] - }, - { - "name": "github.com/jmespath/go-jmespath", - "version": "0.2.2", - "revision": "3433f3ea46d9f8019119e7dd41274e112a2359a9", - "packages": [ - "." - ] - }, - { - "name": "github.com/monochromegane/slack-incoming-webhooks", - "branch": "master", - "revision": "86d1b9ab9a9450c03e86cbe9ee2d0f1bb2de3bbf", - "packages": [ - "." - ] - }, - { - "name": "github.com/spf13/cobra", - "branch": "master", - "revision": "16c014f1a19d865b765b420e74508f80eb831ada", - "packages": [ - "." - ] - }, - { - "name": "github.com/spf13/pflag", - "branch": "master", - "revision": "9ff6c6923cfffbcd502984b8e0c80539a94968b7", - "packages": [ - "." - ] - } - ] -} diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 0967ef4..0000000 --- a/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{} From c0da22e5ba6b0837c8ce2cbc2c24e444988c471e Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Tue, 4 Apr 2017 15:46:17 +0900 Subject: [PATCH 02/10] gitignore .local.vimrc --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 43d08e8..a660856 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bin vendor +.local.vimrc From 012b26620c49d8bc57331acf15c4f83603e898c7 Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Wed, 5 Apr 2017 16:10:57 +0900 Subject: [PATCH 03/10] retagging unique ID to image --- cmd/deploy.go | 248 ++++++++++++++++++++++++++++++++++++++++---------- glide.lock | 63 ++++++++++++- glide.yaml | 15 +++ 3 files changed, 275 insertions(+), 51 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index f46836c..402e597 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -4,13 +4,18 @@ import ( "errors" "fmt" "io" + "math/rand" "os" "regexp" + "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecr" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/docker/distribution/reference" + "github.com/oklog/ulid" "github.com/spf13/cobra" slack "github.com/monochromegane/slack-incoming-webhooks" @@ -20,6 +25,7 @@ type deployCmd struct { cluster string serviceName string revision int + tag string slackNotify string } @@ -29,15 +35,10 @@ func NewDeployCommand(out, errOut io.Writer) *cobra.Command { Use: "deploy [options]", Short: "", RunE: func(cmd *cobra.Command, args []string) error { - err := f.execute(cmd, args, out) + log := NewLogger(f.cluster, f.serviceName, f.slackNotify, out) + err := f.execute(cmd, args, log) if err != nil { - sendFailedMessage( - f.cluster, - f.serviceName, - fmt.Sprintf("failed to deploy. cluster: %s, serviceName: %s\n", f.cluster, f.serviceName), - nil, - f.slackNotify, - ) + log.fail(fmt.Sprintf("failed to deploy. cluster: %s, serviceName: %s\n", f.cluster, f.serviceName)) return err } return nil @@ -46,12 +47,13 @@ func NewDeployCommand(out, errOut io.Writer) *cobra.Command { cmd.Flags().StringVar(&f.cluster, "cluster", "", "ECS Cluster Name") cmd.Flags().StringVar(&f.serviceName, "service-name", "", "ECS Service Name") cmd.Flags().IntVar(&f.revision, "revision", 0, "revision of ECS task definition") + cmd.Flags().StringVar(&f.tag, "tag", "latest", "base tag of ECR image") cmd.Flags().StringVar(&f.slackNotify, "slack-notify", "", "slack webhook URL") return cmd } -func (f *deployCmd) execute(_ *cobra.Command, args []string, out io.Writer) error { +func (f *deployCmd) execute(_ *cobra.Command, args []string, l *logger) error { if f.cluster == "" { return errors.New("--cluster is required") } @@ -84,6 +86,10 @@ func (f *deployCmd) execute(_ *cobra.Command, args []string, out io.Writer) erro Region: aws.String(region), }) + ecrClient := ecr.New(sess, &aws.Config{ + Region: aws.String(region), + }) + service, err := describeService(client, f.cluster, f.serviceName) if err != nil { return err @@ -93,55 +99,62 @@ func (f *deployCmd) execute(_ *cobra.Command, args []string, out io.Writer) erro return errors.New(fmt.Sprintf("%s is currently deployed", f.serviceName)) } - taskDefArn := *service.TaskDefinition - taskDefArn, err = specifyRevision(f.revision, taskDefArn) - if err != nil { - return err + var uniqueID string + { + entropy := rand.New(rand.NewSource(time.Now().UnixNano())) + uniqueID = ulid.MustNew(ulid.Now(), entropy).String() } - taskDef, err := describeTaskDefinition(client, taskDefArn) - if err != nil { - return err - } + var taskDef *ecs.TaskDefinition + var registerdTaskDef *ecs.TaskDefinition + { + taskDefArn := *service.TaskDefinition + taskDefArn, err = specifyRevision(f.revision, taskDefArn) + if err != nil { + return err + } - newTaskDef, err := registerTaskDefinition(client, taskDef) - if err != nil { - return err + taskDef, err = describeTaskDefinition(client, taskDefArn) + if err != nil { + return err + } + + newTaskDef, err := createNewTaskDefinition(uniqueID, taskDef) + if err != nil { + return err + } + + img, err := parseDockerImage(*taskDef.ContainerDefinitions[0].Image) + if err != nil { + return err + } + + err = tagDockerImage(ecrClient, img.RepositoryName, f.tag, uniqueID) + if err != nil { + return err + } + + registerdTaskDef, err = registerTaskDefinition(client, newTaskDef) + if err != nil { + return err + } } - sendMessage( - f.cluster, - f.serviceName, - fmt.Sprintf("task definition registerd successfully: revision %d -> %d\n", *taskDef.Revision, *newTaskDef.Revision), - out, - f.slackNotify, - ) + l.log(fmt.Sprintf("task definition registerd successfully: revision %d -> %d\n", *taskDef.Revision, *registerdTaskDef.Revision)) - err = updateService(client, service, newTaskDef) + err = updateService(client, service, registerdTaskDef) if err != nil { return err } - sendMessage( - f.cluster, - f.serviceName, - fmt.Sprintf("service updating\n"), - out, - f.slackNotify, - ) + l.log(fmt.Sprintf("service updating\n")) - err = waitUpdateService(client, f.cluster, f.serviceName, out) + err = waitUpdateService(client, f.cluster, f.serviceName, l) if err != nil { return err } - sendSuccessfulMessage( - f.cluster, - f.serviceName, - fmt.Sprintf("service updated successfully\n"), - out, - f.slackNotify, - ) + l.success(fmt.Sprintf("service updated successfully\n")) return nil } @@ -177,6 +190,47 @@ func describeTaskDefinition(client *ecs.ECS, arn string) (*ecs.TaskDefinition, e return res.TaskDefinition, nil } +func createNewTaskDefinition(id string, taskDef *ecs.TaskDefinition) (*ecs.TaskDefinition, error) { + if len(taskDef.ContainerDefinitions) > 1 { + return nil, errors.New("multiple container is not supported") + } + + newTaskDef := *taskDef // shallow copy + var containers []*ecs.ContainerDefinition + for _, vp := range taskDef.ContainerDefinitions { + v := *vp // shallow copy + img, err := parseDockerImage(*v.Image) + if err != nil { + return nil, err + } + + v.Image = aws.String(fmt.Sprintf("%s:%s", img.Name, id)) + containers = append(containers, &v) + } + newTaskDef.ContainerDefinitions = containers + + return &newTaskDef, nil +} + +type dockerImage struct { + Name string + Tag string + RepositoryName string +} + +func parseDockerImage(image string) (*dockerImage, error) { + ref, err := reference.Parse(image) + if err != nil { + return nil, err + } + components := strings.Split(ref.(reference.Named).Name(), "/") + return &dockerImage{ + Name: ref.(reference.Named).Name(), + Tag: ref.(reference.Tagged).Tag(), + RepositoryName: components[len(components)-1], + }, nil +} + func registerTaskDefinition(client *ecs.ECS, taskDef *ecs.TaskDefinition) (*ecs.TaskDefinition, error) { params := &ecs.RegisterTaskDefinitionInput{ ContainerDefinitions: taskDef.ContainerDefinitions, @@ -212,7 +266,8 @@ func updateService(client *ecs.ECS, service *ecs.Service, taskDef *ecs.TaskDefin return nil } -func waitUpdateService(client *ecs.ECS, cluster, serviceName string, out io.Writer) error { +func waitUpdateService(client *ecs.ECS, cluster, serviceName string, l *logger) error { + start := time.Now() t := time.NewTicker(10 * time.Second) for { select { @@ -222,11 +277,8 @@ func waitUpdateService(client *ecs.ECS, cluster, serviceName string, out io.Writ return err } - for _, v := range s.Deployments { - fmt.Fprintf(out, - "status: %s | desired: %d, pending: %d, running: %d\n", - *v.Status, *v.DesiredCount, *v.PendingCount, *v.RunningCount) - } + elapsed := time.Now().Sub(start) + l.log(fmt.Sprintf("still service updating... [%s]\n", (elapsed/time.Second)*time.Second)) if len(s.Deployments) == 1 && *s.RunningCount == *s.DesiredCount { return nil @@ -248,6 +300,104 @@ func specifyRevision(revision int, arn string) (string, error) { return re.ReplaceAllString(arn, fmt.Sprintf("${1}:%d", revision)), nil } +func tagDockerImage(ecrClient *ecr.ECR, repoName string, fromTag string, toTag string) error { + params := &ecr.BatchGetImageInput{ + ImageIds: []*ecr.ImageIdentifier{{ImageTag: aws.String(fromTag)}}, + RepositoryName: aws.String(repoName), + + AcceptedMediaTypes: []*string{ + aws.String("application/vnd.docker.distribution.manifest.v1+json"), + aws.String("application/vnd.docker.distribution.manifest.v2+json"), + aws.String("application/vnd.oci.image.manifest.v1+json"), + }, + } + img, err := ecrClient.BatchGetImage(params) + if err != nil { + return err + } + + putParams := &ecr.PutImageInput{ + ImageManifest: img.Images[0].ImageManifest, + RepositoryName: aws.String(repoName), + ImageTag: aws.String(toTag), + } + _, err = ecrClient.PutImage(putParams) + if err != nil { + return err + } + + return nil +} + +type logger struct { + Cluster string + ServiceName string + Out io.Writer + SlackWebhookUrl string +} + +func NewLogger(cluster, serviceName, slackWebhookUrl string, out io.Writer) *logger { + return &logger{ + Cluster: cluster, + ServiceName: serviceName, + SlackWebhookUrl: slackWebhookUrl, + Out: out, + } +} + +func (l *logger) log(message string) { + if l.Out != nil { + fmt.Fprintf(l.Out, message) + } + + if l.SlackWebhookUrl != "" { + client := &slack.Client{WebhookURL: l.SlackWebhookUrl} + payload := &slack.Payload{ + Username: "deploy-bot", + Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", l.Cluster, l.ServiceName, message), + } + client.Post(payload) + } +} + +func (l *logger) logWithType(message string, messageType string) { + if messageType == "" { + l.log(message) + return + } + + if l.Out != nil { + fmt.Fprintf(l.Out, message) + } + + if l.SlackWebhookUrl != "" { + color := func() string { + if messageType == "danger" { + return "danger" + } + return "good" + }() + client := &slack.Client{WebhookURL: l.SlackWebhookUrl} + attachment := &slack.Attachment{ + Color: color, + Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", l.Cluster, l.ServiceName, message), + } + payload := &slack.Payload{ + Username: "deploy-bot", + Attachments: []*slack.Attachment{attachment}, + } + client.Post(payload) + } +} + +func (l *logger) success(message string) { + l.logWithType(message, "good") +} + +func (l *logger) fail(message string) { + l.logWithType(message, "danger") +} + func sendMessage(cluster, serviceName, message string, out io.Writer, slackWebhookUrl string) { if out != nil { fmt.Fprintf(out, message) diff --git a/glide.lock b/glide.lock index 476ca0f..4c3bc9f 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 11cb6f132da94e0cea3dd97da870c896ff31884001672b248c2e8c7ba7d291c5 -updated: 2017-04-04T15:45:10.700235649+09:00 +hash: a12a775871d2aa774aedc6b8e8d1297cb47c933393f4045b3e87385485ee906f +updated: 2017-04-04T22:38:12.306494204+09:00 imports: - name: github.com/aws/aws-sdk-go version: 06d3a0c37ae95ae040548a13952501261cb5fd2b @@ -27,18 +27,77 @@ imports: - private/protocol/query/queryutil - private/protocol/rest - private/protocol/xml/xmlutil + - service/ecr - service/ecs - service/sts +- name: github.com/docker/distribution + version: 0d39820aa79e678113b010bae558345fda98db53 + subpackages: + - digest + - reference +- name: github.com/docker/docker + version: 2f35d73b7dc7a9e234ea06f6145a26c37472c775 + subpackages: + - client +- name: github.com/docker/engine-api + version: 3d1601b9d2436a70b0dfc045a23f6503d19195df + subpackages: + - client + - client/transport + - client/transport/cancellable + - types + - types/blkiodev + - types/container + - types/filters + - types/network + - types/reference + - types/registry + - types/strslice + - types/swarm + - types/time + - types/versions +- name: github.com/docker/go-connections + version: e15c02316c12de00874640cd76311849de2aeed5 + subpackages: + - nat + - sockets + - tlsconfig +- name: github.com/docker/go-units + version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52 - name: github.com/go-ini/ini version: e7fea39b01aea8d5671f6858f0532f56e8bff3a5 - name: github.com/inconshreveable/mousetrap version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 - name: github.com/jmespath/go-jmespath version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d +- name: github.com/Microsoft/go-winio + version: fff283ad5116362ca252298cfc9b95828956d85d - name: github.com/monochromegane/slack-incoming-webhooks version: 86d1b9ab9a9450c03e86cbe9ee2d0f1bb2de3bbf +- name: github.com/oklog/ulid + version: d311cb43c92434ec4072dfbbda3400741d0a6337 +- name: github.com/pkg/errors + version: 645ef00459ed84a119197bfb8d8205042c6df63d +- name: github.com/Sirupsen/logrus + version: 55eb11d21d2a31a3cc93838241d04800f52e823d + subpackages: + - formatters/logstash - name: github.com/spf13/cobra version: 7aeaa2cce6ae7e18d2a6ee597b60ee137e999bd9 - name: github.com/spf13/pflag version: d16db1e50e33dff1b6cdf37596cef36742128670 +- name: golang.org/x/net + version: 4876518f9e71663000c348837735820161a42df7 + subpackages: + - context + - context/ctxhttp + - http2 + - http2/hpack + - internal/timeseries + - proxy + - trace +- name: golang.org/x/sys + version: 493114f68206f85e7e333beccfabc11e98cba8dd + subpackages: + - windows testImports: [] diff --git a/glide.yaml b/glide.yaml index 4e53899..af8b981 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,3 +8,18 @@ import: - service/ecs - package: github.com/monochromegane/slack-incoming-webhooks - package: github.com/spf13/cobra +- package: github.com/oklog/ulid + version: ^0.3.0 +- package: github.com/docker/distribution + version: ^2.6.1-rc.2 + subpackages: + - reference +- package: github.com/docker/engine-api + version: ^0.4.0 + subpackages: + - client + - types +- package: github.com/docker/docker + version: ^17.4.0-ce-rc2 + subpackages: + - client From 07b626b105bae71d23641da3be88d835b8e2ecd9 Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Wed, 5 Apr 2017 16:16:36 +0900 Subject: [PATCH 04/10] remove unuse method --- cmd/deploy.go | 52 --------------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 402e597..07a2098 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -397,55 +397,3 @@ func (l *logger) success(message string) { func (l *logger) fail(message string) { l.logWithType(message, "danger") } - -func sendMessage(cluster, serviceName, message string, out io.Writer, slackWebhookUrl string) { - if out != nil { - fmt.Fprintf(out, message) - } - - if slackWebhookUrl != "" { - func() { - client := &slack.Client{WebhookURL: slackWebhookUrl} - payload := &slack.Payload{ - Username: "deploy-bot", - Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", cluster, serviceName, message), - } - client.Post(payload) - }() - } -} - -func sendSuccessfulMessage(cluster, serviceName, message string, out io.Writer, slackWebhookUrl string) { - sendEndMessage(true, cluster, serviceName, message, out, slackWebhookUrl) -} - -func sendFailedMessage(cluster, serviceName, message string, out io.Writer, slackWebhookUrl string) { - sendEndMessage(false, cluster, serviceName, message, out, slackWebhookUrl) -} - -func sendEndMessage(isSuccess bool, cluster, serviceName, message string, out io.Writer, slackWebhookUrl string) { - if out != nil { - fmt.Fprintf(out, message) - } - - if slackWebhookUrl != "" { - func() { - color := func() string { - if isSuccess { - return "good" - } - return "danger" - }() - client := &slack.Client{WebhookURL: slackWebhookUrl} - attachment := &slack.Attachment{ - Color: color, - Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", cluster, serviceName, message), - } - payload := &slack.Payload{ - Username: "deploy-bot", - Attachments: []*slack.Attachment{attachment}, - } - client.Post(payload) - }() - } -} From 75e044656b586f68b32c6759d4eb78064d9ce90d Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Wed, 5 Apr 2017 21:08:36 +0900 Subject: [PATCH 05/10] refactor logger --- cmd/deploy.go | 69 ++++----------------------------------------- cmd/logger.go | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 cmd/logger.go diff --git a/cmd/deploy.go b/cmd/deploy.go index 07a2098..8ff101b 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -329,71 +329,14 @@ func tagDockerImage(ecrClient *ecr.ECR, repoName string, fromTag string, toTag s return nil } -type logger struct { - Cluster string - ServiceName string - Out io.Writer - SlackWebhookUrl string -} - -func NewLogger(cluster, serviceName, slackWebhookUrl string, out io.Writer) *logger { - return &logger{ - Cluster: cluster, - ServiceName: serviceName, - SlackWebhookUrl: slackWebhookUrl, - Out: out, - } -} - -func (l *logger) log(message string) { - if l.Out != nil { - fmt.Fprintf(l.Out, message) +func getAWSRegion() string { + if os.Getenv("AWS_REGION") != "" { + return os.Getenv("AWS_REGION") } - if l.SlackWebhookUrl != "" { - client := &slack.Client{WebhookURL: l.SlackWebhookUrl} - payload := &slack.Payload{ - Username: "deploy-bot", - Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", l.Cluster, l.ServiceName, message), - } - client.Post(payload) + if os.Getenv("AWS_DEFAULT_REGION") != "" { + return os.Getenv("AWS_DEFAULT_REGION") } -} - -func (l *logger) logWithType(message string, messageType string) { - if messageType == "" { - l.log(message) - return - } - - if l.Out != nil { - fmt.Fprintf(l.Out, message) - } - - if l.SlackWebhookUrl != "" { - color := func() string { - if messageType == "danger" { - return "danger" - } - return "good" - }() - client := &slack.Client{WebhookURL: l.SlackWebhookUrl} - attachment := &slack.Attachment{ - Color: color, - Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", l.Cluster, l.ServiceName, message), - } - payload := &slack.Payload{ - Username: "deploy-bot", - Attachments: []*slack.Attachment{attachment}, - } - client.Post(payload) - } -} - -func (l *logger) success(message string) { - l.logWithType(message, "good") -} -func (l *logger) fail(message string) { - l.logWithType(message, "danger") + return "" } diff --git a/cmd/logger.go b/cmd/logger.go new file mode 100644 index 0000000..170146a --- /dev/null +++ b/cmd/logger.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "fmt" + "io" + + slack "github.com/monochromegane/slack-incoming-webhooks" +) + +type logger struct { + Cluster string + ServiceName string + Out io.Writer + SlackWebhookUrl string +} + +func NewLogger(cluster, serviceName, slackWebhookUrl string, out io.Writer) *logger { + return &logger{ + Cluster: cluster, + ServiceName: serviceName, + SlackWebhookUrl: slackWebhookUrl, + Out: out, + } +} + +func (l *logger) log(message string) { + if l.Out != nil { + fmt.Fprintf(l.Out, message) + } + + if l.SlackWebhookUrl != "" { + client := &slack.Client{WebhookURL: l.SlackWebhookUrl} + payload := &slack.Payload{ + Username: "deploy-bot", + Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", l.Cluster, l.ServiceName, message), + } + client.Post(payload) + } +} + +func (l *logger) logWithType(message string, messageType string) { + if messageType == "" { + l.log(message) + return + } + + if l.Out != nil { + fmt.Fprintf(l.Out, message) + } + + if l.SlackWebhookUrl != "" { + color := func() string { + if messageType == "danger" { + return "danger" + } + return "good" + }() + client := &slack.Client{WebhookURL: l.SlackWebhookUrl} + attachment := &slack.Attachment{ + Color: color, + Text: fmt.Sprintf("cluster: %s, serviceName: %s\n%s", l.Cluster, l.ServiceName, message), + } + payload := &slack.Payload{ + Username: "deploy-bot", + Attachments: []*slack.Attachment{attachment}, + } + client.Post(payload) + } +} + +func (l *logger) success(message string) { + l.logWithType(message, "good") +} + +func (l *logger) fail(message string) { + l.logWithType(message, "danger") +} From 46b54b209ebf787df3252bac6ff112daa9b62631 Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Wed, 5 Apr 2017 21:09:03 +0900 Subject: [PATCH 06/10] add state manager --- cmd/deploy.go | 31 ++++--- cmd/deploystate.go | 195 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 cmd/deploystate.go diff --git a/cmd/deploy.go b/cmd/deploy.go index 8ff101b..205c171 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -14,11 +14,10 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ecr" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/docker/distribution/reference" "github.com/oklog/ulid" "github.com/spf13/cobra" - - slack "github.com/monochromegane/slack-incoming-webhooks" ) type deployCmd struct { @@ -26,6 +25,7 @@ type deployCmd struct { serviceName string revision int tag string + backend string slackNotify string } @@ -48,6 +48,7 @@ func NewDeployCommand(out, errOut io.Writer) *cobra.Command { cmd.Flags().StringVar(&f.serviceName, "service-name", "", "ECS Service Name") cmd.Flags().IntVar(&f.revision, "revision", 0, "revision of ECS task definition") cmd.Flags().StringVar(&f.tag, "tag", "latest", "base tag of ECR image") + cmd.Flags().StringVar(&f.backend, "backend", "SSM", "Backend type of state manager") cmd.Flags().StringVar(&f.slackNotify, "slack-notify", "", "slack webhook URL") return cmd @@ -62,17 +63,7 @@ func (f *deployCmd) execute(_ *cobra.Command, args []string, l *logger) error { return errors.New("--service-name is required") } - region := func() string { - if os.Getenv("AWS_REGION") != "" { - return os.Getenv("AWS_REGION") - } - - if os.Getenv("AWS_DEFAULT_REGION") != "" { - return os.Getenv("AWS_DEFAULT_REGION") - } - - return "" - }() + region := getAWSRegion() if region == "" { return errors.New("AWS region is not found. please set a AWS_DEFAULT_REGION or AWS_REGION") } @@ -140,6 +131,15 @@ func (f *deployCmd) execute(_ *cobra.Command, args []string, l *logger) error { } } + pusher, err := NewStatePusher(f.backend, f.cluster, f.serviceName) + if err != nil { + return err + } + err = pusher.PushPendingState(int(*taskDef.Revision), int(*registerdTaskDef.Revision)) + if err != nil { + return err + } + l.log(fmt.Sprintf("task definition registerd successfully: revision %d -> %d\n", *taskDef.Revision, *registerdTaskDef.Revision)) err = updateService(client, service, registerdTaskDef) @@ -154,6 +154,11 @@ func (f *deployCmd) execute(_ *cobra.Command, args []string, l *logger) error { return err } + err = pusher.UpdateState(int(*taskDef.Revision), int(*registerdTaskDef.Revision)) + if err != nil { + return err + } + l.success(fmt.Sprintf("service updated successfully\n")) return nil diff --git a/cmd/deploystate.go b/cmd/deploystate.go new file mode 100644 index 0000000..abe93b1 --- /dev/null +++ b/cmd/deploystate.go @@ -0,0 +1,195 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ssm" +) + +type deployStatus int + +const ( + deployStatus_UNKNOWN deployStatus = iota + deployStatus_PENDING + deployStatus_DEPLOYED +) + +type deployState struct { + Revision int `json:"revision"` + Status deployStatus `json:"status"` + Cause string `json:"cause"` +} + +type statePusher interface { + PushPendingState(int, int) error + UpdateState(int, int) error + Pull() ([]*deployState, error) +} + +func NewStatePusher(backend, clusterName, serviceName string) (statePusher, error) { + if backend == "SSM" { + return NewSSMStatePusher(clusterName, serviceName) + } + return NewSSMStatePusher(clusterName, serviceName) +} + +type ssmStatePusher struct { + Client *ssm.SSM + ClusterName string + ServiceName string +} + +func NewSSMStatePusher(clusterName, serviceName string) (*ssmStatePusher, error) { + sess, err := session.NewSession() + if err != nil { + return nil, err + } + + region := getAWSRegion() + if region == "" { + return nil, errors.New("AWS region is not found. please set a AWS_DEFAULT_REGION or AWS_REGION") + } + + client := ssm.New(sess, &aws.Config{ + Region: aws.String(region), + }) + + return &ssmStatePusher{ + Client: client, + ClusterName: clusterName, + ServiceName: serviceName, + }, nil +} + +func (s *ssmStatePusher) Push(v string) error { + p := &ssm.PutParameterInput{ + Name: aws.String(s.getName()), + Type: aws.String("String"), + Value: aws.String(v), + Overwrite: aws.Bool(true), + } + _, err := s.Client.PutParameter(p) + if err != nil { + return err + } + return nil +} + +func (s *ssmStatePusher) PushPendingState(oldRevision, revision int) error { + state, err := s.Pull() + if err != nil { + return err + } + for _, v := range state { + if v.Revision == revision { + return errors.New(fmt.Sprintf("validation error: revision %d is already exists", revision)) + } + } + state = append(state, &deployState{ + Revision: revision, + Status: deployStatus_PENDING, + Cause: fmt.Sprintf("deploy: %d -> %d", oldRevision, revision), + }) + + from := 0 + if len(state) > 5 { + from = len(state) - 5 + } + + state = state[from:] + b, err := json.Marshal(state) + if err != nil { + return err + } + + err = s.Push(string(b)) + if err != nil { + return err + } + + return nil +} + +func (s *ssmStatePusher) UpdateState(oldRevision, revision int) error { + state, err := s.Pull() + if err != nil { + return err + } + for i, v := range state { + if v.Revision == revision { + state[i].Status = deployStatus_DEPLOYED + + b, err := json.Marshal(state) + if err != nil { + return err + } + + err = s.Push(string(b)) + if err != nil { + return err + } + + return nil + } + } + + return errors.New("can not found a current state") +} + +func (s *ssmStatePusher) Pull() ([]*deployState, error) { + filter := &ssm.ParametersFilter{ + Key: aws.String("Name"), + Values: []*string{ + aws.String(s.getName()), + }, + } + filters := []*ssm.ParametersFilter{filter} + + var key *string + { + p := &ssm.DescribeParametersInput{ + Filters: filters, + MaxResults: aws.Int64(1), + } + re, err := s.Client.DescribeParameters(p) + if err != nil { + return nil, err + } + if len(re.Parameters) == 0 { + return []*deployState{}, nil + } + + key = re.Parameters[0].Name + } + + var v *string + { + p := &ssm.GetParametersInput{ + Names: []*string{key}, + WithDecryption: aws.Bool(false), + } + re, err := s.Client.GetParameters(p) + if err != nil { + return nil, err + } + + v = re.Parameters[0].Value + } + + var states []*deployState + err := json.NewDecoder(strings.NewReader(*v)).Decode(&states) + if err != nil { + return nil, err + } + + return states, nil +} + +func (s *ssmStatePusher) getName() string { + return fmt.Sprintf("deploy-state.%s.%s", s.ClusterName, s.ServiceName) +} From 93b35b081643b9f01acbc6507073abb1f8db25d2 Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Wed, 5 Apr 2017 21:09:16 +0900 Subject: [PATCH 07/10] update dependencies --- glide.lock | 57 +++--------------------------------------------------- glide.yaml | 9 --------- 2 files changed, 3 insertions(+), 63 deletions(-) diff --git a/glide.lock b/glide.lock index 4c3bc9f..a2e5b8c 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,8 @@ -hash: a12a775871d2aa774aedc6b8e8d1297cb47c933393f4045b3e87385485ee906f -updated: 2017-04-04T22:38:12.306494204+09:00 +hash: eff6a27f282eb3c0c1a32dbd070f339aefdd20103d9217849ab8666fb241c6ab +updated: 2017-04-05T16:15:40.000132963+09:00 imports: - name: github.com/aws/aws-sdk-go - version: 06d3a0c37ae95ae040548a13952501261cb5fd2b + version: 3a4119172097bf8725eb7c1b96b7957cfe2d92dc subpackages: - aws - aws/awserr @@ -35,69 +35,18 @@ imports: subpackages: - digest - reference -- name: github.com/docker/docker - version: 2f35d73b7dc7a9e234ea06f6145a26c37472c775 - subpackages: - - client -- name: github.com/docker/engine-api - version: 3d1601b9d2436a70b0dfc045a23f6503d19195df - subpackages: - - client - - client/transport - - client/transport/cancellable - - types - - types/blkiodev - - types/container - - types/filters - - types/network - - types/reference - - types/registry - - types/strslice - - types/swarm - - types/time - - types/versions -- name: github.com/docker/go-connections - version: e15c02316c12de00874640cd76311849de2aeed5 - subpackages: - - nat - - sockets - - tlsconfig -- name: github.com/docker/go-units - version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52 - name: github.com/go-ini/ini version: e7fea39b01aea8d5671f6858f0532f56e8bff3a5 - name: github.com/inconshreveable/mousetrap version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 - name: github.com/jmespath/go-jmespath version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d -- name: github.com/Microsoft/go-winio - version: fff283ad5116362ca252298cfc9b95828956d85d - name: github.com/monochromegane/slack-incoming-webhooks version: 86d1b9ab9a9450c03e86cbe9ee2d0f1bb2de3bbf - name: github.com/oklog/ulid version: d311cb43c92434ec4072dfbbda3400741d0a6337 -- name: github.com/pkg/errors - version: 645ef00459ed84a119197bfb8d8205042c6df63d -- name: github.com/Sirupsen/logrus - version: 55eb11d21d2a31a3cc93838241d04800f52e823d - subpackages: - - formatters/logstash - name: github.com/spf13/cobra version: 7aeaa2cce6ae7e18d2a6ee597b60ee137e999bd9 - name: github.com/spf13/pflag version: d16db1e50e33dff1b6cdf37596cef36742128670 -- name: golang.org/x/net - version: 4876518f9e71663000c348837735820161a42df7 - subpackages: - - context - - context/ctxhttp - - http2 - - http2/hpack - - internal/timeseries - - proxy - - trace -- name: golang.org/x/sys - version: 493114f68206f85e7e333beccfabc11e98cba8dd - subpackages: - - windows testImports: [] diff --git a/glide.yaml b/glide.yaml index af8b981..223893c 100644 --- a/glide.yaml +++ b/glide.yaml @@ -14,12 +14,3 @@ import: version: ^2.6.1-rc.2 subpackages: - reference -- package: github.com/docker/engine-api - version: ^0.4.0 - subpackages: - - client - - types -- package: github.com/docker/docker - version: ^17.4.0-ce-rc2 - subpackages: - - client From de09ce103bf79911835c2f1fed1817c842df46cc Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Wed, 5 Apr 2017 21:09:45 +0900 Subject: [PATCH 08/10] refactor Makefile --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e2cd917..7ea92c7 100644 --- a/Makefile +++ b/Makefile @@ -8,18 +8,22 @@ LDFLAGS := -ldflags="-s -w -X \"main.Version=$(VERSION)\" -X \"main.Revision=$(R bin/$(NAME): $(SRCS) @go build -a -tags netgo -installsuffix netgo $(LDFLAGS) -o bin/$(NAME) +.PHONY: deps deps: glide install +.PHONY: install install: go install $(LDFLAGS) +.PHONY: clean clean: rm -rf bin rm -rf vendor/* rm -rf dist DIST_DIRS := find ./ -type d -exec +.PHONY: dist dist: bin/${NAME} mkdir -p dist cd bin && \ @@ -27,4 +31,6 @@ dist: bin/${NAME} $(DIST_DIRS) zip -r ../dist/$(NAME)-$(VERSION).zip {} \; && \ cd .. -.PHONY: deps clean install dist +.PHONY: test +test: + @go test $$(go list ./... | grep -v '/vendor/') -cover From 7b7c15c17d6699bd05528ec44a47af27bd4d860a Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Thu, 6 Apr 2017 12:15:33 +0900 Subject: [PATCH 09/10] refactor deploystate --- cmd/deploy.go | 9 ++++++--- cmd/deploystate.go | 17 ++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 205c171..fb930b7 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -48,7 +48,7 @@ func NewDeployCommand(out, errOut io.Writer) *cobra.Command { cmd.Flags().StringVar(&f.serviceName, "service-name", "", "ECS Service Name") cmd.Flags().IntVar(&f.revision, "revision", 0, "revision of ECS task definition") cmd.Flags().StringVar(&f.tag, "tag", "latest", "base tag of ECR image") - cmd.Flags().StringVar(&f.backend, "backend", "SSM", "Backend type of state manager") + cmd.Flags().StringVar(&f.backend, "backend", "SSM", "Backend type of history manager") cmd.Flags().StringVar(&f.slackNotify, "slack-notify", "", "slack webhook URL") return cmd @@ -135,7 +135,10 @@ func (f *deployCmd) execute(_ *cobra.Command, args []string, l *logger) error { if err != nil { return err } - err = pusher.PushPendingState(int(*taskDef.Revision), int(*registerdTaskDef.Revision)) + err = pusher.PushState( + int(*registerdTaskDef.Revision), + fmt.Sprintf("deploy: %d -> %d", *taskDef.Revision, *registerdTaskDef.Revision), + ) if err != nil { return err } @@ -154,7 +157,7 @@ func (f *deployCmd) execute(_ *cobra.Command, args []string, l *logger) error { return err } - err = pusher.UpdateState(int(*taskDef.Revision), int(*registerdTaskDef.Revision)) + err = pusher.UpdateState(int(*registerdTaskDef.Revision)) if err != nil { return err } diff --git a/cmd/deploystate.go b/cmd/deploystate.go index abe93b1..ab899f9 100644 --- a/cmd/deploystate.go +++ b/cmd/deploystate.go @@ -26,8 +26,8 @@ type deployState struct { } type statePusher interface { - PushPendingState(int, int) error - UpdateState(int, int) error + PushState(int, string) error + UpdateState(int) error Pull() ([]*deployState, error) } @@ -80,20 +80,15 @@ func (s *ssmStatePusher) Push(v string) error { return nil } -func (s *ssmStatePusher) PushPendingState(oldRevision, revision int) error { +func (s *ssmStatePusher) PushState(revision int, cause string) error { state, err := s.Pull() if err != nil { return err } - for _, v := range state { - if v.Revision == revision { - return errors.New(fmt.Sprintf("validation error: revision %d is already exists", revision)) - } - } state = append(state, &deployState{ Revision: revision, Status: deployStatus_PENDING, - Cause: fmt.Sprintf("deploy: %d -> %d", oldRevision, revision), + Cause: cause, }) from := 0 @@ -115,13 +110,13 @@ func (s *ssmStatePusher) PushPendingState(oldRevision, revision int) error { return nil } -func (s *ssmStatePusher) UpdateState(oldRevision, revision int) error { +func (s *ssmStatePusher) UpdateState(revision int) error { state, err := s.Pull() if err != nil { return err } for i, v := range state { - if v.Revision == revision { + if v.Revision == revision && v.Status == deployStatus_PENDING { state[i].Status = deployStatus_DEPLOYED b, err := json.Marshal(state) From 6e5a96df3704526e212224db2904efa523a65d0d Mon Sep 17 00:00:00 2001 From: Hiroki Sato Date: Thu, 6 Apr 2017 12:15:50 +0900 Subject: [PATCH 10/10] add rollback command --- cmd/rollback.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 139 insertions(+) create mode 100644 cmd/rollback.go diff --git a/cmd/rollback.go b/cmd/rollback.go new file mode 100644 index 0000000..c44158a --- /dev/null +++ b/cmd/rollback.go @@ -0,0 +1,138 @@ +package cmd + +import ( + "errors" + "fmt" + "io" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/spf13/cobra" +) + +type rollbackCmd struct { + cluster string + serviceName string + backend string + slackNotify string +} + +func NewRollbackCommand(out, errOut io.Writer) *cobra.Command { + f := &rollbackCmd{} + cmd := &cobra.Command{ + Use: "rollback [options]", + Short: "", + RunE: func(cmd *cobra.Command, args []string) error { + log := NewLogger(f.cluster, f.serviceName, f.slackNotify, out) + err := f.execute(cmd, args, log) + if err != nil { + log.fail(fmt.Sprintf("failed to deploy. cluster: %s, serviceName: %s\n", f.cluster, f.serviceName)) + return err + } + return nil + }, + } + cmd.Flags().StringVar(&f.cluster, "cluster", "", "ECS Cluster Name") + cmd.Flags().StringVar(&f.serviceName, "service-name", "", "ECS Service Name") + cmd.Flags().StringVar(&f.backend, "backend", "SSM", "Backend type of state manager") + cmd.Flags().StringVar(&f.slackNotify, "slack-notify", "", "slack webhook URL") + + return cmd +} + +func (f *rollbackCmd) execute(_ *cobra.Command, args []string, l *logger) error { + if f.cluster == "" { + return errors.New("--cluster is required") + } + + if f.serviceName == "" { + return errors.New("--service-name is required") + } + + region := getAWSRegion() + if region == "" { + return errors.New("AWS region is not found. please set a AWS_DEFAULT_REGION or AWS_REGION") + } + + sess, err := session.NewSession() + if err != nil { + return err + } + + client := ecs.New(sess, &aws.Config{ + Region: aws.String(region), + }) + + pusher, err := NewStatePusher(f.backend, f.cluster, f.serviceName) + if err != nil { + return err + } + + states, err := pusher.Pull() + if err != nil { + return err + } + if len(states) < 2 { + return errors.New("can not found a prev state") + } + prevState := states[len(states)-2] + state := states[len(states)-1] + if state.Status != deployStatus_DEPLOYED { + return errors.New("found pending deploy") + } + + service, err := describeService(client, f.cluster, f.serviceName) + if err != nil { + return err + } + + if len(service.Deployments) > 1 { + return errors.New(fmt.Sprintf("%s is currently deployed", f.serviceName)) + } + + var taskDef *ecs.TaskDefinition + { + taskDefArn := *service.TaskDefinition + taskDefArn, err = specifyRevision(prevState.Revision, taskDefArn) + if err != nil { + return err + } + + taskDef, err = describeTaskDefinition(client, taskDefArn) + if err != nil { + return err + } + } + + l.log(fmt.Sprintf("rollback: revision %d -> %d\n", state.Revision, prevState.Revision)) + + err = pusher.PushState( + prevState.Revision, + fmt.Sprintf("rollback: %d -> %d", state.Revision, prevState.Revision), + ) + if err != nil { + return err + } + + err = updateService(client, service, taskDef) + if err != nil { + return err + } + + l.log(fmt.Sprintf("service updating\n")) + + err = waitUpdateService(client, f.cluster, f.serviceName, l) + if err != nil { + return err + } + + err = pusher.UpdateState(prevState.Revision) + if err != nil { + return err + } + + l.success(fmt.Sprintf("service updated successfully\n")) + + return nil +} diff --git a/main.go b/main.go index ad5fe0f..05cd373 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ var rootCmd = &cobra.Command{ func main() { rootCmd.AddCommand( cmd.NewDeployCommand(os.Stdout, os.Stderr), + cmd.NewRollbackCommand(os.Stdout, os.Stderr), ) if err := rootCmd.Execute(); err != nil {