From 47a9c287463719abc6d76d6c11119877e0bf0d2d Mon Sep 17 00:00:00 2001 From: hamstah Date: Sun, 28 Jan 2018 22:02:00 +0000 Subject: [PATCH] Added ecs-deploy and updated docs --- README.md | 10 +-- VERSION | 2 +- ecs/deploy/main.go | 137 +++++++++++++++++++++++++++++++++++++ scripts/create-release.py | 52 ++++++++++++++ scripts/prepare-release.sh | 7 ++ 5 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 ecs/deploy/main.go create mode 100755 scripts/create-release.py create mode 100755 scripts/prepare-release.sh diff --git a/README.md b/README.md index f9cb5ce..145dfd6 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,10 @@ Some specialised tools to avoid pulling boto3 -* `elb`: ELB classic only (no ALB). Given a name returns the zone53 record associated with the ELB, including scheme (https returned if both available) and port. -* `elb-name`: Both ELB classic and ALB. Given a name, returns route53 record associated with the ELB. Does not include scheme or port as it doesn't check listeners. -* `s3-download`: Download a single file from s3 +* `cloudwatch-put-metric-data`: Basic sending a metric value to cloudwatch * `ec2-ip-from-name`: Given an EC2 name, list up to `-max-results` IPs associated with instances with that name (default is 1). -* `ecs`: Run a task definition +* `ecs-deploy`: Update the container images of a task and update services to use it +* `ecs-run-task`: Run a task definition +* `elb-resolve-elb-external-url`: ELB classic only (no ALB). Given a name returns the zone53 record associated with the ELB, including scheme (https returned if both available) and port. +* `elb-resolve-alb-external-url`: Both ELB classic and ALB. Given a name, returns route53 record associated with the ELB. Does not include scheme or port as it doesn't check listeners. +* `s3-download`: Download a single file from s3 diff --git a/VERSION b/VERSION index 8c50098..a3ec5a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1 +3.2 diff --git a/ecs/deploy/main.go b/ecs/deploy/main.go new file mode 100644 index 0000000..3ae9640 --- /dev/null +++ b/ecs/deploy/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecs" + + kingpin "gopkg.in/alecthomas/kingpin.v2" +) + +var ( + app = kingpin.New("ecs-deploy", "Update a task definition on ECS") + region = app.Flag("region", "AWS Region").Default("eu-west-1").String() + taskName = app.Flag("task-name", "ECS task name").Required().String() + clusterName = app.Flag("cluster", "ECS cluster").Required().String() + services = app.Flag("service", "ECS services").Required().Strings() + images = app.Flag("images", "Change the images to the new ones. Container name=image").StringMap() + timeout = app.Flag("timeout", "Timeout when waiting for services to update").Default("300s").Duration() +) + + +func getTaskDefinition(svc *ecs.ECS, taskName string) (*ecs.TaskDefinition) { + input := &ecs.DescribeTaskDefinitionInput{ + TaskDefinition: aws.String(taskName), + } + + result, err := svc.DescribeTaskDefinition(input) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to fetch the task definition", err) + os.Exit(1) + } + return result.TaskDefinition +} + +func updateTaskDefinition(svc *ecs.ECS, taskDefinition *ecs.TaskDefinition) (*ecs.TaskDefinition) { + updateInput := &ecs.RegisterTaskDefinitionInput{ + ContainerDefinitions: taskDefinition.ContainerDefinitions, + Cpu: taskDefinition.Cpu, + ExecutionRoleArn: taskDefinition.ExecutionRoleArn, + Family: taskDefinition.Family, + Memory: taskDefinition.Memory, + NetworkMode: taskDefinition.NetworkMode, + PlacementConstraints: taskDefinition.PlacementConstraints, + RequiresCompatibilities: taskDefinition.RequiresCompatibilities, + TaskRoleArn: taskDefinition.TaskRoleArn, + Volumes: taskDefinition.Volumes, + } + + updateResult, err := svc.RegisterTaskDefinition(updateInput) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to update the task definition", err) + os.Exit(1) + } + return updateResult.TaskDefinition +} + +func main() { + kingpin.MustParse(app.Parse(os.Args[1:])) + + config := aws.Config{Region: aws.String(*region)} + session := session.New(&config) + svc := ecs.New(session) + + taskDefinition := getTaskDefinition(svc, *taskName) + + if len(*images) != 0 { + for _, containerDefinition := range taskDefinition.ContainerDefinitions { + newImage := (*images)[*containerDefinition.Name] + if newImage != "" { + containerDefinition.Image = &newImage + } + } + } + + newTaskDefinition := updateTaskDefinition(svc, taskDefinition) + fmt.Println(*newTaskDefinition.TaskDefinitionArn) + + pending := 0 + for _, service := range *services { + + updateServiceInput := &ecs.UpdateServiceInput{ + Cluster: aws.String(*clusterName), + Service: aws.String(service), + TaskDefinition: aws.String(*newTaskDefinition.TaskDefinitionArn), + } + _, err := svc.UpdateService(updateServiceInput) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to update service", service, err) + } else { + pending += 1 + } + } + + serviceNamesInput := []*string{} + for _, service := range *services { + serviceNamesInput = append(serviceNamesInput, aws.String(service)) + } + + servicesInput := &ecs.DescribeServicesInput{ + Cluster: aws.String(*clusterName), + Services: serviceNamesInput, + } + + start := time.Now() + previousPending := 0 + for pending > 0 { + servicesResult, err := svc.DescribeServices(servicesInput) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to fetch services", err) + os.Exit(2) + } + + previousPending = pending + pending = 0 + for _, service := range servicesResult.Services { + + if *service.Deployments[0].PendingCount != 0 { + pending += 1 + } + } + + if pending != 0 { + if time.Since(start) >= *timeout { + fmt.Println(os.Stderr, fmt.Sprintf("%s still pending, giving up after %s", pending, *timeout)) + os.Exit(3) + } + if previousPending != pending { + fmt.Println(fmt.Sprintf("Waiting for %d service(s) to become ready", pending)) + } + time.Sleep(1 * time.Second) + } + } +} diff --git a/scripts/create-release.py b/scripts/create-release.py new file mode 100755 index 0000000..a9545f9 --- /dev/null +++ b/scripts/create-release.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +import github3 +import getpass +import os +import mimetypes + +base = os.path.join(os.path.dirname(__file__), "..") +version = open(os.path.join(base, "VERSION")).read().strip() + +last_2fa = None + +def my_two_factor_function(): + global last_2fa + code = '' + while not code: + code = input('Enter 2FA code: [%s] ' % last_2fa) or last_2fa + last_2fa = code + return code + + +mimetypes.init() + +current_user = os.getenv("USER") +username = input('Username: [%s] ' % current_user) or current_user +password = getpass.getpass('Password: ') + +client = github3.login(username, password, two_factor_callback=my_two_factor_function) + +repo = client.repository(username, 'awstools') + +release = repo.release_from_tag('v%s' % version) +if release is None: + release = repo.create_release('v%s' % version, draft=False, prerelease=False) + +bin_dir = os.path.join(base, "bin") +for file in os.listdir(bin_dir): + rel_file = os.path.join(bin_dir, file) + content_type, _ = mimetypes.guess_type(rel_file) + if content_type is None: + content_type = "application/octet-stream" + + print(rel_file, content_type) + try: + asset = release.upload_asset( + content_type=content_type, + name=file, + asset=open(rel_file, 'rb').read(), + ) + except Exception as e: + print(e) + + print("\n\n\n") diff --git a/scripts/prepare-release.sh b/scripts/prepare-release.sh new file mode 100755 index 0000000..841cc99 --- /dev/null +++ b/scripts/prepare-release.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +base=$(dirname $0)/.. +version=$(cat ${base}/VERSION) + +git tag -s v${version} -m "v${version}" +git push origin v${version}