From bddefa6cd029596de11f9fc2d73dde394455a1c9 Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 00:44:07 +0900 Subject: [PATCH 01/11] wip --- .editorconfig | 4 +- cage.go | 37 ++- cli/cage/commands/command.go | 25 +- cli/cage/commands/flags.go | 16 +- cli/cage/commands/recreate.go | 55 ++++ cli/cage/commands/rollout.go | 5 +- cli/cage/commands/run.go | 3 + cli/cage/commands/up.go | 5 +- cli/cage/main.go | 14 +- cli/cage/prompt/prompt.go | 75 ++++++ cli/cage/prompt/prompt_test.go | 66 +++++ env.go | 8 +- env_test.go | 2 +- fixtures/service.json | 81 +++--- fixtures/task-definition.json | 442 ++++++++++++++------------------- go.mod | 38 +-- go.sum | 80 +++--- recreate.go | 149 +++++++++++ recreate_test.go | 35 +++ rollout.go | 38 +-- rollout_test.go | 259 ++++++------------- run.go | 4 +- run_test.go | 179 +++++-------- task_definition.go | 37 +++ task_definition_test.go | 23 ++ test/context.go | 90 +++---- test/fake_timer.go | 29 +++ test/setup.go | 88 +++++++ test/task_definiton.go | 83 +++++++ time.go | 18 +- up.go | 27 +- up_test.go | 19 +- util.go | 34 ++- 33 files changed, 1225 insertions(+), 843 deletions(-) create mode 100644 cli/cage/commands/recreate.go create mode 100644 cli/cage/prompt/prompt.go create mode 100644 cli/cage/prompt/prompt_test.go create mode 100644 recreate.go create mode 100644 recreate_test.go create mode 100644 task_definition.go create mode 100644 task_definition_test.go create mode 100644 test/fake_timer.go create mode 100644 test/setup.go create mode 100644 test/task_definiton.go diff --git a/.editorconfig b/.editorconfig index 4491afa..877f56e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ [*.go] -indent_size = 4 +indent_size = 2 indent_style = tab [Shakefile] indent_size = 2 -indent_style = tab \ No newline at end of file +indent_style = tab diff --git a/cage.go b/cage.go index 7702420..30729a4 100644 --- a/cage.go +++ b/cage.go @@ -2,6 +2,7 @@ package cage import ( "context" + "time" "github.com/loilo-inc/canarycage/awsiface" ) @@ -10,27 +11,39 @@ type Cage interface { Up(ctx context.Context) (*UpResult, error) Run(ctx context.Context, input *RunInput) (*RunResult, error) RollOut(ctx context.Context) (*RollOutResult, error) + Recreate(ctx context.Context) (*RecreateResult, error) +} + +type Time interface { + Now() time.Time + NewTimer(time.Duration) *time.Timer } type cage struct { - env *Envars - ecs awsiface.EcsClient - alb awsiface.AlbClient - ec2 awsiface.Ec2Client + env *Envars + ecs awsiface.EcsClient + alb awsiface.AlbClient + ec2 awsiface.Ec2Client + time Time } type Input struct { - Env *Envars - ECS awsiface.EcsClient - ALB awsiface.AlbClient - EC2 awsiface.Ec2Client + Env *Envars + ECS awsiface.EcsClient + ALB awsiface.AlbClient + EC2 awsiface.Ec2Client + Time Time } func NewCage(input *Input) Cage { + if input.Time == nil { + input.Time = &timeImpl{} + } return &cage{ - env: input.Env, - ecs: input.ECS, - alb: input.ALB, - ec2: input.EC2, + env: input.Env, + ecs: input.ECS, + alb: input.ALB, + ec2: input.EC2, + time: input.Time, } } diff --git a/cli/cage/commands/command.go b/cli/cage/commands/command.go index b9b171f..5b3ab15 100644 --- a/cli/cage/commands/command.go +++ b/cli/cage/commands/command.go @@ -2,19 +2,32 @@ package commands import ( "context" + "io" + + "github.com/loilo-inc/canarycage/cli/cage/prompt" "github.com/urfave/cli/v2" ) type CageCommands interface { - Up() *cli.Command - RollOut() *cli.Command - Run() *cli.Command + Commands() []*cli.Command } type cageCommands struct { - ctx context.Context + ctx context.Context + prompt *prompt.Prompter +} + +func NewCageCommands(ctx context.Context, stdin io.Reader) CageCommands { + return &cageCommands{ctx: ctx, + prompt: prompt.NewPrompter(stdin), + } } -func NewCageCommands(ctx context.Context) CageCommands { - return &cageCommands{ctx: ctx} +func (c *cageCommands) Commands() []*cli.Command { + return []*cli.Command{ + c.Up(), + c.RollOut(), + c.Run(), + c.Recreate(), + } } diff --git a/cli/cage/commands/flags.go b/cli/cage/commands/flags.go index ff784e8..9b95028 100644 --- a/cli/cage/commands/flags.go +++ b/cli/cage/commands/flags.go @@ -1,11 +1,10 @@ package commands import ( - "context" + "os" "github.com/apex/log" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/loilo-inc/canarycage" + cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) @@ -56,21 +55,12 @@ func (c *cageCommands) aggregateEnvars( ctx *cli.Context, envars *cage.Envars, ) { - cfg, err := config.LoadDefaultConfig(context.Background()) - if err != nil { - log.Fatalf(err.Error()) - } - if envars.Region != "" { log.Infof("๐Ÿ—บ region was set: %s", envars.Region) - } - - if cfg.Region != "" { - log.Infof("๐Ÿ—บ region was loaded from default config: %s", cfg.Region) } else { log.Fatalf("๐Ÿ™„ region must specified by --region flag or aws session") } - + envars.CI = os.Getenv("CI") == "true" if ctx.NArg() > 0 { dir := ctx.Args().Get(0) td, svc, err := cage.LoadDefinitionsFromFiles(dir) diff --git a/cli/cage/commands/recreate.go b/cli/cage/commands/recreate.go new file mode 100644 index 0000000..ee4414a --- /dev/null +++ b/cli/cage/commands/recreate.go @@ -0,0 +1,55 @@ +package commands + +import ( + "context" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ecs" + elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" + cage "github.com/loilo-inc/canarycage" + "github.com/urfave/cli/v2" +) + +func (c *cageCommands) Recreate() *cli.Command { + envars := cage.Envars{} + return &cli.Command{ + Name: "recreate", + Usage: "recreate ECS service with specified service/task definition", + Description: "recreate ECS service with specified service/task definition", + ArgsUsage: "[directory path of service.json and task-definition.json (default=.)]", + Flags: []cli.Flag{ + RegionFlag(&envars.Region), + ClusterFlag(&envars.Cluster), + ServiceFlag(&envars.Service), + TaskDefinitionArnFlag(&envars.TaskDefinitionArn), + CanaryTaskIdleDurationFlag(&envars.CanaryTaskIdleDuration), + }, + Action: func(ctx *cli.Context) error { + c.aggregateEnvars(ctx, &envars) + + if err := c.prompt.ConfirmService(&envars); err != nil { + return err + } + var cfg aws.Config + if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { + return err + } else { + cfg = o + } + cagecli := cage.NewCage(&cage.Input{ + Env: &envars, + ECS: ecs.NewFromConfig(cfg), + EC2: ec2.NewFromConfig(cfg), + ALB: elbv2.NewFromConfig(cfg), + }) + _, err := cagecli.Recreate(context.Background()) + if err != nil { + log.Error(err.Error()) + } + return err + }, + } +} diff --git a/cli/cage/commands/rollout.go b/cli/cage/commands/rollout.go index d4ba5c8..7bae253 100644 --- a/cli/cage/commands/rollout.go +++ b/cli/cage/commands/rollout.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ecs" elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/loilo-inc/canarycage" + cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) @@ -35,6 +35,9 @@ func (c *cageCommands) RollOut() *cli.Command { }, Action: func(ctx *cli.Context) error { c.aggregateEnvars(ctx, &envars) + if err := c.prompt.ConfirmService(&envars); err != nil { + return err + } var cfg aws.Config if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { return err diff --git a/cli/cage/commands/run.go b/cli/cage/commands/run.go index c918f25..2e3769f 100644 --- a/cli/cage/commands/run.go +++ b/cli/cage/commands/run.go @@ -28,6 +28,9 @@ func (c *cageCommands) Run() *cli.Command { }, Action: func(ctx *cli.Context) error { c.aggregateEnvars(ctx, &envars) + if err := c.prompt.ConfirmTask(&envars); err != nil { + return err + } var cfg aws.Config if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { return err diff --git a/cli/cage/commands/up.go b/cli/cage/commands/up.go index ca9ad98..1f5b251 100644 --- a/cli/cage/commands/up.go +++ b/cli/cage/commands/up.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ecs" elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/loilo-inc/canarycage" + cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) @@ -29,6 +29,9 @@ func (c *cageCommands) Up() *cli.Command { }, Action: func(ctx *cli.Context) error { c.aggregateEnvars(ctx, &envars) + if err := c.prompt.ConfirmService(&envars); err != nil { + return err + } var cfg aws.Config if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { return err diff --git a/cli/cage/main.go b/cli/cage/main.go index 5b1a4f0..66c5627 100644 --- a/cli/cage/main.go +++ b/cli/cage/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "log" "os" "github.com/loilo-inc/canarycage/cli/cage/commands" @@ -11,18 +12,13 @@ import ( func main() { app := cli.NewApp() app.Name = "canarycage" - app.Version = "3.6.0" + app.Version = "3.7.0" app.Description = "A gradual roll-out deployment tool for AWS ECS" ctx := context.Background() - cmds := commands.NewCageCommands(ctx) - app.Commands = cli.Commands{ - cmds.RollOut(), - cmds.Up(), - cmds.Run(), - } + cmds := commands.NewCageCommands(ctx, os.Stdin) + app.Commands = cmds.Commands() err := app.Run(os.Args) if err != nil { - os.Exit(1) + log.Fatal(err) } - os.Exit(0) } diff --git a/cli/cage/prompt/prompt.go b/cli/cage/prompt/prompt.go new file mode 100644 index 0000000..e970efc --- /dev/null +++ b/cli/cage/prompt/prompt.go @@ -0,0 +1,75 @@ +package prompt + +import ( + "bufio" + "fmt" + "io" + "os" + + cage "github.com/loilo-inc/canarycage" + "golang.org/x/xerrors" +) + +type Prompter struct { + Reader *bufio.Reader +} + +func NewPrompter(stdin io.Reader) *Prompter { + return &Prompter{Reader: bufio.NewReader(stdin)} +} + +func (s *Prompter) Confirm( + name string, + value string, +) error { + fmt.Fprintf(os.Stderr, "please confirm [%s]: ", name) + if text, err := s.Reader.ReadString('\n'); err != nil { + return xerrors.Errorf("failed to read from stdin: %w", err) + } else if text[:len(text)-1] != value { + return xerrors.Errorf("%s is not matched. expected: %s", name, value) + } + return nil +} + +func (s *Prompter) ConfirmTask( + envars *cage.Envars, +) error { + return s.confirmStackChange(envars, false) +} + +func (s *Prompter) ConfirmService( + envars *cage.Envars, +) error { + return s.confirmStackChange(envars, true) +} + +func (s *Prompter) confirmStackChange( + envars *cage.Envars, + service bool, +) error { + // Skip confirmation if running in CI + if envars.CI { + return nil + } + if err := s.Confirm("region", envars.Region); err != nil { + return err + } + if err := s.Confirm("cluster", envars.Cluster); err != nil { + return err + } + if service { + if err := s.Confirm("service", envars.Service); err != nil { + return err + } + } + fmt.Fprintf(os.Stderr, "confirm changes:\n") + fmt.Fprintf(os.Stderr, "[region]: %s\n", envars.Region) + fmt.Fprintf(os.Stderr, "[cluster]: %s\n", envars.Cluster) + if service { + fmt.Fprintf(os.Stderr, "[service]: %s\n", envars.Service) + } + if err := s.Confirm("yes", "yes"); err != nil { + return err + } + return nil +} diff --git a/cli/cage/prompt/prompt_test.go b/cli/cage/prompt/prompt_test.go new file mode 100644 index 0000000..1d495e0 --- /dev/null +++ b/cli/cage/prompt/prompt_test.go @@ -0,0 +1,66 @@ +package prompt_test + +import ( + "strings" + "testing" + + cage "github.com/loilo-inc/canarycage" + "github.com/loilo-inc/canarycage/cli/cage/prompt" + "github.com/stretchr/testify/assert" +) + +func TestPrompter(t *testing.T) { + t.Run("Confirm", func(t *testing.T) { + t.Run("yes", func(t *testing.T) { + reader := strings.NewReader("yes\n") + prompter := prompt.NewPrompter(reader) + err := prompter.Confirm("test", "yes") + assert.NoError(t, err) + }) + t.Run("no", func(t *testing.T) { + reader := strings.NewReader("no\n") + prompter := prompt.NewPrompter(reader) + err := prompter.Confirm("test", "yes") + assert.Error(t, err) + }) + }) + envars := &cage.Envars{ + Region: "ap-northeast-1", + Cluster: "test-cluster", + Service: "test-service", + } + t.Run("ConfirmTask", func(t *testing.T) { + t.Run("ok", func(t *testing.T) { + reader := strings.NewReader("ap-northeast-1\ntest-cluster\nyes\n") + prompter := prompt.NewPrompter(reader) + err := prompter.ConfirmTask(envars) + assert.NoError(t, err) + }) + }) + t.Run("ConfirmService", func(t *testing.T) { + t.Run("yes", func(t *testing.T) { + reader := strings.NewReader("ap-northeast-1\ntest-cluster\ntest-service\nyes\n") + prompter := prompt.NewPrompter(reader) + err := prompter.ConfirmService(envars) + assert.NoError(t, err) + }) + t.Run("region mismatch", func(t *testing.T) { + reader := strings.NewReader("us-west-2\ntest-cluster\nyes\n") + prompter := prompt.NewPrompter(reader) + err := prompter.ConfirmTask(envars) + assert.Error(t, err) + }) + t.Run("cluster mismatch", func(t *testing.T) { + reader := strings.NewReader("ap-northeast-1\ndifferent-cluster\nyes\n") + prompter := prompt.NewPrompter(reader) + err := prompter.ConfirmTask(envars) + assert.Error(t, err) + }) + t.Run("service mismatch", func(t *testing.T) { + reader := strings.NewReader("ap-northeast-1\ntest-cluster\ndifferent-service\nyes\n") + prompter := prompt.NewPrompter(reader) + err := prompter.ConfirmTask(envars) + assert.Error(t, err) + }) + }) +} diff --git a/env.go b/env.go index 05e8ec1..9f14cd4 100644 --- a/env.go +++ b/env.go @@ -8,10 +8,12 @@ import ( "github.com/apex/log" "github.com/aws/aws-sdk-go-v2/service/ecs" + "golang.org/x/xerrors" ) type Envars struct { _ struct{} `type:"struct"` + CI bool `json:"ci" type:"bool"` Region string `json:"region" type:"string"` Cluster string `json:"cluster" type:"string" required:"true"` Service string `json:"service" type:"string" required:"true"` @@ -39,12 +41,12 @@ func EnsureEnvars( ) error { // required if dest.Cluster == "" { - return NewErrorf("--cluster [%s] is required", ClusterKey) + return xerrors.Errorf("--cluster [%s] is required", ClusterKey) } else if dest.Service == "" { - return NewErrorf("--service [%s] is required", ServiceKey) + return xerrors.Errorf("--service [%s] is required", ServiceKey) } if dest.TaskDefinitionArn == "" && dest.TaskDefinitionInput == nil { - return NewErrorf("--nextTaskDefinitionArn or deploy context must be provided") + return xerrors.Errorf("--nextTaskDefinitionArn or deploy context must be provided") } if dest.Region == "" { log.Fatalf("region must be specified. set --region flag or see also https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html") diff --git a/env_test.go b/env_test.go index d0c9169..83e8799 100644 --- a/env_test.go +++ b/env_test.go @@ -36,7 +36,7 @@ func TestEnsureEnvars(t *testing.T) { Cluster: "cluster", Service: "next", } - if err := EnsureEnvars(e); err == nil { + if err := EnsureEnvars(e); err != nil { t.Fatalf(err.Error()) } }) diff --git a/fixtures/service.json b/fixtures/service.json index 95a4da5..e4a1e54 100644 --- a/fixtures/service.json +++ b/fixtures/service.json @@ -1,55 +1,30 @@ { - "cluster": "cluster", - "serviceName": "service", - "taskDefinition": "hoge", - "loadBalancers": [ - { - "targetGroupArn": "aaaa/targetgroup/aaa/bbb", - "loadBalancerName": "lb", - "containerName": "container", - "containerPort": 8000 - } - ], - "serviceRegistries": [ - { - "registryArn": "", - "port": 0, - "containerName": "", - "containerPort": 0 - } - ], - "desiredCount": 0, - "clientToken": "", - "launchType": "FARGATE", - "platformVersion": "", - "role": "", - "deploymentConfiguration": { - "maximumPercent": 0, - "minimumHealthyPercent": 0 - }, - "placementConstraints": [ - { - "type": "distinctInstance", - "expression": "" - } - ], - "placementStrategy": [ - { - "type": "binpack", - "field": "" - } - ], - "networkConfiguration": { - "awsvpcConfiguration": { - "subnets": [ - "" - ], - "securityGroups": [ - "" - ], - "assignPublicIp": "ENABLED" - } - }, - "healthCheckGracePeriodSeconds": 0, - "schedulingStrategy": "REPLICA" + "cluster": "cluster", + "serviceName": "service", + "taskDefinition": "test-task:1", + "loadBalancers": [ + { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/tg/1234567890123456", + "loadBalancerName": "lb", + "containerName": "container", + "containerPort": 8000 + } + ], + "desiredCount": 1, + "launchType": "FARGATE", + "platformVersion": "1.4.0", + "role": "ecsServiceRole", + "deploymentConfiguration": { + "maximumPercent": 200, + "minimumHealthyPercent": 100 + }, + "networkConfiguration": { + "awsvpcConfiguration": { + "subnets": [""], + "securityGroups": [""], + "assignPublicIp": "ENABLED" + } + }, + "healthCheckGracePeriodSeconds": 0, + "schedulingStrategy": "REPLICA" } diff --git a/fixtures/task-definition.json b/fixtures/task-definition.json index a1267f7..23ecc4e 100644 --- a/fixtures/task-definition.json +++ b/fixtures/task-definition.json @@ -1,260 +1,202 @@ { - "family": "", - "taskRoleArn": "", - "executionRoleArn": "", - "networkMode": "awsvpc", - "containerDefinitions": [ + "family": "test-task", + "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole", + "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole", + "networkMode": "awsvpc", + "containerDefinitions": [ + { + "name": "container", + "image": "", + "repositoryCredentials": { + "credentialsParameter": "" + }, + "cpu": 0, + "memory": 0, + "memoryReservation": 0, + "links": [""], + "portMappings": [ { - "name": "container", - "image": "", - "repositoryCredentials": { - "credentialsParameter": "" - }, - "cpu": 0, - "memory": 0, - "memoryReservation": 0, - "links": [ - "" - ], - "portMappings": [ - { - "containerPort": 8000, - "hostPort": 80 - } - ], - "essential": true, - "entryPoint": [ - "" - ], - "command": [ - "" - ], - "environment": [ - { - "name": "", - "value": "" - } - ], - "mountPoints": [ - { - "sourceVolume": "", - "containerPath": "", - "readOnly": true - } - ], - "volumesFrom": [ - { - "sourceContainer": "", - "readOnly": true - } - ], - "linuxParameters": { - "capabilities": { - "add": [ - "" - ], - "drop": [ - "" - ] - }, - "devices": [ - { - "hostPath": "", - "containerPath": "", - "permissions": [ - "mknod" - ] - } - ], - "initProcessEnabled": true, - "sharedMemorySize": 0, - "tmpfs": [ - { - "containerPath": "", - "size": 0, - "mountOptions": [ - "" - ] - } - ] - }, - "hostname": "", - "user": "", - "workingDirectory": "", - "disableNetworking": true, - "privileged": true, - "readonlyRootFilesystem": true, - "dnsServers": [ - "" - ], - "dnsSearchDomains": [ - "" - ], - "extraHosts": [ - { - "hostname": "", - "ipAddress": "" - } - ], - "dockerSecurityOptions": [ - "" - ], - "dockerLabels": { - "KeyName": "" - }, - "ulimits": [ - { - "name": "core", - "softLimit": 0, - "hardLimit": 0 - } - ], - "logConfiguration": { - "logDriver": "gelf", - "options": { - "KeyName": "" - } - }, - "healthCheck": { - "command": [ - "" - ], - "interval": 0, - "timeout": 0, - "retries": 0, - "startPeriod": 0 - } + "containerPort": 8000, + "hostPort": 80 + } + ], + "essential": true, + "entryPoint": [""], + "command": [""], + "environment": [ + { + "name": "", + "value": "" + } + ], + "mountPoints": [ + { + "sourceVolume": "", + "containerPath": "", + "readOnly": true + } + ], + "volumesFrom": [ + { + "sourceContainer": "", + "readOnly": true + } + ], + "linuxParameters": { + "capabilities": { + "add": [""], + "drop": [""] }, + "devices": [ + { + "hostPath": "", + "containerPath": "", + "permissions": ["mknod"] + } + ], + "initProcessEnabled": true, + "sharedMemorySize": 0, + "tmpfs": [ + { + "containerPath": "", + "size": 0, + "mountOptions": [""] + } + ] + }, + "hostname": "", + "user": "", + "workingDirectory": "", + "disableNetworking": true, + "privileged": true, + "readonlyRootFilesystem": true, + "dnsServers": [""], + "dnsSearchDomains": [""], + "extraHosts": [ + { + "hostname": "", + "ipAddress": "" + } + ], + "dockerSecurityOptions": [""], + "dockerLabels": { + "KeyName": "" + }, + "ulimits": [ + { + "name": "core", + "softLimit": 0, + "hardLimit": 0 + } + ], + "logConfiguration": { + "logDriver": "gelf", + "options": { + "KeyName": "" + } + }, + "healthCheck": { + "command": [""], + "interval": 0, + "timeout": 0, + "retries": 0, + "startPeriod": 0 + } + }, + { + "name": "containerWithoutHealthCheck", + "image": "", + "repositoryCredentials": { + "credentialsParameter": "" + }, + "cpu": 0, + "memory": 0, + "memoryReservation": 0, + "links": [""], + "portMappings": [ + { + "containerPort": 8000, + "hostPort": 80 + } + ], + "essential": true, + "entryPoint": [""], + "command": [""], + "environment": [ + { + "name": "", + "value": "" + } + ], + "mountPoints": [ { - "name": "containerWithoutHealthCheck", - "image": "", - "repositoryCredentials": { - "credentialsParameter": "" - }, - "cpu": 0, - "memory": 0, - "memoryReservation": 0, - "links": [ - "" - ], - "portMappings": [ - { - "containerPort": 8000, - "hostPort": 80 - } - ], - "essential": true, - "entryPoint": [ - "" - ], - "command": [ - "" - ], - "environment": [ - { - "name": "", - "value": "" - } - ], - "mountPoints": [ - { - "sourceVolume": "", - "containerPath": "", - "readOnly": true - } - ], - "volumesFrom": [ - { - "sourceContainer": "", - "readOnly": true - } - ], - "linuxParameters": { - "capabilities": { - "add": [ - "" - ], - "drop": [ - "" - ] - }, - "devices": [ - { - "hostPath": "", - "containerPath": "", - "permissions": [ - "mknod" - ] - } - ], - "initProcessEnabled": true, - "sharedMemorySize": 0, - "tmpfs": [ - { - "containerPath": "", - "size": 0, - "mountOptions": [ - "" - ] - } - ] - }, - "hostname": "", - "user": "", - "workingDirectory": "", - "disableNetworking": true, - "privileged": true, - "readonlyRootFilesystem": true, - "dnsServers": [ - "" - ], - "dnsSearchDomains": [ - "" - ], - "extraHosts": [ - { - "hostname": "", - "ipAddress": "" - } - ], - "dockerSecurityOptions": [ - "" - ], - "dockerLabels": { - "KeyName": "" - }, - "ulimits": [ - { - "name": "core", - "softLimit": 0, - "hardLimit": 0 - } - ], - "logConfiguration": { - "logDriver": "gelf", - "options": { - "KeyName": "" - } - } + "sourceVolume": "", + "containerPath": "", + "readOnly": true } - ], - "volumes": [ + ], + "volumesFrom": [ { - "name": "", - "host": { - "sourcePath": "" - } + "sourceContainer": "", + "readOnly": true } - ], - "placementConstraints": [ + ], + "linuxParameters": { + "capabilities": { + "add": [""], + "drop": [""] + }, + "devices": [ + { + "hostPath": "", + "containerPath": "", + "permissions": ["mknod"] + } + ], + "initProcessEnabled": true, + "sharedMemorySize": 0, + "tmpfs": [ + { + "containerPath": "", + "size": 0, + "mountOptions": [""] + } + ] + }, + "hostname": "", + "user": "", + "workingDirectory": "", + "disableNetworking": true, + "privileged": true, + "readonlyRootFilesystem": true, + "dnsServers": [""], + "dnsSearchDomains": [""], + "extraHosts": [ { - "type": "memberOf", - "expression": "" + "hostname": "", + "ipAddress": "" + } + ], + "dockerSecurityOptions": [""], + "dockerLabels": { + "KeyName": "" + }, + "ulimits": [ + { + "name": "core", + "softLimit": 0, + "hardLimit": 0 + } + ], + "logConfiguration": { + "logDriver": "gelf", + "options": { + "KeyName": "" } - ], - "requiresCompatibilities": [ - "FARGATE" - ], - "cpu": "", - "memory": "" + } + } + ], + "requiresCompatibilities": ["FARGATE"], + "cpu": "256", + "memory": "512" } diff --git a/go.mod b/go.mod index e74385b..f230330 100644 --- a/go.mod +++ b/go.mod @@ -4,35 +4,37 @@ go 1.22 require ( github.com/apex/log v1.9.0 - github.com/aws/aws-sdk-go-v2 v1.26.0 - github.com/aws/aws-sdk-go-v2/config v1.27.9 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.154.0 - github.com/aws/aws-sdk-go-v2/service/ecs v1.41.5 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.4 + github.com/aws/aws-sdk-go-v2 v1.27.0 + github.com/aws/aws-sdk-go-v2/config v1.27.16 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.161.4 + github.com/aws/aws-sdk-go-v2/service/ecs v1.41.11 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.31.1 github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 - github.com/stretchr/testify v1.8.2 - github.com/urfave/cli/v2 v2.27.1 + github.com/stretchr/testify v1.9.0 + github.com/urfave/cli/v2 v2.27.2 ) require ( - github.com/aws/aws-sdk-go-v2/credentials v1.17.9 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4 // indirect + github.com/aws/aws-sdk-go v1.53.10 + github.com/aws/aws-sdk-go-v2/credentials v1.17.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.20.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 // indirect - github.com/aws/smithy-go v1.20.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.10 // indirect + github.com/aws/smithy-go v1.20.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7170df6..3cc274a 100644 --- a/go.sum +++ b/go.sum @@ -4,38 +4,40 @@ github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDw github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v1.26.0 h1:/Ce4OCiM3EkpW7Y+xUnfAFpchU78K7/Ug01sZni9PgA= -github.com/aws/aws-sdk-go-v2 v1.26.0/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I= -github.com/aws/aws-sdk-go-v2/config v1.27.9 h1:gRx/NwpNEFSk+yQlgmk1bmxxvQ5TyJ76CWXs9XScTqg= -github.com/aws/aws-sdk-go-v2/config v1.27.9/go.mod h1:dK1FQfpwpql83kbD873E9vz4FyAxuJtR22wzoXn3qq0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.9 h1:N8s0/7yW+h8qR8WaRlPQeJ6czVMNQVNtNdUqf6cItao= -github.com/aws/aws-sdk-go-v2/credentials v1.17.9/go.mod h1:446YhIdmSV0Jf/SLafGZalQo+xr2iw7/fzXGDPTU1yQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 h1:af5YzcLf80tv4Em4jWVD75lpnOHSBkPUZxZfGkrI3HI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0/go.mod h1:nQ3how7DMnFMWiU1SpECohgC82fpn4cKZ875NDMmwtA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4 h1:0ScVK/4qZ8CIW0k8jOeFVsyS/sAiXpYxRBLolMkuLQM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4/go.mod h1:84KyjNZdHC6QZW08nfHI6yZgPd+qRgaWcYsyLUo3QY8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4 h1:sHmMWWX5E7guWEFQ9SVo6A3S4xpPrWnd77a6y4WM6PU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4/go.mod h1:WjpDrhWisWOIoS9n3nk67A3Ll1vfULJ9Kq6h29HTD48= +github.com/aws/aws-sdk-go v1.53.10 h1:3enP5l5WtezT9Ql+XZqs56JBf5YUd/FEzTCg///OIGY= +github.com/aws/aws-sdk-go v1.53.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo= +github.com/aws/aws-sdk-go-v2 v1.27.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/config v1.27.16 h1:knpCuH7laFVGYTNd99Ns5t+8PuRjDn4HnnZK48csipM= +github.com/aws/aws-sdk-go-v2/config v1.27.16/go.mod h1:vutqgRhDUktwSge3hrC3nkuirzkJ4E/mLj5GvI0BQas= +github.com/aws/aws-sdk-go-v2/credentials v1.17.16 h1:7d2QxY83uYl0l58ceyiSpxg9bSbStqBC6BeEeHEchwo= +github.com/aws/aws-sdk-go-v2/credentials v1.17.16/go.mod h1:Ae6li/6Yc6eMzysRL2BXlPYvnrLLBg3D11/AmOjw50k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 h1:dQLK4TjtnlRGb0czOht2CevZ5l6RSyRWAnKeGd7VAFE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3/go.mod h1:TL79f2P6+8Q7dTsILpiVST+AL9lkF6PPGI167Ny0Cjw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 h1:lf/8VTF2cM+N4SLzaYJERKEWAXq8MOMpZfU6wEPWsPk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7/go.mod h1:4SjkU7QiqK2M9oozyMzfZ/23LmUY+h3oFqhdeP5OMiI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 h1:4OYVp0705xu8yjdyoWix0r9wPIRXnIzzOoUpQVHIJ/g= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7/go.mod h1:vd7ESTEvI76T2Na050gODNmNU7+OyKrIKroYTu4ABiI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.154.0 h1:+OJ9EhHaqjtA4YTTbxxLxMffrWuGWh0qMaBmGJTLSSg= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.154.0/go.mod h1:TeZ9dVQzGaLG+SBIgdLIDbJ6WmfFvksLeG3EHGnNfZM= -github.com/aws/aws-sdk-go-v2/service/ecs v1.41.5 h1:KUB2aLoYzKGZIh2mybcwBqBy7FjwElilVFfoxp8OINE= -github.com/aws/aws-sdk-go-v2/service/ecs v1.41.5/go.mod h1:7b5ZXNyT7SjZhy+MOuXwL2XtsrFDl1bOL4Mqrgr5c3k= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.4 h1:Lq2q/AWzFv5jHVoGJ2Hz1PkxwHYNdGzAB3lbw2g7IEU= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.4/go.mod h1:SNhjWOsnsHSveL4fDQL0sDiAIMVnKrvJTp9Z/MNspx0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6 h1:b+E7zIUHMmcB4Dckjpkapoy47W6C9QBv/zoUP+Hn8Kc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6/go.mod h1:S2fNV0rxrP78NhPbCZeQgY8H9jdDMeGtwcfZIRxzBqU= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.3 h1:mnbuWHOcM70/OFUlZZ5rcdfA8PflGXXiefU/O+1S3+8= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.3/go.mod h1:5HFu51Elk+4oRBZVxmHrSds5jFXmFj8C3w7DVF2gnrs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 h1:uLq0BKatTmDzWa/Nu4WO0M1AaQDaPpwTKAeByEc6WFM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3/go.mod h1:b+qdhjnxj8GSR6t5YfphOffeoQSQ1KmpoVVuBn+PWxs= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 h1:J/PpTf/hllOjx8Xu9DMflff3FajfLxqM5+tepvVXmxg= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.5/go.mod h1:0ih0Z83YDH/QeQ6Ori2yGE2XvWYv/Xm+cZc01LC6oK0= -github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= -github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.161.4 h1:JBcPadBAnSwqUZQ1o2XOkTXy7GBcidpupkXZf02parw= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.161.4/go.mod h1:iJ2sQeUTkjNp3nL7kE/Bav0xXYhtiRCRP5ZXk4jFhCQ= +github.com/aws/aws-sdk-go-v2/service/ecs v1.41.11 h1:/27vG0bgOsJmMqSbjCuF4UdEWZyRqPF9gQ4MYGiIEYc= +github.com/aws/aws-sdk-go-v2/service/ecs v1.41.11/go.mod h1:ixRB9qcKi35waDtPb6uw31Eb7Df+MOcjtpWxxPO5XvI= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.31.1 h1:ZdIaRvkbFBS4mrH4slH8ypbW8XuFJOey3nhdYfPCsC8= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.31.1/go.mod h1:8OpnCueyLye/uyNWHz/AW+1uxcXoZ1U/ss4Ql3gogRM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 h1:Wx0rlZoEJR7JwlSZcHnEa7CNjrSIyVxMFWGAaXy4fJY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9/go.mod h1:aVMHdE0aHO3v+f/iw01fmXV/5DbfQ3Bi9nN7nd9bE9Y= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.9 h1:aD7AGQhvPuAxlSUfo0CWU7s6FpkbyykMhGYMvlqTjVs= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.9/go.mod h1:c1qtZUWtygI6ZdvKppzCSXsDOq5I4luJPZ0Ud3juFCA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3 h1:Pav5q3cA260Zqez42T9UhIlsd9QeypszRPwC9LdSSsQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3/go.mod h1:9lmoVDVLz/yUZwLaQ676TK02fhCu4+PgRSmMaKR1ozk= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.10 h1:69tpbPED7jKPyzMcrwSvhWcJ9bPnZsZs18NT40JwM0g= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.10/go.mod h1:0Aqn1MnEuitqfsCNyKsdKLhDUOr4txD/g19EfiUqgws= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -85,14 +87,10 @@ github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUr github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= @@ -100,10 +98,10 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -133,6 +131,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/recreate.go b/recreate.go new file mode 100644 index 0000000..4cb5f07 --- /dev/null +++ b/recreate.go @@ -0,0 +1,149 @@ +package cage + +import ( + "context" + "fmt" + "time" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "github.com/aws/aws-sdk-go/aws" + "golang.org/x/xerrors" +) + +type RecreateResult struct { + Service *ecstypes.Service + TaskDefinition *ecstypes.TaskDefinition +} + +func (c *cage) Recreate(ctx context.Context) (*RecreateResult, error) { + // Check if the service already exists + log.Infof("checking existence of service '%s'", c.env.Service) + var oldService *ecstypes.Service + var transitService *ecstypes.Service + var newService *ecstypes.Service + if o, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.env.Cluster, + Services: []string{c.env.Service}, + }); err != nil { + return nil, xerrors.Errorf("couldn't describe service: %w", err) + } else if len(o.Services) == 0 { + return nil, fmt.Errorf("service '%s' does not exist. Use 'cage up' instead", c.env.Service) + } else { + oldService = &o.Services[0] + if *oldService.Status == "INACTIVE" { + return nil, fmt.Errorf("service '%s' is already INACTIVE. Use 'cage up' instead", c.env.Service) + } + } + var err error + // Create a new task definition + td, err := c.CreateNextTaskDefinition(ctx) + if err != nil { + return nil, err + } + transitServiceName := fmt.Sprintf("%s-%d", *oldService.ServiceName, c.time.Now().Unix()) + newServiceInput := *c.env.ServiceDefinitionInput + curDesiredCount := oldService.DesiredCount + c.env.ServiceDefinitionInput.TaskDefinition = td.TaskDefinitionArn + transitServiceDifinitonInput := *c.env.ServiceDefinitionInput + transitServiceDifinitonInput.ServiceName = &transitServiceName + transitServiceDifinitonInput.DesiredCount = aws.Int32(1) + // Create a transit service + if transitService, err = c.createService(ctx, &transitServiceDifinitonInput); err != nil { + return nil, err + } + // Update transit service to same task count as previous service + if err = c.updateServiceTaskCount(ctx, *transitService.ServiceName, oldService.DesiredCount); err != nil { + return nil, err + } + // Update old service to 0 tasks + if err = c.updateServiceTaskCount(ctx, *oldService.ServiceName, 0); err != nil { + return nil, err + } + // Delete old service + if err = c.deleteService(ctx, *oldService.ServiceName); err != nil { + return nil, err + } + oldService = nil + // Create a new service + if newService, err = c.createService(ctx, &newServiceInput); err != nil { + return nil, err + } + // Update new service to same task count as transit service + if err = c.updateServiceTaskCount(ctx, *newService.ServiceName, curDesiredCount); err != nil { + return nil, err + } + // Update transit service to 0 tasks + if err = c.updateServiceTaskCount(ctx, *transitService.ServiceName, 0); err != nil { + return nil, err + } + // Delete transit service + if err = c.deleteService(ctx, *transitService.ServiceName); err != nil { + return nil, err + } + transitService = nil + return &RecreateResult{TaskDefinition: td, Service: newService}, nil +} + +func (c *cage) createService(ctx context.Context, serviceDefinitionInput *ecs.CreateServiceInput) (*ecstypes.Service, error) { + log.Infof("creating service '%s' with task-definition '%s'...", *serviceDefinitionInput.ServiceName, *serviceDefinitionInput.TaskDefinition) + o, err := c.ecs.CreateService(ctx, serviceDefinitionInput) + if err != nil { + return nil, fmt.Errorf("failed to create service '%s': %s", *serviceDefinitionInput.ServiceName, err.Error()) + } + log.Infof("waiting for service '%s' to be STABLE", *serviceDefinitionInput.ServiceName) + if err := ecs.NewServicesStableWaiter(c.ecs).Wait(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.env.Cluster, + Services: []string{*serviceDefinitionInput.ServiceName}, + }, WaitDuration); err != nil { + return nil, fmt.Errorf("failed to wait for service '%s' to be STABLE: %s", *serviceDefinitionInput.ServiceName, err.Error()) + } + return o.Service, nil +} + +func (c *cage) updateServiceTaskCount(ctx context.Context, service string, count int32) error { + log.Infof("updating service '%s' desired count to %d...", service, count) + if _, err := c.ecs.UpdateService(ctx, &ecs.UpdateServiceInput{ + Cluster: &c.env.Cluster, + Service: &service, + DesiredCount: &count, + }); err != nil { + return fmt.Errorf("failed to update service '%s': %w", service, err) + } + log.Infof("waiting for service '%s' to be STABLE", service) + if err := ecs.NewServicesStableWaiter(c.ecs).Wait(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.env.Cluster, + Services: []string{service}, + }, WaitDuration); err != nil { + return fmt.Errorf("failed to wait for service '%s' to be STABLE: %v", service, err) + } + return nil +} + +func (c *cage) deleteService(ctx context.Context, service string) error { + log.Infof("deleting service '%s'...", service) + if _, err := c.ecs.DeleteService(ctx, &ecs.DeleteServiceInput{ + Cluster: &c.env.Cluster, + Service: &service, + }); err != nil { + return fmt.Errorf("failed to delete service '%s': %w", service, err) + } + var retryCount int = 0 + for retryCount < 10 { + <-c.time.NewTimer(15 * time.Second).C + log.Infof("waiting for service '%s' to be INACTIVE", service) + if o, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.env.Cluster, + Services: []string{service}, + }); err != nil { + return fmt.Errorf("failed to describe service '%s': %w", service, err) + } else if len(o.Services) == 0 { + break + } else if *o.Services[0].Status == "INACTIVE" { + break + } + retryCount++ + } + return nil +} diff --git a/recreate_test.go b/recreate_test.go new file mode 100644 index 0000000..c3e9419 --- /dev/null +++ b/recreate_test.go @@ -0,0 +1,35 @@ +package cage_test + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" + "github.com/loilo-inc/canarycage/test" + "github.com/stretchr/testify/assert" +) + +func TestRecreate(t *testing.T) { + t.Run("basic", func(t *testing.T) { + env := test.DefaultEnvars() + ctrl := gomock.NewController(t) + ctx := context.TODO() + mocker, ecsMock, _, _ := test.Setup(ctrl, env, 1, "FARGATE") + mocker.CreateService(ctx, env.ServiceDefinitionInput) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + ALB: nil, + EC2: nil, + Time: test.NewFakeTime(), + }) + result, err := cagecli.Recreate(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, result.Service) + assert.NotNil(t, result.TaskDefinition) + assert.Equal(t, len(mocker.Services), 1) + assert.Equal(t, len(mocker.TaskDefinitions.List()), 2) + assert.Equal(t, *mocker.Services["service"].ServiceName, *result.Service.ServiceName) + }) +} diff --git a/rollout.go b/rollout.go index b9e74db..67b5ffa 100644 --- a/rollout.go +++ b/rollout.go @@ -25,17 +25,17 @@ var WaitDuration = 15 * time.Minute func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { ret := &RollOutResult{ - StartTime: now(), + StartTime: c.time.Now(), ServiceIntact: true, } var aggregatedError error throw := func(err error) (*RollOutResult, error) { - ret.EndTime = now() + ret.EndTime = c.time.Now() aggregatedError = err return ret, err } defer func(result *RollOutResult) { - ret.EndTime = now() + ret.EndTime = c.time.Now() }(ret) var service ecstypes.Service if out, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ @@ -139,7 +139,7 @@ func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { return throw(err) } log.Infof("๐Ÿฅด service '%s' has become to be stable!", c.env.Service) - ret.EndTime = now() + ret.EndTime = c.time.Now() return ret, nil } @@ -155,7 +155,7 @@ func (c *cage) EnsureTaskHealthy( var initialized = false var recentState *elbv2types.TargetHealthStateEnum for { - <-newTimer(time.Duration(15) * time.Second).C + <-c.time.NewTimer(time.Duration(15) * time.Second).C if o, err := c.alb.DescribeTargetHealth(ctx, &elbv2.DescribeTargetHealthInput{ TargetGroupArn: tgArn, Targets: []elbv2types.TargetDescription{{ @@ -196,7 +196,6 @@ func (c *cage) EnsureTaskHealthy( func GetTargetIsHealthy(o *elbv2.DescribeTargetHealthOutput, targetId *string, targetPort *int32) *elbv2types.TargetHealthStateEnum { for _, desc := range o.TargetHealthDescriptions { - log.Debugf("%+v", desc) if *desc.Target.Id == *targetId && *desc.Target.Port == *targetPort { return &desc.TargetHealth.State } @@ -204,29 +203,6 @@ func GetTargetIsHealthy(o *elbv2.DescribeTargetHealthOutput, targetId *string, t return nil } -func (c *cage) CreateNextTaskDefinition(ctx context.Context) (*ecstypes.TaskDefinition, error) { - if c.env.TaskDefinitionArn != "" { - log.Infof("--taskDefinitionArn was set to '%s'. skip registering new task definition.", c.env.TaskDefinitionArn) - o, err := c.ecs.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{ - TaskDefinition: &c.env.TaskDefinitionArn, - }) - if err != nil { - log.Errorf( - "failed to describe next task definition '%s' due to: %s", - c.env.TaskDefinitionArn, err, - ) - return nil, err - } - return o.TaskDefinition, nil - } else { - if out, err := c.ecs.RegisterTaskDefinition(ctx, c.env.TaskDefinitionInput); err != nil { - return nil, err - } else { - return out.TaskDefinition, nil - } - } -} - func (c *cage) DescribeSubnet(ctx context.Context, subnetId *string) (ec2types.Subnet, error) { if o, err := c.ec2.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{ SubnetIds: []string{*subnetId}, @@ -315,7 +291,7 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes if duration < 10 { wt = duration } - <-time.NewTimer(time.Duration(wt) * time.Second).C + <-c.time.NewTimer(time.Duration(wt) * time.Second).C duration -= 10 } wait <- true @@ -414,7 +390,7 @@ func (c *cage) waitUntilContainersBecomeHealthy(ctx context.Context, taskArn str } for count := 0; count < 10; count++ { - <-newTimer(time.Duration(15) * time.Second).C + <-c.time.NewTimer(time.Duration(15) * time.Second).C log.Infof("canary task '%s' waits until %d container(s) become healthy", taskArn, len(containerHasHealthChecks)) if o, err := c.ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ Cluster: &c.env.Cluster, diff --git a/rollout_test.go b/rollout_test.go index 1417633..504c41f 100644 --- a/rollout_test.go +++ b/rollout_test.go @@ -1,9 +1,7 @@ -package cage +package cage_test import ( "context" - "encoding/json" - "io/ioutil" "regexp" "strings" "testing" @@ -15,94 +13,20 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" "github.com/loilo-inc/canarycage/mocks/mock_awsiface" "github.com/loilo-inc/canarycage/test" "github.com/stretchr/testify/assert" ) -func DefaultEnvars() *Envars { - d, _ := ioutil.ReadFile("fixtures/task-definition.json") - var taskDefinition ecs.RegisterTaskDefinitionInput - if err := json.Unmarshal(d, &taskDefinition); err != nil { - log.Fatalf(err.Error()) - } - return &Envars{ - Region: "us-west-2", - Cluster: "cage-test", - Service: "service", - ServiceDefinitionInput: ReadServiceDefinition("fixtures/service.json"), - TaskDefinitionInput: &taskDefinition, - } -} - -func ReadServiceDefinition(path string) *ecs.CreateServiceInput { - d, _ := ioutil.ReadFile(path) - var dest ecs.CreateServiceInput - if err := json.Unmarshal(d, &dest); err != nil { - log.Fatalf(err.Error()) - } - return &dest -} - -func Setup(ctrl *gomock.Controller, envars *Envars, currentTaskCount int, launchType string) ( - *test.MockContext, - *mock_awsiface.MockEcsClient, - *mock_awsiface.MockAlbClient, - *mock_awsiface.MockEc2Client, -) { - mocker := test.NewMockContext() - - ecsMock := mock_awsiface.NewMockEcsClient(ctrl) - ecsMock.EXPECT().CreateService(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.CreateService).AnyTimes() - ecsMock.EXPECT().UpdateService(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.UpdateService).AnyTimes() - ecsMock.EXPECT().DeleteService(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeleteService).AnyTimes() - ecsMock.EXPECT().StartTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.StartTask).AnyTimes() - ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTaskDefinition).AnyTimes() - ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeServices).AnyTimes() - ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks).AnyTimes() - ecsMock.EXPECT().ListTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.ListTasks).AnyTimes() - ecsMock.EXPECT().DescribeContainerInstances(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeContainerInstances).AnyTimes() - ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask).AnyTimes() - ecsMock.EXPECT().StopTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.StopTask).AnyTimes() - - albMock := mock_awsiface.NewMockAlbClient(ctrl) - albMock.EXPECT().DescribeTargetGroups(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetGroups).AnyTimes() - albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetHealth).AnyTimes() - albMock.EXPECT().DescribeTargetGroupAttributes(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetGroupAttibutes).AnyTimes() - albMock.EXPECT().RegisterTargets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTarget).AnyTimes() - albMock.EXPECT().DeregisterTargets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeregisterTarget).AnyTimes() - - ec2Mock := mock_awsiface.NewMockEc2Client(ctrl) - ec2Mock.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeSubnets).AnyTimes() - ec2Mock.EXPECT().DescribeInstances(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeInstances).AnyTimes() - td, _ := mocker.RegisterTaskDefinition(context.Background(), envars.TaskDefinitionInput) - a := &ecs.CreateServiceInput{ - ServiceName: &envars.Service, - LoadBalancers: envars.ServiceDefinitionInput.LoadBalancers, - TaskDefinition: td.TaskDefinition.TaskDefinitionArn, - DesiredCount: aws.Int32(int32(currentTaskCount)), - LaunchType: ecstypes.LaunchType(launchType), - } - svc, _ := mocker.CreateService(context.Background(), a) - if len(svc.Service.LoadBalancers) > 0 { - _, _ = mocker.RegisterTarget(context.Background(), &elbv2.RegisterTargetsInput{ - TargetGroupArn: svc.Service.LoadBalancers[0].TargetGroupArn, - }) - } - return mocker, ecsMock, albMock, ec2Mock -} - func TestCage_RollOut_FARGATE(t *testing.T) { log.SetLevel(log.DebugLevel) - newTimer = fakeTimer - t.Cleanup(recoverTimer) - t.Run("basic", func(t *testing.T) { for _, v := range []int{1, 2, 15} { log.Info("====") - envars := DefaultEnvars() + envars := test.DefaultEnvars() ctrl := gomock.NewController(t) - mctx, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, v, "FARGATE") + mctx, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, v, "FARGATE") if mctx.ServiceSize() != 1 { t.Fatalf("current service not setup") @@ -112,27 +36,25 @@ func TestCage_RollOut_FARGATE(t *testing.T) { t.Fatalf("current tasks not setup: %d/%d", v, taskCnt) } - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - ALB: albMock, - EC2: ec2Mock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + ALB: albMock, + EC2: ec2Mock, + Time: test.NewFakeTime(), }) ctx := context.Background() result, err := cagecli.RollOut(ctx) - if err != nil { - t.Fatalf("%s", err) - } + assert.NoError(t, err) assert.False(t, result.ServiceIntact) assert.Equal(t, 1, mctx.ServiceSize()) assert.Equal(t, v, mctx.RunningTaskSize()) } }) - - t.Run("canary taskใŒtgใซ็™ป้Œฒใ•ใ‚Œใ‚‹ใพใงๅฐ‘ใ—ๅพ…ใค", func(t *testing.T) { - envars := DefaultEnvars() + t.Run("wait until canary task is registered to target group", func(t *testing.T) { + envars := test.DefaultEnvars() ctrl := gomock.NewController(t) - mocker, ecsMock, _, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + mocker, ecsMock, _, ec2Mock := test.Setup(ctrl, envars, 2, "FARGATE") albMock := mock_awsiface.NewMockAlbClient(ctrl) albMock.EXPECT().RegisterTargets(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTarget).AnyTimes() @@ -153,24 +75,23 @@ func TestCage_RollOut_FARGATE(t *testing.T) { }, nil).Times(2), albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetHealth).AnyTimes(), ) - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - ALB: albMock, - EC2: ec2Mock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + ALB: albMock, + EC2: ec2Mock, + Time: test.NewFakeTime(), }) ctx := context.Background() result, err := cagecli.RollOut(ctx) - if err != nil { - t.Fatalf(err.Error()) - } + assert.NoError(t, err) assert.NotNil(t, result) }) - t.Run("canary taskใŒtgใซ็™ป้Œฒใ•ใ‚Œใชใ„ๅ ดๅˆใฏๆ‰“ใกๅˆ‡ใ‚‹", func(t *testing.T) { - envars := DefaultEnvars() + t.Run("stop rolloing out when canary task is not registered to target group", func(t *testing.T) { + envars := test.DefaultEnvars() ctrl := gomock.NewController(t) - mocker, ecsMock, _, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + mocker, ecsMock, _, ec2Mock := test.Setup(ctrl, envars, 2, "FARGATE") albMock := mock_awsiface.NewMockAlbClient(ctrl) albMock.EXPECT().RegisterTargets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTarget).AnyTimes() albMock.EXPECT().DeregisterTargets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeregisterTarget).AnyTimes() @@ -198,11 +119,12 @@ func TestCage_RollOut_FARGATE(t *testing.T) { }, nil), albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetHealth).AnyTimes(), ) - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - EC2: ec2Mock, - ALB: albMock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + Time: test.NewFakeTime(), }) ctx := context.Background() _, err := cagecli.RollOut(ctx) @@ -210,11 +132,11 @@ func TestCage_RollOut_FARGATE(t *testing.T) { }) t.Run("Show error if service doesn't exist", func(t *testing.T) { - envars := DefaultEnvars() + envars := test.DefaultEnvars() ctrl := gomock.NewController(t) - mocker, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + mocker, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, 2, "FARGATE") delete(mocker.Services, envars.Service) - cagecli := NewCage(&Input{ + cagecli := cage.NewCage(&cage.Input{ Env: envars, ECS: ecsMock, EC2: ec2Mock, @@ -224,18 +146,18 @@ func TestCage_RollOut_FARGATE(t *testing.T) { _, err := cagecli.RollOut(ctx) assert.EqualError(t, err, "service 'service' doesn't exist. Run 'cage up' or create service before rolling out") }) - - t.Run("lbใŒใชใ„ใ‚ตใƒผใƒ“ใ‚นใฎๅ ดๅˆใ‚‚ใƒญใƒผใƒซใ‚ขใ‚ฆใƒˆใ™ใ‚‹", func(t *testing.T) { - envars := DefaultEnvars() + t.Run("Roll out even if the service does not have a load balancer", func(t *testing.T) { + envars := test.DefaultEnvars() envars.ServiceDefinitionInput.LoadBalancers = nil envars.CanaryTaskIdleDuration = 1 ctrl := gomock.NewController(t) - _, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - EC2: ec2Mock, - ALB: albMock, + _, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, 2, "FARGATE") + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + Time: test.NewFakeTime(), }) ctx := context.Background() if res, err := cagecli.RollOut(ctx); err != nil { @@ -246,7 +168,7 @@ func TestCage_RollOut_FARGATE(t *testing.T) { }) t.Run("stop rolloing out when service status is inactive", func(t *testing.T) { - envars := DefaultEnvars() + envars := test.DefaultEnvars() ctrl := gomock.NewController(t) ecsMock := mock_awsiface.NewMockEcsClient(ctrl) ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return( @@ -256,18 +178,18 @@ func TestCage_RollOut_FARGATE(t *testing.T) { }, }, nil, ) - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + Time: test.NewFakeTime(), }) _, err := cagecli.RollOut(context.Background()) assert.EqualError(t, err, "๐Ÿ˜ต 'service' status is 'INACTIVE'. Stop rolling out") }) - - t.Run("canary task container ใŒ healthy ใซใชใ‚‰ใชใ„ๅ ดๅˆใฏๆ‰“ใกๅˆ‡ใ‚‹", func(t *testing.T) { - envars := DefaultEnvars() + t.Run("Stop rolling out if the canary task container does not become healthy", func(t *testing.T) { + envars := test.DefaultEnvars() ctrl := gomock.NewController(t) - mocker, _, albMock, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + mocker, _, albMock, ec2Mock := test.Setup(ctrl, envars, 2, "FARGATE") ecsMock := mock_awsiface.NewMockEcsClient(ctrl) ecsMock.EXPECT().CreateService(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.CreateService).AnyTimes() @@ -299,11 +221,12 @@ func TestCage_RollOut_FARGATE(t *testing.T) { ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask).AnyTimes() ecsMock.EXPECT().StopTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.StopTask).AnyTimes() - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - EC2: ec2Mock, - ALB: albMock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + Time: test.NewFakeTime(), }) ctx := context.Background() res, err := cagecli.RollOut(ctx) @@ -320,16 +243,14 @@ func TestCage_RollOut_FARGATE(t *testing.T) { func TestCage_RollOut_EC2(t *testing.T) { log.SetLevel(log.DebugLevel) - newTimer = fakeTimer - defer recoverTimer() for _, v := range []int{1, 2, 15} { log.Info("====") canaryInstanceArn := "arn:aws:ecs:us-west-2:1234567689012:container-instance/abcdefg-hijk-lmn-opqrstuvwxyz" attributeValue := "true" - envars := DefaultEnvars() + envars := test.DefaultEnvars() envars.CanaryInstanceArn = canaryInstanceArn ctrl := gomock.NewController(t) - mctx, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, v, "ec2") + mctx, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, v, "ec2") ecsMock.EXPECT().ListAttributes(gomock.Any(), gomock.Any()).Return(&ecs.ListAttributesOutput{ Attributes: []ecstypes.Attribute{ { @@ -345,11 +266,12 @@ func TestCage_RollOut_EC2(t *testing.T) { if taskCnt := mctx.RunningTaskSize(); taskCnt != v { t.Fatalf("current tasks not setup: %d/%d", v, taskCnt) } - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - EC2: ec2Mock, - ALB: albMock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + Time: test.NewFakeTime(), }) ctx := context.Background() result, err := cagecli.RollOut(ctx) @@ -364,23 +286,22 @@ func TestCage_RollOut_EC2(t *testing.T) { func TestCage_RollOut_EC2_without_ContainerInstanceArn(t *testing.T) { log.SetLevel(log.DebugLevel) - newTimer = fakeTimer - defer recoverTimer() log.Info("====") - envars := DefaultEnvars() + envars := test.DefaultEnvars() ctrl := gomock.NewController(t) - mctx, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, 1, "EC2") + mctx, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, 1, "EC2") if mctx.ServiceSize() != 1 { t.Fatalf("current service not setup") } if taskCnt := mctx.RunningTaskSize(); taskCnt != 1 { t.Fatalf("current tasks not setup: %d/%d", 1, taskCnt) } - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - EC2: ec2Mock, - ALB: albMock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + Time: test.NewFakeTime(), }) ctx := context.Background() result, err := cagecli.RollOut(ctx) @@ -394,14 +315,12 @@ func TestCage_RollOut_EC2_without_ContainerInstanceArn(t *testing.T) { func TestCage_RollOut_EC2_no_attribute(t *testing.T) { log.SetLevel(log.DebugLevel) - newTimer = fakeTimer - defer recoverTimer() log.Info("====") canaryInstanceArn := "arn:aws:ecs:us-west-2:1234567689012:container-instance/abcdefg-hijk-lmn-opqrstuvwxyz" - envars := DefaultEnvars() + envars := test.DefaultEnvars() envars.CanaryInstanceArn = canaryInstanceArn ctrl := gomock.NewController(t) - mctx, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, 1, "EC2") + mctx, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, 1, "EC2") if mctx.ServiceSize() != 1 { t.Fatalf("current service not setup") } @@ -412,11 +331,12 @@ func TestCage_RollOut_EC2_no_attribute(t *testing.T) { Attributes: []ecstypes.Attribute{}, }, nil).AnyTimes() ecsMock.EXPECT().PutAttributes(gomock.Any(), gomock.Any()).Return(&ecs.PutAttributesOutput{}, nil).AnyTimes() - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - EC2: ec2Mock, - ALB: albMock, + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + Time: test.NewFakeTime(), }) ctx := context.Background() result, err := cagecli.RollOut(ctx) @@ -427,22 +347,3 @@ func TestCage_RollOut_EC2_no_attribute(t *testing.T) { assert.Equal(t, 1, mctx.ServiceSize()) assert.Equal(t, 1, mctx.RunningTaskSize()) } - -func TestCage_CreateNextTaskDefinition(t *testing.T) { - envars := &Envars{ - TaskDefinitionArn: "arn://task", - } - ctrl := gomock.NewController(t) - e := mock_awsiface.NewMockEcsClient(ctrl) - e.EXPECT().DescribeTaskDefinition(gomock.Any(), gomock.Any()).Return( - &ecs.DescribeTaskDefinitionOutput{ - TaskDefinition: &ecstypes.TaskDefinition{TaskDefinitionArn: aws.String("arn://task")}, - }, nil) - // nextTaskDefinitionArnใŒใ‚ใ‚‹ๅ ดๅˆใฏdescribeTaskDefinitionใ‹ใ‚‰่ฟ”ใ™ - cagecli := &cage{env: envars, ecs: e} - o, err := cagecli.CreateNextTaskDefinition(context.Background()) - if err != nil { - t.Fatalf(err.Error()) - } - assert.Equal(t, envars.TaskDefinitionArn, *o.TaskDefinitionArn) -} diff --git a/run.go b/run.go index e4910df..b5cca30 100644 --- a/run.go +++ b/run.go @@ -6,6 +6,7 @@ import ( "time" "github.com/apex/log" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" ) @@ -42,6 +43,7 @@ func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { NetworkConfiguration: c.env.ServiceDefinitionInput.NetworkConfiguration, PlatformVersion: c.env.ServiceDefinitionInput.PlatformVersion, Overrides: input.Overrides, + Group: aws.String("cage:run-task"), }) if err != nil { return nil, err @@ -54,7 +56,7 @@ func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { var exitCode int32 = -1 log.Infof("๐Ÿค– waiting until task '%s' is running...", *taskArn) for count < maxCount { - <-newTimer(interval).C + <-c.time.NewTimer(interval).C o, err := c.ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ Cluster: &c.env.Cluster, Tasks: []string{*taskArn}, diff --git a/run_test.go b/run_test.go index ab454ec..cc5b34b 100644 --- a/run_test.go +++ b/run_test.go @@ -1,4 +1,4 @@ -package cage +package cage_test import ( "context" @@ -8,83 +8,59 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" "github.com/loilo-inc/canarycage/mocks/mock_awsiface" + "github.com/loilo-inc/canarycage/test" "github.com/stretchr/testify/assert" ) func TestCage_Run(t *testing.T) { - container := "container" - setupForBasic := func(ctx context.Context, - ctrl *gomock.Controller, - results []*ecs.DescribeTasksOutput) *mock_awsiface.MockEcsClient { + setupForBasic := func(t *testing.T) (*cage.Envars, *mock_awsiface.MockEcsClient) { + env := test.DefaultEnvars() + mocker := test.NewMockContext() + ctrl := gomock.NewController(t) ecsMock := mock_awsiface.NewMockEcsClient(ctrl) - td := &ecs.RegisterTaskDefinitionOutput{ - TaskDefinition: &ecstypes.TaskDefinition{ - ContainerDefinitions: []ecstypes.ContainerDefinition{ - {Name: &container}, - }, - }, - } - ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(td, nil) - runTaskOutput := &ecs.RunTaskOutput{ - Tasks: []ecstypes.Task{ - {TaskArn: aws.String("arn")}, - }, - } - ecsMock.EXPECT().RunTask(ctx, gomock.Any()).Return(runTaskOutput, nil) - for _, o := range results { - ecsMock.EXPECT().DescribeTasks(ctx, gomock.Any()).Return(o, nil) - } - return ecsMock + ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTaskDefinition).AnyTimes() + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks).AnyTimes() + return env, ecsMock } t.Run("basic", func(t *testing.T) { - env := DefaultEnvars() - ctrl := gomock.NewController(t) - defer ctrl.Finish() overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - ecsMock := setupForBasic(ctx, ctrl, []*ecs.DescribeTasksOutput{ - {Tasks: []ecstypes.Task{ - {LastStatus: aws.String("RUNNING"), - Containers: []ecstypes.Container{{ - Name: &container, - ExitCode: nil, - }}}, - }}, - {Tasks: []ecstypes.Task{ - {LastStatus: aws.String("STOPPED"), - Containers: []ecstypes.Container{{ - Name: &container, - ExitCode: aws.Int32(0), - }}, - }, - }}, - }) - - cagecli := NewCage(&Input{ - Env: env, - ECS: ecsMock, - ALB: nil, - EC2: nil, + env, ecsMock := setupForBasic(t) + ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *ecs.RunTaskInput, optFns ...func(*ecs.Options)) (*ecs.RunTaskOutput, error) { + task, err := mocker.RunTask(ctx, input) + if err != nil { + return nil, err + } + stop, err := mocker.StopTask(ctx, &ecs.StopTaskInput{Cluster: input.Cluster, Task: task.Tasks[0].TaskArn}) + if err != nil { + return nil, err + } + return &ecs.RunTaskOutput{Tasks: []ecstypes.Task{*stop.Task}}, nil + }, + ).AnyTimes() + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + ALB: nil, + EC2: nil, + Time: test.NewFakeTime(), }) - newTimer = fakeTimer - defer recoverTimer() - result, err := cagecli.Run(ctx, &RunInput{ + result, err := cagecli.Run(ctx, &cage.RunInput{ Container: &container, Overrides: overrides, }) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, result.ExitCode, int32(0)) }) t.Run("should error if max attempts exceeded", func(t *testing.T) { - env := DefaultEnvars() - ctrl := gomock.NewController(t) - defer ctrl.Finish() overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - ecsMock := setupForBasic(ctx, ctrl, nil) + env, ecsMock := setupForBasic(t) ecsMock.EXPECT().DescribeTasks(ctx, gomock.Any()).AnyTimes().Return(&ecs.DescribeTasksOutput{ Tasks: []ecstypes.Task{ {LastStatus: aws.String("RUNNING"), @@ -94,15 +70,14 @@ func TestCage_Run(t *testing.T) { }}}, }, }, nil) - cagecli := NewCage(&Input{ - Env: env, - ECS: ecsMock, - ALB: nil, - EC2: nil, + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + ALB: nil, + EC2: nil, + Time: test.NewFakeTime(), }) - newTimer = fakeTimer - defer recoverTimer() - result, err := cagecli.Run(ctx, &RunInput{ + result, err := cagecli.Run(ctx, &cage.RunInput{ Container: &container, Overrides: overrides, }) @@ -110,30 +85,18 @@ func TestCage_Run(t *testing.T) { assert.EqualError(t, err, "๐Ÿšซ max attempts exceeded") }) t.Run("should error if exit code was not 0", func(t *testing.T) { - env := DefaultEnvars() - ctrl := gomock.NewController(t) - defer ctrl.Finish() overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - ecsMock := setupForBasic(ctx, ctrl, []*ecs.DescribeTasksOutput{ - {Tasks: []ecstypes.Task{ - {LastStatus: aws.String("STOPPED"), - Containers: []ecstypes.Container{{ - Name: &container, - ExitCode: aws.Int32(1), - }}}, - }}, + env, ecsMock := setupForBasic(t) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + ALB: nil, + EC2: nil, + Time: test.NewFakeTime(), }) - cagecli := NewCage(&Input{ - Env: env, - ECS: ecsMock, - ALB: nil, - EC2: nil, - }) - newTimer = fakeTimer - defer recoverTimer() - result, err := cagecli.Run(ctx, &RunInput{ + result, err := cagecli.Run(ctx, &cage.RunInput{ Container: &container, Overrides: overrides, }) @@ -141,30 +104,18 @@ func TestCage_Run(t *testing.T) { assert.EqualError(t, err, "๐Ÿšซ task exited with 1") }) t.Run("should error if exit code is nil", func(t *testing.T) { - env := DefaultEnvars() - ctrl := gomock.NewController(t) - defer ctrl.Finish() overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - ecsMock := setupForBasic(ctx, ctrl, []*ecs.DescribeTasksOutput{ - {Tasks: []ecstypes.Task{ - {LastStatus: aws.String("STOPPED"), - Containers: []ecstypes.Container{{ - Name: &container, - ExitCode: nil, - }}}, - }}, - }) - cagecli := NewCage(&Input{ - Env: env, - ECS: ecsMock, - ALB: nil, - EC2: nil, + env, ecsMock := setupForBasic(t) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + ALB: nil, + EC2: nil, + Time: test.NewFakeTime(), }) - newTimer = fakeTimer - defer recoverTimer() - result, err := cagecli.Run(ctx, &RunInput{ + result, err := cagecli.Run(ctx, &cage.RunInput{ Container: &container, Overrides: overrides, }) @@ -172,13 +123,10 @@ func TestCage_Run(t *testing.T) { assert.EqualError(t, err, "๐Ÿšซ container 'container' hasn't exit") }) t.Run("should error if container doesn't exist in definition", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - env := DefaultEnvars() overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + env, ecsMock := setupForBasic(t) td := &ecs.RegisterTaskDefinitionOutput{ TaskDefinition: &ecstypes.TaskDefinition{ ContainerDefinitions: []ecstypes.ContainerDefinition{ @@ -188,13 +136,14 @@ func TestCage_Run(t *testing.T) { } ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(td, nil) - cagecli := NewCage(&Input{ - Env: env, - ECS: ecsMock, - ALB: nil, - EC2: nil, + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + ALB: nil, + EC2: nil, + Time: test.NewFakeTime(), }) - result, err := cagecli.Run(ctx, &RunInput{ + result, err := cagecli.Run(ctx, &cage.RunInput{ Container: aws.String("foo"), Overrides: overrides, }) diff --git a/task_definition.go b/task_definition.go new file mode 100644 index 0000000..c4a919a --- /dev/null +++ b/task_definition.go @@ -0,0 +1,37 @@ +package cage + +import ( + "context" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" +) + +func (c *cage) CreateNextTaskDefinition(ctx context.Context) (*ecstypes.TaskDefinition, error) { + if c.env.TaskDefinitionArn != "" { + log.Infof("--taskDefinitionArn was set to '%s'. skip registering new task definition.", c.env.TaskDefinitionArn) + o, err := c.ecs.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{ + TaskDefinition: &c.env.TaskDefinitionArn, + }) + if err != nil { + log.Errorf( + "failed to describe next task definition '%s' due to: %s", + c.env.TaskDefinitionArn, err, + ) + return nil, err + } + return o.TaskDefinition, nil + } else { + log.Infof("creating next task definition...") + if out, err := c.ecs.RegisterTaskDefinition(ctx, c.env.TaskDefinitionInput); err != nil { + return nil, err + } else { + log.Infof( + "task definition '%s:%d' has been registered", + *out.TaskDefinition.Family, out.TaskDefinition.Revision, + ) + return out.TaskDefinition, nil + } + } +} diff --git a/task_definition_test.go b/task_definition_test.go new file mode 100644 index 0000000..dc64f15 --- /dev/null +++ b/task_definition_test.go @@ -0,0 +1,23 @@ +package cage_test + +// func TestCage_CreateNextTaskDefinition(t *testing.T) { +// envars := &cage.Envars{ +// TaskDefinitionArn: "arn://task", +// } +// ctrl := gomock.NewController(t) +// e := mock_awsiface.NewMockEcsClient(ctrl) +// e.EXPECT().DescribeTaskDefinition(gomock.Any(), gomock.Any()).Return( +// &ecs.DescribeTaskDefinitionOutput{ +// TaskDefinition: &ecstypes.TaskDefinition{TaskDefinitionArn: aws.String("arn://task")}, +// }, nil) +// // nextTaskDefinitionArnใŒใ‚ใ‚‹ๅ ดๅˆใฏdescribeTaskDefinitionใ‹ใ‚‰่ฟ”ใ™ +// cagecli := cage.NewCage(&cage.Input{ +// Env: envars, +// ECS: e, +// }) +// o, err := cagecli.CreateNextTaskDefinition(context.Background()) +// if err != nil { +// t.Fatalf(err.Error()) +// } +// assert.Equal(t, envars.TaskDefinitionArn, *o.TaskDefinitionArn) +// } diff --git a/test/context.go b/test/context.go index 43d660a..c19b135 100644 --- a/test/context.go +++ b/test/context.go @@ -20,17 +20,19 @@ import ( type MockContext struct { Services map[string]*types.Service Tasks map[string]*types.Task - TaskDefinitions map[string]*types.TaskDefinition + TaskDefinitions *TaskDefinitionRepository TargetGroups map[string]struct{} mux sync.Mutex } func NewMockContext() *MockContext { return &MockContext{ - Services: make(map[string]*types.Service), - Tasks: make(map[string]*types.Task), - TaskDefinitions: make(map[string]*types.TaskDefinition), - TargetGroups: make(map[string]struct{}), + Services: make(map[string]*types.Service), + Tasks: make(map[string]*types.Task), + TaskDefinitions: &TaskDefinitionRepository{ + families: make(map[string]*TaskDefinitionFamily), + }, + TargetGroups: make(map[string]struct{}), } } @@ -71,6 +73,9 @@ func (ctx *MockContext) ServiceSize() int { func (ctx *MockContext) CreateService(c context.Context, input *ecs.CreateServiceInput, _ ...func(options *ecs.Options)) (*ecs.CreateServiceOutput, error) { idstr := uuid.New().String() st := "ACTIVE" + if _, ok := ctx.Services[*input.ServiceName]; ok { + return nil, fmt.Errorf("service already exists: %s", *input.ServiceName) + } ret := &types.Service{ ServiceName: input.ServiceName, RunningCount: 0, @@ -102,6 +107,9 @@ func (ctx *MockContext) CreateService(c context.Context, input *ecs.CreateServic TaskDefinition: input.TaskDefinition, }) } + ctx.mux.Lock() + ctx.Services[*input.ServiceName].RunningCount = *input.DesiredCount + ctx.mux.Unlock() log.Debugf("%s: running=%d", *input.ServiceName, ret.RunningCount) return &ecs.CreateServiceOutput{ Service: ret, @@ -113,6 +121,10 @@ func (ctx *MockContext) UpdateService(c context.Context, input *ecs.UpdateServic s := ctx.Services[*input.Service] ctx.mux.Unlock() nextDesiredCount := s.DesiredCount + nextTaskDefinition := s.TaskDefinition + if input.TaskDefinition != nil { + nextTaskDefinition = input.TaskDefinition + } if input.DesiredCount != nil { nextDesiredCount = *input.DesiredCount } @@ -123,7 +135,7 @@ func (ctx *MockContext) UpdateService(c context.Context, input *ecs.UpdateServic ctx.StartTask(c, &ecs.StartTaskInput{ Cluster: input.Cluster, Group: aws.String(fmt.Sprintf("service:%s", *input.Service)), - TaskDefinition: input.TaskDefinition, + TaskDefinition: nextTaskDefinition, }) } } else if diff < 0 { @@ -146,7 +158,7 @@ func (ctx *MockContext) UpdateService(c context.Context, input *ecs.UpdateServic } ctx.mux.Lock() s.DesiredCount = nextDesiredCount - s.TaskDefinition = input.TaskDefinition + s.TaskDefinition = nextTaskDefinition s.RunningCount = nextDesiredCount s.Deployments = []types.Deployment{ { @@ -189,27 +201,21 @@ func (ctx *MockContext) DeleteService(c context.Context, input *ecs.DeleteServic } func (ctx *MockContext) RegisterTaskDefinition(_ context.Context, input *ecs.RegisterTaskDefinitionInput, _ ...func(options *ecs.Options)) (*ecs.RegisterTaskDefinitionOutput, error) { - ctx.mux.Lock() - defer ctx.mux.Unlock() - - idstr := uuid.New().String() - ctx.TaskDefinitions[idstr] = &types.TaskDefinition{ - TaskDefinitionArn: &idstr, - Family: aws.String("family"), - Revision: 1, - ContainerDefinitions: input.ContainerDefinitions, + td, err := ctx.TaskDefinitions.Register(input) + if err != nil { + return nil, err } - return &ecs.RegisterTaskDefinitionOutput{ - TaskDefinition: ctx.TaskDefinitions[idstr], - }, nil + return &ecs.RegisterTaskDefinitionOutput{TaskDefinition: td}, nil } func (ctx *MockContext) StartTask(_ context.Context, input *ecs.StartTaskInput, _ ...func(options *ecs.Options)) (*ecs.StartTaskOutput, error) { ctx.mux.Lock() defer ctx.mux.Unlock() - - id := uuid.New() - idstr := id.String() + td := ctx.TaskDefinitions.Get(*input.TaskDefinition) + if td == nil { + return nil, fmt.Errorf("task definition not found: %s", *input.TaskDefinition) + } + taskArn := fmt.Sprintf("arn:aws:ecs:us-west-2:012345678910:task/%s", uuid.New().String()) attachments := []types.Attachment{{ Details: []types.KeyValuePair{ { @@ -222,9 +228,8 @@ func (ctx *MockContext) StartTask(_ context.Context, input *ecs.StartTaskInput, }, }, }} - - containers := make([]types.Container, len(ctx.TaskDefinitions[*input.TaskDefinition].ContainerDefinitions)) - for i, v := range ctx.TaskDefinitions[*input.TaskDefinition].ContainerDefinitions { + containers := make([]types.Container, len(td.ContainerDefinitions)) + for i, v := range td.ContainerDefinitions { containers[i] = types.Container{ Name: v.Name, Image: v.Image, @@ -238,24 +243,18 @@ func (ctx *MockContext) StartTask(_ context.Context, input *ecs.StartTaskInput, } ret := types.Task{ - TaskArn: &idstr, + TaskArn: &taskArn, ClusterArn: input.Cluster, TaskDefinitionArn: input.TaskDefinition, Group: input.Group, Containers: containers, } - ctx.Tasks[idstr] = &ret - s, ok := ctx.Services[*input.Group] + ctx.Tasks[taskArn] = &ret var launchType types.LaunchType - if ok { - s.RunningCount += 1 - launchType = s.LaunchType + if len(input.ContainerInstances) > 0 { + launchType = types.LaunchTypeEc2 } else { - if len(input.ContainerInstances) > 0 { - launchType = types.LaunchTypeEc2 - } else { - launchType = types.LaunchTypeFargate - } + launchType = types.LaunchTypeFargate } ret.LaunchType = launchType if launchType == types.LaunchTypeFargate { @@ -268,6 +267,7 @@ func (ctx *MockContext) StartTask(_ context.Context, input *ecs.StartTaskInput, Tasks: []types.Task{ret}, }, nil } + func (ctx *MockContext) RunTask(c context.Context, input *ecs.RunTaskInput, _ ...func(options *ecs.Options)) (*ecs.RunTaskOutput, error) { o, err := ctx.StartTask(c, &ecs.StartTaskInput{ Cluster: input.Cluster, @@ -287,16 +287,22 @@ func (ctx *MockContext) StopTask(_ context.Context, input *ecs.StopTaskInput, _ ctx.mux.Lock() defer ctx.mux.Unlock() log.Debugf("stop: %s", input) - ret := ctx.Tasks[*input.Task] + ret, ok := ctx.Tasks[*input.Task] + if !ok { + return nil, fmt.Errorf("task not found: %s", *input.Task) + } + for i := range ret.Containers { + v := &ret.Containers[i] + v.ExitCode = aws.Int32(0) + v.LastStatus = aws.String("STOPPED") + } ret.LastStatus = aws.String("STOPPED") ret.DesiredStatus = aws.String("STOPPED") service, ok := ctx.Services[*ret.Group] if ok { service.RunningCount -= 1 } - return &ecs.StopTaskOutput{ - Task: ret, - }, nil + return &ecs.StopTaskOutput{Task: ret}, nil } func (ctx *MockContext) ListTasks(_ context.Context, input *ecs.ListTasksInput, _ ...func(options *ecs.Options)) (*ecs.ListTasksOutput, error) { @@ -426,9 +432,7 @@ func (ctx *MockContext) RegisterTarget(_ context.Context, input *elbv2.RegisterT } func (ctx *MockContext) DeregisterTarget(_ context.Context, input *elbv2.DeregisterTargetsInput, _ ...func(options *elbv2.Options)) (*elbv2.DeregisterTargetsOutput, error) { - if _, ok := ctx.TargetGroups[*input.TargetGroupArn]; ok { - delete(ctx.TargetGroups, *input.TargetGroupArn) - } + delete(ctx.TargetGroups, *input.TargetGroupArn) return &elbv2.DeregisterTargetsOutput{}, nil } diff --git a/test/fake_timer.go b/test/fake_timer.go new file mode 100644 index 0000000..0ebeb14 --- /dev/null +++ b/test/fake_timer.go @@ -0,0 +1,29 @@ +package test + +import ( + "time" + + cage "github.com/loilo-inc/canarycage" +) + +func newTimer(_ time.Duration) *time.Timer { + ch := make(chan time.Time) + go func() { + ch <- time.Now() + }() + return &time.Timer{ + C: ch, + } +} + +type timeImpl struct{} + +func (t *timeImpl) Now() time.Time { + return time.Now() +} +func (t *timeImpl) NewTimer(d time.Duration) *time.Timer { + return newTimer(d) +} +func NewFakeTime() cage.Time { + return &timeImpl{} +} diff --git a/test/setup.go b/test/setup.go new file mode 100644 index 0000000..e4c7d89 --- /dev/null +++ b/test/setup.go @@ -0,0 +1,88 @@ +package test + +import ( + "context" + "encoding/json" + "log" + "os" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" + "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" + "github.com/loilo-inc/canarycage/mocks/mock_awsiface" +) + +func Setup(ctrl *gomock.Controller, envars *cage.Envars, currentTaskCount int, launchType string) ( + *MockContext, + *mock_awsiface.MockEcsClient, + *mock_awsiface.MockAlbClient, + *mock_awsiface.MockEc2Client, +) { + mocker := NewMockContext() + + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + ecsMock.EXPECT().CreateService(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.CreateService).AnyTimes() + ecsMock.EXPECT().UpdateService(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.UpdateService).AnyTimes() + ecsMock.EXPECT().DeleteService(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeleteService).AnyTimes() + ecsMock.EXPECT().StartTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.StartTask).AnyTimes() + ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTaskDefinition).AnyTimes() + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeServices).AnyTimes() + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks).AnyTimes() + ecsMock.EXPECT().ListTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.ListTasks).AnyTimes() + ecsMock.EXPECT().DescribeContainerInstances(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeContainerInstances).AnyTimes() + ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask).AnyTimes() + ecsMock.EXPECT().StopTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.StopTask).AnyTimes() + + albMock := mock_awsiface.NewMockAlbClient(ctrl) + albMock.EXPECT().DescribeTargetGroups(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetGroups).AnyTimes() + albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetHealth).AnyTimes() + albMock.EXPECT().DescribeTargetGroupAttributes(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetGroupAttibutes).AnyTimes() + albMock.EXPECT().RegisterTargets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTarget).AnyTimes() + albMock.EXPECT().DeregisterTargets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeregisterTarget).AnyTimes() + + ec2Mock := mock_awsiface.NewMockEc2Client(ctrl) + ec2Mock.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeSubnets).AnyTimes() + ec2Mock.EXPECT().DescribeInstances(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeInstances).AnyTimes() + td, _ := mocker.RegisterTaskDefinition(context.Background(), envars.TaskDefinitionInput) + a := &ecs.CreateServiceInput{ + ServiceName: &envars.Service, + LoadBalancers: envars.ServiceDefinitionInput.LoadBalancers, + TaskDefinition: td.TaskDefinition.TaskDefinitionArn, + DesiredCount: aws.Int32(int32(currentTaskCount)), + LaunchType: ecstypes.LaunchType(launchType), + } + svc, _ := mocker.CreateService(context.Background(), a) + if len(svc.Service.LoadBalancers) > 0 { + _, _ = mocker.RegisterTarget(context.Background(), &elbv2.RegisterTargetsInput{ + TargetGroupArn: svc.Service.LoadBalancers[0].TargetGroupArn, + }) + } + return mocker, ecsMock, albMock, ec2Mock +} + +func DefaultEnvars() *cage.Envars { + d, _ := os.ReadFile("fixtures/task-definition.json") + var taskDefinition ecs.RegisterTaskDefinitionInput + if err := json.Unmarshal(d, &taskDefinition); err != nil { + log.Fatalf(err.Error()) + } + return &cage.Envars{ + Region: "us-west-2", + Cluster: "cage-test", + Service: "service", + ServiceDefinitionInput: ReadServiceDefinition("fixtures/service.json"), + TaskDefinitionInput: &taskDefinition, + } +} + +func ReadServiceDefinition(path string) *ecs.CreateServiceInput { + d, _ := os.ReadFile(path) + var dest ecs.CreateServiceInput + if err := json.Unmarshal(d, &dest); err != nil { + log.Fatalf(err.Error()) + } + return &dest +} diff --git a/test/task_definiton.go b/test/task_definiton.go new file mode 100644 index 0000000..8f8ebb9 --- /dev/null +++ b/test/task_definiton.go @@ -0,0 +1,83 @@ +package test + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" +) + +type TaskDefinitionRepository struct { + families map[string]*TaskDefinitionFamily +} + +type TaskDefinitionFamily struct { + family string + revision int32 + revisions map[int32]*ecstypes.TaskDefinition +} + +func (t *TaskDefinitionRepository) Register(input *ecs.RegisterTaskDefinitionInput) (*ecstypes.TaskDefinition, error) { + family := *input.Family + if _, ok := t.families[family]; !ok { + t.families[family] = &TaskDefinitionFamily{ + family: family, + revisions: make(map[int32]*ecstypes.TaskDefinition), + } + } + return t.families[family].Register(input) +} + +func parseTaskDefinitionArn(arn string) (string, int32) { + if regexp.MustCompile(`arn:aws:ecs:.*:.*:task-definition/.*:\d+`).MatchString(arn) { + split := strings.Split(arn, "/") + familyRev := split[len(split)-1] + split = strings.Split(familyRev, ":") + family := split[0] + revision, _ := strconv.ParseInt(split[1], 10, 32) + return family, int32(revision) + } else if regexp.MustCompile(`.*:\d+`).MatchString(arn) { + split := strings.Split(arn, ":") + family := split[0] + revision, _ := strconv.ParseInt(split[1], 10, 32) + return family, int32(revision) + } + return "", 0 +} + +func (t *TaskDefinitionRepository) Get(familyRev string) *ecstypes.TaskDefinition { + family, revision := parseTaskDefinitionArn(familyRev) + if f, ok := t.families[family]; !ok { + return nil + } else if td, ok := f.revisions[int32(revision)]; !ok { + return nil + } else { + return td + } +} + +func (t *TaskDefinitionRepository) List() []*ecstypes.TaskDefinition { + var tds []*ecstypes.TaskDefinition + for _, f := range t.families { + for _, td := range f.revisions { + tds = append(tds, td) + } + } + return tds +} + +func (t *TaskDefinitionFamily) Register(input *ecs.RegisterTaskDefinitionInput) (*ecstypes.TaskDefinition, error) { + t.revision++ + arn := fmt.Sprintf("arn:aws:ecs:us-west-2:012345678910:task-definition/%s:%d", t.family, t.revision) + td := &ecstypes.TaskDefinition{ + TaskDefinitionArn: &arn, + Family: &t.family, + Revision: t.revision, + ContainerDefinitions: input.ContainerDefinitions, + } + t.revisions[t.revision] = td + return td, nil +} diff --git a/time.go b/time.go index 4d521aa..d4f9e6a 100644 --- a/time.go +++ b/time.go @@ -2,19 +2,11 @@ package cage import "time" -var newTimer = time.NewTimer -var now = time.Now +type timeImpl struct{} -func fakeTimer(d time.Duration) *time.Timer { - ch := make(chan time.Time) - go func() { - ch <- time.Now() - }() - return &time.Timer{ - C: ch, - } +func (t *timeImpl) Now() time.Time { + return time.Now() } - -func recoverTimer() { - newTimer = time.NewTimer +func (t *timeImpl) NewTimer(d time.Duration) *time.Timer { + return time.NewTimer(d) } diff --git a/up.go b/up.go index 6a869a7..c940301 100644 --- a/up.go +++ b/up.go @@ -32,30 +32,9 @@ func (c *cage) Up(ctx context.Context) (*UpResult, error) { } } c.env.ServiceDefinitionInput.TaskDefinition = td.TaskDefinitionArn - log.Infof("creating service '%s' with task-definition '%s'...", c.env.Service, *td.TaskDefinitionArn) - if o, err := c.ecs.CreateService(ctx, c.env.ServiceDefinitionInput); err != nil { - return nil, fmt.Errorf("failed to create service '%s': %s", c.env.Service, err.Error()) - } else { - log.Infof("service created: '%s'", *o.Service.ServiceArn) - } - log.Infof("waiting for service '%s' to be STABLE", c.env.Service) - if err := ecs.NewServicesStableWaiter(c.ecs).Wait(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, - Services: []string{c.env.Service}, - }, WaitDuration); err != nil { - return nil, fmt.Errorf(err.Error()) - } else { - log.Infof("become: STABLE") - } - svc, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, - Services: []string{c.env.Service}, - }) - if err != nil { + if service, err := c.createService(ctx, c.env.ServiceDefinitionInput); err != nil { return nil, err + } else { + return &UpResult{TaskDefinition: td, Service: service}, nil } - return &UpResult{ - TaskDefinition: td, - Service: &svc.Services[0], - }, nil } diff --git a/up_test.go b/up_test.go index fa6ea4d..45a1243 100644 --- a/up_test.go +++ b/up_test.go @@ -1,19 +1,22 @@ -package cage +package cage_test import ( "context" + "testing" + "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" + "github.com/loilo-inc/canarycage/test" "github.com/stretchr/testify/assert" - "testing" ) func TestCage_Up(t *testing.T) { t.Run("basic", func(t *testing.T) { - env := DefaultEnvars() + env := test.DefaultEnvars() ctrl := gomock.NewController(t) - ctx, ecsMock, _, _ := Setup(ctrl, env, 1, "FARGATE") + ctx, ecsMock, _, _ := test.Setup(ctrl, env, 1, "FARGATE") delete(ctx.Services, env.Service) - cagecli := NewCage(&Input{ + cagecli := cage.NewCage(&cage.Input{ Env: env, ECS: ecsMock, ALB: nil, @@ -25,10 +28,10 @@ func TestCage_Up(t *testing.T) { assert.NotNil(t, result.TaskDefinition) }) t.Run("should show error if service exists", func(t *testing.T) { - env := DefaultEnvars() + env := test.DefaultEnvars() ctrl := gomock.NewController(t) - _, ecsMock, _, _ := Setup(ctrl, env, 1, "FARGATE") - cagecli := NewCage(&Input{ + _, ecsMock, _, _ := test.Setup(ctrl, env, 1, "FARGATE") + cagecli := cage.NewCage(&cage.Input{ Env: env, ECS: ecsMock, ALB: nil, diff --git a/util.go b/util.go index 39b864d..522dc46 100644 --- a/util.go +++ b/util.go @@ -1,33 +1,27 @@ package cage import ( - "errors" - "fmt" - "github.com/apex/log" - "io/ioutil" "os" "regexp" "strings" + + "github.com/apex/log" ) func ReadFileAndApplyEnvars(path string) ([]byte, error) { - if d, err := ioutil.ReadFile(path); err != nil { + d, err := os.ReadFile(path) + if err != nil { return nil, err - } else { - str := string(d) - reg := regexp.MustCompile("\\${(.+?)}") - submatches := reg.FindAllStringSubmatch(str, -1) - for _, m := range submatches { - if envar, ok := os.LookupEnv(m[1]); ok { - str = strings.Replace(str, m[0], envar, -1) - } else { - log.Fatalf("envar literal '%s' found in %s but was not defined", m[0], path) - } + } + str := string(d) + reg := regexp.MustCompile(`\${(.+?)}`) + submatches := reg.FindAllStringSubmatch(str, -1) + for _, m := range submatches { + if envar, ok := os.LookupEnv(m[1]); ok { + str = strings.Replace(str, m[0], envar, -1) + } else { + log.Fatalf("envar literal '%s' found in %s but was not defined", m[0], path) } - return []byte(str), nil } -} - -func NewErrorf(f string, args ...interface{}) error { - return errors.New(fmt.Sprintf(f, args...)) + return []byte(str), nil } From a53a27f6d7398bb65baa6fb19cf5b44f6b297212 Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 20:02:23 +0900 Subject: [PATCH 02/11] fix tests --- env_test.go | 5 +- run.go | 72 ++++++++++++++--------------- run_test.go | 128 ++++++++++++++++++++++++++++++++-------------------- 3 files changed, 114 insertions(+), 91 deletions(-) diff --git a/env_test.go b/env_test.go index 83e8799..84b4ebe 100644 --- a/env_test.go +++ b/env_test.go @@ -36,9 +36,8 @@ func TestEnsureEnvars(t *testing.T) { Cluster: "cluster", Service: "next", } - if err := EnsureEnvars(e); err != nil { - t.Fatalf(err.Error()) - } + err := EnsureEnvars(e) + assert.Errorf(t, err, "--nextTaskDefinitionArn or deploy context must be provided") }) t.Run("should return err if required props are not defined", func(t *testing.T) { dummy := "aaa" diff --git a/run.go b/run.go index b5cca30..2b792dd 100644 --- a/run.go +++ b/run.go @@ -9,17 +9,19 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "golang.org/x/xerrors" ) type RunInput struct { Container *string Overrides *types.TaskOverride + MaxWait time.Duration } type RunResult struct { ExitCode int32 } -func containerExistsInDefinition(td *types.TaskDefinition, container *string) bool { +func containerExistsInDefinition(td *ecs.RegisterTaskDefinitionInput, container *string) bool { for _, v := range td.ContainerDefinitions { if *v.Name == *container { return true @@ -29,13 +31,16 @@ func containerExistsInDefinition(td *types.TaskDefinition, container *string) bo } func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { + if input.MaxWait == 0 { + input.MaxWait = 5 * time.Minute + } + if !containerExistsInDefinition(c.env.TaskDefinitionInput, input.Container) { + return nil, fmt.Errorf("๐Ÿšซ '%s' not found in container definitions", *input.Container) + } td, err := c.CreateNextTaskDefinition(ctx) if err != nil { return nil, err } - if !containerExistsInDefinition(td, input.Container) { - return nil, fmt.Errorf("๐Ÿšซ '%s' not found in container definitions", *input.Container) - } o, err := c.ecs.RunTask(ctx, &ecs.RunTaskInput{ Cluster: &c.env.Cluster, TaskDefinition: td.TaskDefinitionArn, @@ -49,42 +54,33 @@ func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { return nil, err } taskArn := o.Tasks[0].TaskArn - count := 0 - // 5min - maxCount := 30 - interval := time.Second * 10 - var exitCode int32 = -1 - log.Infof("๐Ÿค– waiting until task '%s' is running...", *taskArn) - for count < maxCount { - <-c.time.NewTimer(interval).C - o, err := c.ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, - Tasks: []string{*taskArn}, - }) - if err != nil { - return nil, err - } - task := o.Tasks[0] - log.Infof("๐Ÿค– task status is '%s'", *task.LastStatus) - if *task.LastStatus != "STOPPED" { - count++ - continue - } - for _, container := range task.Containers { - if *container.Name == *input.Container { - if container.ExitCode == nil { - return nil, fmt.Errorf("๐Ÿšซ container '%s' hasn't exit", *input.Container) + log.Infof("waiting for task '%s' to start...", *taskArn) + if err := ecs.NewTasksRunningWaiter(c.ecs).Wait(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.env.Cluster, + Tasks: []string{*taskArn}, + }, input.MaxWait); err != nil { + return nil, xerrors.Errorf("task failed to start: %w", err) + } + log.Infof("task '%s' is running", *taskArn) + log.Infof("waiting for task '%s' to stop...", *taskArn) + if result, err := ecs.NewTasksStoppedWaiter(c.ecs).WaitForOutput(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.env.Cluster, + Tasks: []string{*taskArn}, + }, input.MaxWait); err != nil { + return nil, xerrors.Errorf("task failed to stop: %w", err) + } else { + task := result.Tasks[0] + for _, c := range task.Containers { + if *c.Name == *input.Container { + if c.ExitCode == nil { + return nil, fmt.Errorf("container '%s' hasn't exit", *input.Container) + } else if *c.ExitCode != 0 { + return nil, fmt.Errorf("task exited with %d", *c.ExitCode) } - exitCode = *container.ExitCode - goto next + return &RunResult{ExitCode: *c.ExitCode}, nil } } - return nil, fmt.Errorf("๐Ÿšซ container '%s' not found in results", *input.Container) - } - return nil, fmt.Errorf("๐Ÿšซ max attempts exceeded") -next: - if exitCode != 0 { - return nil, fmt.Errorf("๐Ÿšซ task exited with %d", exitCode) + // Never reached? + return nil, fmt.Errorf("task '%s' not found in result", *taskArn) } - return &RunResult{ExitCode: exitCode}, nil } diff --git a/run_test.go b/run_test.go index cc5b34b..039573b 100644 --- a/run_test.go +++ b/run_test.go @@ -15,33 +15,29 @@ import ( ) func TestCage_Run(t *testing.T) { - setupForBasic := func(t *testing.T) (*cage.Envars, *mock_awsiface.MockEcsClient) { + setupForBasic := func(t *testing.T) (*cage.Envars, + *test.MockContext, + *mock_awsiface.MockEcsClient) { env := test.DefaultEnvars() mocker := test.NewMockContext() ctrl := gomock.NewController(t) ecsMock := mock_awsiface.NewMockEcsClient(ctrl) ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTaskDefinition).AnyTimes() - ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks).AnyTimes() - return env, ecsMock + return env, mocker, ecsMock } t.Run("basic", func(t *testing.T) { overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - env, ecsMock := setupForBasic(t) - ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, input *ecs.RunTaskInput, optFns ...func(*ecs.Options)) (*ecs.RunTaskOutput, error) { - task, err := mocker.RunTask(ctx, input) - if err != nil { - return nil, err - } - stop, err := mocker.StopTask(ctx, &ecs.StopTaskInput{Cluster: input.Cluster, Task: task.Tasks[0].TaskArn}) - if err != nil { - return nil, err - } - return &ecs.RunTaskOutput{Tasks: []ecstypes.Task{*stop.Task}}, nil - }, - ).AnyTimes() + env, mocker, ecsMock := setupForBasic(t) + gomock.InOrder( + ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, input *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + mocker.StopTask(ctx, &ecs.StopTaskInput{Cluster: &env.Cluster, Task: &input.Tasks[0]}) + return mocker.DescribeTasks(ctx, input) + }), + ) cagecli := cage.NewCage(&cage.Input{ Env: env, ECS: ecsMock, @@ -56,44 +52,77 @@ func TestCage_Run(t *testing.T) { assert.NoError(t, err) assert.Equal(t, result.ExitCode, int32(0)) }) - t.Run("should error if max attempts exceeded", func(t *testing.T) { + t.Run("should error if task failed to start", func(t *testing.T) { overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - env, ecsMock := setupForBasic(t) - ecsMock.EXPECT().DescribeTasks(ctx, gomock.Any()).AnyTimes().Return(&ecs.DescribeTasksOutput{ - Tasks: []ecstypes.Task{ - {LastStatus: aws.String("RUNNING"), - Containers: []ecstypes.Container{{ - Name: &container, - ExitCode: nil, - }}}, - }, - }, nil) + env, mocker, ecsMock := setupForBasic(t) + gomock.InOrder( + ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + res, err := mocker.DescribeTasks(ctx, input) + for i := range res.Tasks { + res.Tasks[i].LastStatus = aws.String("PROVISIONING") + } + return res, err + }, + ), + ) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + Time: test.NewFakeTime(), + }) + result, err := cagecli.Run(ctx, &cage.RunInput{ + Container: &container, + Overrides: overrides, + MaxWait: 1, + }) + assert.Nil(t, result) + assert.EqualError(t, err, "task failed to start: exceeded max wait time for TasksRunning waiter") + }) + t.Run("should error if task failed to stop", func(t *testing.T) { + overrides := &ecstypes.TaskOverride{} + container := "container" + ctx := context.Background() + env, mocker, ecsMock := setupForBasic(t) + gomock.InOrder( + ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks).Times(2), + ) cagecli := cage.NewCage(&cage.Input{ Env: env, ECS: ecsMock, - ALB: nil, - EC2: nil, Time: test.NewFakeTime(), }) result, err := cagecli.Run(ctx, &cage.RunInput{ Container: &container, Overrides: overrides, + MaxWait: 1, }) assert.Nil(t, result) - assert.EqualError(t, err, "๐Ÿšซ max attempts exceeded") + assert.EqualError(t, err, "task failed to stop: exceeded max wait time for TasksStopped waiter") }) t.Run("should error if exit code was not 0", func(t *testing.T) { overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - env, ecsMock := setupForBasic(t) + env, mocker, ecsMock := setupForBasic(t) + gomock.InOrder( + ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, input *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + stop, _ := mocker.StopTask(ctx, &ecs.StopTaskInput{Cluster: &env.Cluster, Task: &input.Tasks[0]}) + for i := range stop.Task.Containers { + stop.Task.Containers[i].ExitCode = aws.Int32(1) + } + return mocker.DescribeTasks(ctx, input) + }), + ) cagecli := cage.NewCage(&cage.Input{ Env: env, ECS: ecsMock, - ALB: nil, - EC2: nil, Time: test.NewFakeTime(), }) result, err := cagecli.Run(ctx, &cage.RunInput{ @@ -101,18 +130,27 @@ func TestCage_Run(t *testing.T) { Overrides: overrides, }) assert.Nil(t, result) - assert.EqualError(t, err, "๐Ÿšซ task exited with 1") + assert.EqualError(t, err, "task exited with 1") }) t.Run("should error if exit code is nil", func(t *testing.T) { overrides := &ecstypes.TaskOverride{} container := "container" ctx := context.Background() - env, ecsMock := setupForBasic(t) + env, mocker, ecsMock := setupForBasic(t) + gomock.InOrder( + ecsMock.EXPECT().RunTask(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RunTask), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTasks), + ecsMock.EXPECT().DescribeTasks(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, input *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + stop, _ := mocker.StopTask(ctx, &ecs.StopTaskInput{Cluster: &env.Cluster, Task: &input.Tasks[0]}) + for i := range stop.Task.Containers { + stop.Task.Containers[i].ExitCode = nil + } + return mocker.DescribeTasks(ctx, input) + }), + ) cagecli := cage.NewCage(&cage.Input{ Env: env, ECS: ecsMock, - ALB: nil, - EC2: nil, Time: test.NewFakeTime(), }) result, err := cagecli.Run(ctx, &cage.RunInput{ @@ -120,22 +158,12 @@ func TestCage_Run(t *testing.T) { Overrides: overrides, }) assert.Nil(t, result) - assert.EqualError(t, err, "๐Ÿšซ container 'container' hasn't exit") + assert.EqualError(t, err, "container 'container' hasn't exit") }) t.Run("should error if container doesn't exist in definition", func(t *testing.T) { overrides := &ecstypes.TaskOverride{} - container := "container" ctx := context.Background() - env, ecsMock := setupForBasic(t) - td := &ecs.RegisterTaskDefinitionOutput{ - TaskDefinition: &ecstypes.TaskDefinition{ - ContainerDefinitions: []ecstypes.ContainerDefinition{ - {Name: &container}, - }, - }, - } - - ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(td, nil) + env, _, ecsMock := setupForBasic(t) cagecli := cage.NewCage(&cage.Input{ Env: env, ECS: ecsMock, From 296dcfbe398d920c7861f4304deb0d17573be3aa Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 20:18:42 +0900 Subject: [PATCH 03/11] add td tests --- cage.go | 20 +++---- export_test.go | 3 ++ recreate.go | 44 +++++++-------- rollout.go | 116 ++++++++++++++++++++-------------------- run.go | 18 +++---- task_definition.go | 19 +++---- task_definition_test.go | 100 ++++++++++++++++++++++++++-------- up.go | 14 ++--- 8 files changed, 196 insertions(+), 138 deletions(-) create mode 100644 export_test.go diff --git a/cage.go b/cage.go index 30729a4..52fd840 100644 --- a/cage.go +++ b/cage.go @@ -20,11 +20,11 @@ type Time interface { } type cage struct { - env *Envars - ecs awsiface.EcsClient - alb awsiface.AlbClient - ec2 awsiface.Ec2Client - time Time + Env *Envars + Ecs awsiface.EcsClient + Alb awsiface.AlbClient + Ec2 awsiface.Ec2Client + Time Time } type Input struct { @@ -40,10 +40,10 @@ func NewCage(input *Input) Cage { input.Time = &timeImpl{} } return &cage{ - env: input.Env, - ecs: input.ECS, - alb: input.ALB, - ec2: input.EC2, - time: input.Time, + Env: input.Env, + Ecs: input.ECS, + Alb: input.ALB, + Ec2: input.EC2, + Time: input.Time, } } diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..901fd27 --- /dev/null +++ b/export_test.go @@ -0,0 +1,3 @@ +package cage + +type CageExport = cage diff --git a/recreate.go b/recreate.go index 4cb5f07..df64968 100644 --- a/recreate.go +++ b/recreate.go @@ -19,21 +19,21 @@ type RecreateResult struct { func (c *cage) Recreate(ctx context.Context) (*RecreateResult, error) { // Check if the service already exists - log.Infof("checking existence of service '%s'", c.env.Service) + log.Infof("checking existence of service '%s'", c.Env.Service) var oldService *ecstypes.Service var transitService *ecstypes.Service var newService *ecstypes.Service - if o, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, - Services: []string{c.env.Service}, + if o, err := c.Ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, + Services: []string{c.Env.Service}, }); err != nil { return nil, xerrors.Errorf("couldn't describe service: %w", err) } else if len(o.Services) == 0 { - return nil, fmt.Errorf("service '%s' does not exist. Use 'cage up' instead", c.env.Service) + return nil, fmt.Errorf("service '%s' does not exist. Use 'cage up' instead", c.Env.Service) } else { oldService = &o.Services[0] if *oldService.Status == "INACTIVE" { - return nil, fmt.Errorf("service '%s' is already INACTIVE. Use 'cage up' instead", c.env.Service) + return nil, fmt.Errorf("service '%s' is already INACTIVE. Use 'cage up' instead", c.Env.Service) } } var err error @@ -42,11 +42,11 @@ func (c *cage) Recreate(ctx context.Context) (*RecreateResult, error) { if err != nil { return nil, err } - transitServiceName := fmt.Sprintf("%s-%d", *oldService.ServiceName, c.time.Now().Unix()) - newServiceInput := *c.env.ServiceDefinitionInput + transitServiceName := fmt.Sprintf("%s-%d", *oldService.ServiceName, c.Time.Now().Unix()) + newServiceInput := *c.Env.ServiceDefinitionInput curDesiredCount := oldService.DesiredCount - c.env.ServiceDefinitionInput.TaskDefinition = td.TaskDefinitionArn - transitServiceDifinitonInput := *c.env.ServiceDefinitionInput + c.Env.ServiceDefinitionInput.TaskDefinition = td.TaskDefinitionArn + transitServiceDifinitonInput := *c.Env.ServiceDefinitionInput transitServiceDifinitonInput.ServiceName = &transitServiceName transitServiceDifinitonInput.DesiredCount = aws.Int32(1) // Create a transit service @@ -88,13 +88,13 @@ func (c *cage) Recreate(ctx context.Context) (*RecreateResult, error) { func (c *cage) createService(ctx context.Context, serviceDefinitionInput *ecs.CreateServiceInput) (*ecstypes.Service, error) { log.Infof("creating service '%s' with task-definition '%s'...", *serviceDefinitionInput.ServiceName, *serviceDefinitionInput.TaskDefinition) - o, err := c.ecs.CreateService(ctx, serviceDefinitionInput) + o, err := c.Ecs.CreateService(ctx, serviceDefinitionInput) if err != nil { return nil, fmt.Errorf("failed to create service '%s': %s", *serviceDefinitionInput.ServiceName, err.Error()) } log.Infof("waiting for service '%s' to be STABLE", *serviceDefinitionInput.ServiceName) - if err := ecs.NewServicesStableWaiter(c.ecs).Wait(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, + if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, Services: []string{*serviceDefinitionInput.ServiceName}, }, WaitDuration); err != nil { return nil, fmt.Errorf("failed to wait for service '%s' to be STABLE: %s", *serviceDefinitionInput.ServiceName, err.Error()) @@ -104,16 +104,16 @@ func (c *cage) createService(ctx context.Context, serviceDefinitionInput *ecs.Cr func (c *cage) updateServiceTaskCount(ctx context.Context, service string, count int32) error { log.Infof("updating service '%s' desired count to %d...", service, count) - if _, err := c.ecs.UpdateService(ctx, &ecs.UpdateServiceInput{ - Cluster: &c.env.Cluster, + if _, err := c.Ecs.UpdateService(ctx, &ecs.UpdateServiceInput{ + Cluster: &c.Env.Cluster, Service: &service, DesiredCount: &count, }); err != nil { return fmt.Errorf("failed to update service '%s': %w", service, err) } log.Infof("waiting for service '%s' to be STABLE", service) - if err := ecs.NewServicesStableWaiter(c.ecs).Wait(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, + if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, Services: []string{service}, }, WaitDuration); err != nil { return fmt.Errorf("failed to wait for service '%s' to be STABLE: %v", service, err) @@ -123,18 +123,18 @@ func (c *cage) updateServiceTaskCount(ctx context.Context, service string, count func (c *cage) deleteService(ctx context.Context, service string) error { log.Infof("deleting service '%s'...", service) - if _, err := c.ecs.DeleteService(ctx, &ecs.DeleteServiceInput{ - Cluster: &c.env.Cluster, + if _, err := c.Ecs.DeleteService(ctx, &ecs.DeleteServiceInput{ + Cluster: &c.Env.Cluster, Service: &service, }); err != nil { return fmt.Errorf("failed to delete service '%s': %w", service, err) } var retryCount int = 0 for retryCount < 10 { - <-c.time.NewTimer(15 * time.Second).C + <-c.Time.NewTimer(15 * time.Second).C log.Infof("waiting for service '%s' to be INACTIVE", service) - if o, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, + if o, err := c.Ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, Services: []string{service}, }); err != nil { return fmt.Errorf("failed to describe service '%s': %w", service, err) diff --git a/rollout.go b/rollout.go index 67b5ffa..28a5072 100644 --- a/rollout.go +++ b/rollout.go @@ -25,36 +25,36 @@ var WaitDuration = 15 * time.Minute func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { ret := &RollOutResult{ - StartTime: c.time.Now(), + StartTime: c.Time.Now(), ServiceIntact: true, } var aggregatedError error throw := func(err error) (*RollOutResult, error) { - ret.EndTime = c.time.Now() + ret.EndTime = c.Time.Now() aggregatedError = err return ret, err } defer func(result *RollOutResult) { - ret.EndTime = c.time.Now() + ret.EndTime = c.Time.Now() }(ret) var service ecstypes.Service - if out, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, + if out, err := c.Ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, Services: []string{ - c.env.Service, + c.Env.Service, }, }); err != nil { log.Errorf("failed to describe current service due to: %s", err.Error()) return throw(err) } else if len(out.Services) == 0 { - return throw(fmt.Errorf("service '%s' doesn't exist. Run 'cage up' or create service before rolling out", c.env.Service)) + return throw(fmt.Errorf("service '%s' doesn't exist. Run 'cage up' or create service before rolling out", c.Env.Service)) } else { service = out.Services[0] } if *service.Status != "ACTIVE" { - return throw(fmt.Errorf("๐Ÿ˜ต '%s' status is '%s'. Stop rolling out", c.env.Service, *service.Status)) + return throw(fmt.Errorf("๐Ÿ˜ต '%s' status is '%s'. Stop rolling out", c.Env.Service, *service.Status)) } - if service.LaunchType == ecstypes.LaunchTypeEc2 && c.env.CanaryInstanceArn == "" { + if service.LaunchType == ecstypes.LaunchTypeEc2 && c.Env.CanaryInstanceArn == "" { return throw(fmt.Errorf("๐Ÿฅบ --canaryInstanceArn is required when LaunchType = 'EC2'")) } var ( @@ -88,7 +88,7 @@ func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { if aggregatedError == nil { log.Infof( "๐Ÿฅ service '%s' successfully rolled out to '%s:%d'!", - c.env.Service, *nextTaskDefinition.Family, nextTaskDefinition.Revision, + c.Env.Service, *nextTaskDefinition.Family, nextTaskDefinition.Revision, ) } else { log.Errorf( @@ -120,26 +120,26 @@ func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { ret.ServiceIntact = false log.Infof( "updating the task definition of '%s' into '%s:%d'...", - c.env.Service, *nextTaskDefinition.Family, nextTaskDefinition.Revision, + c.Env.Service, *nextTaskDefinition.Family, nextTaskDefinition.Revision, ) - if _, err := c.ecs.UpdateService(ctx, &ecs.UpdateServiceInput{ - Cluster: &c.env.Cluster, - Service: &c.env.Service, + if _, err := c.Ecs.UpdateService(ctx, &ecs.UpdateServiceInput{ + Cluster: &c.Env.Cluster, + Service: &c.Env.Service, TaskDefinition: nextTaskDefinition.TaskDefinitionArn, }); err != nil { return throw(err) } - log.Infof("waiting for service '%s' to be stable...", c.env.Service) + log.Infof("waiting for service '%s' to be stable...", c.Env.Service) //TODO: avoid stdout sticking while CI - if err := ecs.NewServicesStableWaiter(c.ecs).Wait(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, - Services: []string{c.env.Service}, + if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, + Services: []string{c.Env.Service}, }, WaitDuration); err != nil { return throw(err) } - log.Infof("๐Ÿฅด service '%s' has become to be stable!", c.env.Service) - ret.EndTime = c.time.Now() + log.Infof("๐Ÿฅด service '%s' has become to be stable!", c.Env.Service) + ret.EndTime = c.Time.Now() return ret, nil } @@ -155,8 +155,8 @@ func (c *cage) EnsureTaskHealthy( var initialized = false var recentState *elbv2types.TargetHealthStateEnum for { - <-c.time.NewTimer(time.Duration(15) * time.Second).C - if o, err := c.alb.DescribeTargetHealth(ctx, &elbv2.DescribeTargetHealthInput{ + <-c.Time.NewTimer(time.Duration(15) * time.Second).C + if o, err := c.Alb.DescribeTargetHealth(ctx, &elbv2.DescribeTargetHealthInput{ TargetGroupArn: tgArn, Targets: []elbv2types.TargetDescription{{ Id: targetId, @@ -204,7 +204,7 @@ func GetTargetIsHealthy(o *elbv2.DescribeTargetHealthOutput, targetId *string, t } func (c *cage) DescribeSubnet(ctx context.Context, subnetId *string) (ec2types.Subnet, error) { - if o, err := c.ec2.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{ + if o, err := c.Ec2.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{ SubnetIds: []string{*subnetId}, }); err != nil { return ec2types.Subnet{}, err @@ -224,34 +224,34 @@ type StartCanaryTaskOutput struct { func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes.TaskDefinition) (*StartCanaryTaskOutput, error) { var service ecstypes.Service - if o, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, - Services: []string{c.env.Service}, + if o, err := c.Ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, + Services: []string{c.Env.Service}, }); err != nil { return nil, err } else { service = o.Services[0] } var taskArn *string - if c.env.CanaryInstanceArn != "" { + if c.Env.CanaryInstanceArn != "" { // ec2 startTask := &ecs.StartTaskInput{ - Cluster: &c.env.Cluster, - Group: aws.String(fmt.Sprintf("cage:canary-task:%s", c.env.Service)), + Cluster: &c.Env.Cluster, + Group: aws.String(fmt.Sprintf("cage:canary-task:%s", c.Env.Service)), NetworkConfiguration: service.NetworkConfiguration, TaskDefinition: nextTaskDefinition.TaskDefinitionArn, - ContainerInstances: []string{c.env.CanaryInstanceArn}, + ContainerInstances: []string{c.Env.CanaryInstanceArn}, } - if o, err := c.ecs.StartTask(ctx, startTask); err != nil { + if o, err := c.Ecs.StartTask(ctx, startTask); err != nil { return nil, err } else { taskArn = o.Tasks[0].TaskArn } } else { // fargate - if o, err := c.ecs.RunTask(ctx, &ecs.RunTaskInput{ - Cluster: &c.env.Cluster, - Group: aws.String(fmt.Sprintf("cage:canary-task:%s", c.env.Service)), + if o, err := c.Ecs.RunTask(ctx, &ecs.RunTaskInput{ + Cluster: &c.Env.Cluster, + Group: aws.String(fmt.Sprintf("cage:canary-task:%s", c.Env.Service)), NetworkConfiguration: service.NetworkConfiguration, TaskDefinition: nextTaskDefinition.TaskDefinitionArn, LaunchType: ecstypes.LaunchTypeFargate, @@ -263,16 +263,16 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes } } log.Infof("๐Ÿฅš waiting for canary task '%s' is running...", *taskArn) - if err := ecs.NewTasksRunningWaiter(c.ecs).Wait(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, + if err := ecs.NewTasksRunningWaiter(c.Ecs).Wait(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.Env.Cluster, Tasks: []string{*taskArn}, }, WaitDuration); err != nil { return nil, err } log.Infof("๐Ÿฃ canary task '%s' is running!๏ธ", *taskArn) var task ecstypes.Task - if o, err := c.ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, + if o, err := c.Ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.Env.Cluster, Tasks: []string{*taskArn}, }); err != nil { return nil, err @@ -281,24 +281,24 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes } if len(service.LoadBalancers) == 0 { log.Infof("no load balancer is attached to service '%s'. skip registration to target group", *service.ServiceName) - log.Infof("wait %d seconds for ensuring the task goes stable", c.env.CanaryTaskIdleDuration) + log.Infof("wait %d seconds for ensuring the task goes stable", c.Env.CanaryTaskIdleDuration) wait := make(chan bool) go func() { - duration := c.env.CanaryTaskIdleDuration + duration := c.Env.CanaryTaskIdleDuration for duration > 0 { log.Infof("still waiting...; %d seconds left", duration) wt := 10 if duration < 10 { wt = duration } - <-c.time.NewTimer(time.Duration(wt) * time.Second).C + <-c.Time.NewTimer(time.Duration(wt) * time.Second).C duration -= 10 } wait <- true }() <-wait - o, err := c.ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, + o, err := c.Ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.Env.Cluster, Tasks: []string{*taskArn}, }) if err != nil { @@ -332,7 +332,7 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes privateIp = v.Value } } - if o, err := c.ec2.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{ + if o, err := c.Ec2.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{ SubnetIds: []string{*subnetId}, }); err != nil { return nil, err @@ -343,15 +343,15 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes log.Infof("canary task was placed: privateIp = '%s', hostPort = '%d', az = '%s'", *targetId, *targetPort, *subnet.AvailabilityZone) } else { var containerInstance ecstypes.ContainerInstance - if outputs, err := c.ecs.DescribeContainerInstances(ctx, &ecs.DescribeContainerInstancesInput{ - Cluster: &c.env.Cluster, - ContainerInstances: []string{c.env.CanaryInstanceArn}, + if outputs, err := c.Ecs.DescribeContainerInstances(ctx, &ecs.DescribeContainerInstancesInput{ + Cluster: &c.Env.Cluster, + ContainerInstances: []string{c.Env.CanaryInstanceArn}, }); err != nil { return nil, err } else { containerInstance = outputs.ContainerInstances[0] } - if o, err := c.ec2.DescribeInstances(ctx, &ec2.DescribeInstancesInput{ + if o, err := c.Ec2.DescribeInstances(ctx, &ec2.DescribeInstancesInput{ InstanceIds: []string{*containerInstance.Ec2InstanceId}, }); err != nil { return nil, err @@ -363,7 +363,7 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes } log.Infof("canary task was placed: instanceId = '%s', hostPort = '%d', az = '%s'", *targetId, *targetPort, *subnet.AvailabilityZone) } - if _, err := c.alb.RegisterTargets(ctx, &elbv2.RegisterTargetsInput{ + if _, err := c.Alb.RegisterTargets(ctx, &elbv2.RegisterTargetsInput{ TargetGroupArn: service.LoadBalancers[0].TargetGroupArn, Targets: []elbv2types.TargetDescription{{ AvailabilityZone: subnet.AvailabilityZone, @@ -390,10 +390,10 @@ func (c *cage) waitUntilContainersBecomeHealthy(ctx context.Context, taskArn str } for count := 0; count < 10; count++ { - <-c.time.NewTimer(time.Duration(15) * time.Second).C + <-c.Time.NewTimer(time.Duration(15) * time.Second).C log.Infof("canary task '%s' waits until %d container(s) become healthy", taskArn, len(containerHasHealthChecks)) - if o, err := c.ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, + if o, err := c.Ecs.DescribeTasks(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.Env.Cluster, Tasks: []string{taskArn}, }); err != nil { return err @@ -426,7 +426,7 @@ func (c *cage) StopCanaryTask(ctx context.Context, input *StartCanaryTaskOutput) log.Info("no load balancer is attached to service. Skip deregisteration.") } else { log.Infof("deregistering the canary task from target group '%s'...", *input.targetId) - if _, err := c.alb.DeregisterTargets(ctx, &elbv2.DeregisterTargetsInput{ + if _, err := c.Alb.DeregisterTargets(ctx, &elbv2.DeregisterTargetsInput{ TargetGroupArn: input.targetGroupArn, Targets: []elbv2types.TargetDescription{{ AvailabilityZone: input.availabilityZone, @@ -436,7 +436,7 @@ func (c *cage) StopCanaryTask(ctx context.Context, input *StartCanaryTaskOutput) }); err != nil { return err } - if err := elbv2.NewTargetDeregisteredWaiter(c.alb).Wait(ctx, &elbv2.DescribeTargetHealthInput{ + if err := elbv2.NewTargetDeregisteredWaiter(c.Alb).Wait(ctx, &elbv2.DescribeTargetHealthInput{ TargetGroupArn: input.targetGroupArn, Targets: []elbv2types.TargetDescription{{ AvailabilityZone: input.availabilityZone, @@ -453,14 +453,14 @@ func (c *cage) StopCanaryTask(ctx context.Context, input *StartCanaryTaskOutput) } log.Infof("stopping the canary task '%s'...", *input.task.TaskArn) - if _, err := c.ecs.StopTask(ctx, &ecs.StopTaskInput{ - Cluster: &c.env.Cluster, + if _, err := c.Ecs.StopTask(ctx, &ecs.StopTaskInput{ + Cluster: &c.Env.Cluster, Task: input.task.TaskArn, }); err != nil { return err } - if err := ecs.NewTasksStoppedWaiter(c.ecs).Wait(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, + if err := ecs.NewTasksStoppedWaiter(c.Ecs).Wait(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.Env.Cluster, Tasks: []string{*input.task.TaskArn}, }, WaitDuration); err != nil { return err diff --git a/run.go b/run.go index 2b792dd..53d47e5 100644 --- a/run.go +++ b/run.go @@ -34,19 +34,19 @@ func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { if input.MaxWait == 0 { input.MaxWait = 5 * time.Minute } - if !containerExistsInDefinition(c.env.TaskDefinitionInput, input.Container) { + if !containerExistsInDefinition(c.Env.TaskDefinitionInput, input.Container) { return nil, fmt.Errorf("๐Ÿšซ '%s' not found in container definitions", *input.Container) } td, err := c.CreateNextTaskDefinition(ctx) if err != nil { return nil, err } - o, err := c.ecs.RunTask(ctx, &ecs.RunTaskInput{ - Cluster: &c.env.Cluster, + o, err := c.Ecs.RunTask(ctx, &ecs.RunTaskInput{ + Cluster: &c.Env.Cluster, TaskDefinition: td.TaskDefinitionArn, LaunchType: types.LaunchTypeFargate, - NetworkConfiguration: c.env.ServiceDefinitionInput.NetworkConfiguration, - PlatformVersion: c.env.ServiceDefinitionInput.PlatformVersion, + NetworkConfiguration: c.Env.ServiceDefinitionInput.NetworkConfiguration, + PlatformVersion: c.Env.ServiceDefinitionInput.PlatformVersion, Overrides: input.Overrides, Group: aws.String("cage:run-task"), }) @@ -55,16 +55,16 @@ func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { } taskArn := o.Tasks[0].TaskArn log.Infof("waiting for task '%s' to start...", *taskArn) - if err := ecs.NewTasksRunningWaiter(c.ecs).Wait(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, + if err := ecs.NewTasksRunningWaiter(c.Ecs).Wait(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.Env.Cluster, Tasks: []string{*taskArn}, }, input.MaxWait); err != nil { return nil, xerrors.Errorf("task failed to start: %w", err) } log.Infof("task '%s' is running", *taskArn) log.Infof("waiting for task '%s' to stop...", *taskArn) - if result, err := ecs.NewTasksStoppedWaiter(c.ecs).WaitForOutput(ctx, &ecs.DescribeTasksInput{ - Cluster: &c.env.Cluster, + if result, err := ecs.NewTasksStoppedWaiter(c.Ecs).WaitForOutput(ctx, &ecs.DescribeTasksInput{ + Cluster: &c.Env.Cluster, Tasks: []string{*taskArn}, }, input.MaxWait); err != nil { return nil, xerrors.Errorf("task failed to stop: %w", err) diff --git a/task_definition.go b/task_definition.go index c4a919a..5e3820c 100644 --- a/task_definition.go +++ b/task_definition.go @@ -6,26 +6,23 @@ import ( "github.com/apex/log" "github.com/aws/aws-sdk-go-v2/service/ecs" ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "golang.org/x/xerrors" ) func (c *cage) CreateNextTaskDefinition(ctx context.Context) (*ecstypes.TaskDefinition, error) { - if c.env.TaskDefinitionArn != "" { - log.Infof("--taskDefinitionArn was set to '%s'. skip registering new task definition.", c.env.TaskDefinitionArn) - o, err := c.ecs.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{ - TaskDefinition: &c.env.TaskDefinitionArn, + if c.Env.TaskDefinitionArn != "" { + log.Infof("--taskDefinitionArn was set to '%s'. skip registering new task definition.", c.Env.TaskDefinitionArn) + o, err := c.Ecs.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{ + TaskDefinition: &c.Env.TaskDefinitionArn, }) if err != nil { - log.Errorf( - "failed to describe next task definition '%s' due to: %s", - c.env.TaskDefinitionArn, err, - ) - return nil, err + return nil, xerrors.Errorf("failed to describe next task definition: %w", err) } return o.TaskDefinition, nil } else { log.Infof("creating next task definition...") - if out, err := c.ecs.RegisterTaskDefinition(ctx, c.env.TaskDefinitionInput); err != nil { - return nil, err + if out, err := c.Ecs.RegisterTaskDefinition(ctx, c.Env.TaskDefinitionInput); err != nil { + return nil, xerrors.Errorf("failed to register task definition: %w", err) } else { log.Infof( "task definition '%s:%d' has been registered", diff --git a/task_definition_test.go b/task_definition_test.go index dc64f15..c7f447d 100644 --- a/task_definition_test.go +++ b/task_definition_test.go @@ -1,23 +1,81 @@ package cage_test -// func TestCage_CreateNextTaskDefinition(t *testing.T) { -// envars := &cage.Envars{ -// TaskDefinitionArn: "arn://task", -// } -// ctrl := gomock.NewController(t) -// e := mock_awsiface.NewMockEcsClient(ctrl) -// e.EXPECT().DescribeTaskDefinition(gomock.Any(), gomock.Any()).Return( -// &ecs.DescribeTaskDefinitionOutput{ -// TaskDefinition: &ecstypes.TaskDefinition{TaskDefinitionArn: aws.String("arn://task")}, -// }, nil) -// // nextTaskDefinitionArnใŒใ‚ใ‚‹ๅ ดๅˆใฏdescribeTaskDefinitionใ‹ใ‚‰่ฟ”ใ™ -// cagecli := cage.NewCage(&cage.Input{ -// Env: envars, -// ECS: e, -// }) -// o, err := cagecli.CreateNextTaskDefinition(context.Background()) -// if err != nil { -// t.Fatalf(err.Error()) -// } -// assert.Equal(t, envars.TaskDefinitionArn, *o.TaskDefinitionArn) -// } +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" + "github.com/loilo-inc/canarycage/mocks/mock_awsiface" + "github.com/loilo-inc/canarycage/test" + "github.com/stretchr/testify/assert" + "golang.org/x/xerrors" +) + +func TestCage_CreateNextTaskDefinition(t *testing.T) { + t.Run("should return task definition if taskDefinitionArn is set", func(t *testing.T) { + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + env := &cage.Envars{ + TaskDefinitionArn: "arn://aaa", + } + c := &cage.CageExport{ + Env: env, + Ecs: ecsMock, + } + ecsMock.EXPECT().DescribeTaskDefinition(gomock.Any(), gomock.Any()).Return(&ecs.DescribeTaskDefinitionOutput{ + TaskDefinition: &ecstypes.TaskDefinition{}, + }, nil) + td, err := c.CreateNextTaskDefinition(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, td) + }) + t.Run("should return error if taskDefinitionArn is set and failed to describe task definition", func(t *testing.T) { + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + env := &cage.Envars{ + TaskDefinitionArn: "arn://aaa", + } + c := &cage.CageExport{ + Env: env, + Ecs: ecsMock, + } + ecsMock.EXPECT().DescribeTaskDefinition(gomock.Any(), gomock.Any()).Return(nil, xerrors.New("error")) + td, err := c.CreateNextTaskDefinition(context.Background()) + assert.Errorf(t, err, "failed to describe next task definition: error") + assert.Nil(t, td) + }) + t.Run("should return task definition if taskDefinitionArn is not set", func(t *testing.T) { + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + env := test.DefaultEnvars() + c := &cage.CageExport{ + Env: env, + Ecs: ecsMock, + } + ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(&ecs.RegisterTaskDefinitionOutput{ + TaskDefinition: &ecstypes.TaskDefinition{ + Family: env.TaskDefinitionInput.Family, + Revision: 1, + }, + }, nil) + td, err := c.CreateNextTaskDefinition(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, td) + }) + t.Run("should return error if taskDefinitionArn is not set and failed to register task definition", func(t *testing.T) { + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + env := test.DefaultEnvars() + c := &cage.CageExport{ + Env: env, + Ecs: ecsMock, + } + ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(nil, xerrors.New("error")) + td, err := c.CreateNextTaskDefinition(context.Background()) + assert.Errorf(t, err, "failed to register task definition: error") + assert.Nil(t, td) + }) +} diff --git a/up.go b/up.go index c940301..6b60abb 100644 --- a/up.go +++ b/up.go @@ -19,20 +19,20 @@ func (c *cage) Up(ctx context.Context) (*UpResult, error) { if err != nil { return nil, err } - log.Infof("checking existence of service '%s'", c.env.Service) - if o, err := c.ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.env.Cluster, - Services: []string{c.env.Service}, + log.Infof("checking existence of service '%s'", c.Env.Service) + if o, err := c.Ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, + Services: []string{c.Env.Service}, }); err != nil { return nil, fmt.Errorf("couldn't describe service: %s", err.Error()) } else if len(o.Services) > 0 { svc := o.Services[0] if *svc.Status != "INACTIVE" { - return nil, fmt.Errorf("service '%s' already exists. Use 'cage rollout' instead", c.env.Service) + return nil, fmt.Errorf("service '%s' already exists. Use 'cage rollout' instead", c.Env.Service) } } - c.env.ServiceDefinitionInput.TaskDefinition = td.TaskDefinitionArn - if service, err := c.createService(ctx, c.env.ServiceDefinitionInput); err != nil { + c.Env.ServiceDefinitionInput.TaskDefinition = td.TaskDefinitionArn + if service, err := c.createService(ctx, c.Env.ServiceDefinitionInput); err != nil { return nil, err } else { return &UpResult{TaskDefinition: td, Service: service}, nil From 8d8357760f37de6fcbf53be3781f96b40b12fbb8 Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 20:21:19 +0900 Subject: [PATCH 04/11] Update push.yml --- .github/workflows/push.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 4b56b47..94b9e08 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -15,5 +15,9 @@ jobs: - run: go build - run: go build github.com/loilo-inc/canarycage/cli/cage - run: go test -coverprofile=coverage.txt -covermode=count - - name: Upload Coverage - run: bash <(curl -s https://codecov.io/bash) + - uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + fail_ci_if_error: true + files: coverage.txt From 1a1829ed82a4170b7ca662f8b8bf41a7277af43e Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 20:23:58 +0900 Subject: [PATCH 05/11] mod tidy --- .github/workflows/push.yml | 2 +- go.sum | 2 -- recreate.go | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 94b9e08..ea95939 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -14,7 +14,7 @@ jobs: git diff --exit-code - run: go build - run: go build github.com/loilo-inc/canarycage/cli/cage - - run: go test -coverprofile=coverage.txt -covermode=count + - run: go test ./... -coverprofile=coverage.txt -covermode=count - uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/go.sum b/go.sum index 3cc274a..a0c026d 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDw github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.53.10 h1:3enP5l5WtezT9Ql+XZqs56JBf5YUd/FEzTCg///OIGY= -github.com/aws/aws-sdk-go v1.53.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo= github.com/aws/aws-sdk-go-v2 v1.27.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/config v1.27.16 h1:knpCuH7laFVGYTNd99Ns5t+8PuRjDn4HnnZK48csipM= diff --git a/recreate.go b/recreate.go index df64968..1197019 100644 --- a/recreate.go +++ b/recreate.go @@ -6,9 +6,9 @@ import ( "time" "github.com/apex/log" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecs" ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/aws/aws-sdk-go/aws" "golang.org/x/xerrors" ) From c0a3b58994ae1f72152ba7281acf7a9d711953d4 Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 20:29:48 +0900 Subject: [PATCH 06/11] codecov --- cli/cage/main.go | 2 +- codecov.yml | 5 +++++ go.mod | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 codecov.yml diff --git a/cli/cage/main.go b/cli/cage/main.go index 66c5627..5b0340f 100644 --- a/cli/cage/main.go +++ b/cli/cage/main.go @@ -13,7 +13,7 @@ func main() { app := cli.NewApp() app.Name = "canarycage" app.Version = "3.7.0" - app.Description = "A gradual roll-out deployment tool for AWS ECS" + app.Description = "A deployment tool for AWS ECS" ctx := context.Background() cmds := commands.NewCageCommands(ctx, os.Stdin) app.Commands = cmds.Commands() diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..5a71d2b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +ignore: + - "test/*" + - "mocks/*" + - "cmd/cli/cage" + - "cmd/cli/cage/commands/*" diff --git a/go.mod b/go.mod index f230330..54f2985 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( ) require ( - github.com/aws/aws-sdk-go v1.53.10 github.com/aws/aws-sdk-go-v2/credentials v1.17.16 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 // indirect From 1d64454170e19634cffe0d13461559a668ed9591 Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 23:10:17 +0900 Subject: [PATCH 07/11] add commands tests --- cage.go | 1 + cli/cage/commands/command.go | 91 +++++++++++++++--- cli/cage/commands/command_test.go | 112 ++++++++++++++++++++++ cli/cage/commands/export_test.go | 3 + cli/cage/commands/flags.go | 32 +------ cli/cage/commands/recreate.go | 37 +++----- cli/cage/commands/rollout.go | 37 +++----- cli/cage/commands/run.go | 44 +++------ cli/cage/commands/up.go | 36 +++----- cli/cage/main.go | 22 +++-- codecov.yml | 1 - mocks/mock_cage/cage.go | 148 ++++++++++++++++++++++++++++++ test/setup.go | 26 +++--- 13 files changed, 424 insertions(+), 166 deletions(-) create mode 100644 cli/cage/commands/command_test.go create mode 100644 cli/cage/commands/export_test.go create mode 100644 mocks/mock_cage/cage.go diff --git a/cage.go b/cage.go index 52fd840..aa477f2 100644 --- a/cage.go +++ b/cage.go @@ -1,3 +1,4 @@ +//go:generate go run github.com/golang/mock/mockgen -source $GOFILE -destination ../mocks/mock_$GOPACKAGE/$GOFILE -package mock_$GOPACKAGE package cage import ( diff --git a/cli/cage/commands/command.go b/cli/cage/commands/command.go index 5b3ab15..42b8f11 100644 --- a/cli/cage/commands/command.go +++ b/cli/cage/commands/command.go @@ -4,30 +4,99 @@ import ( "context" "io" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ecs" + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" + cage "github.com/loilo-inc/canarycage" "github.com/loilo-inc/canarycage/cli/cage/prompt" "github.com/urfave/cli/v2" + "golang.org/x/xerrors" ) type CageCommands interface { - Commands() []*cli.Command + Commands( + envars *cage.Envars, + ) []*cli.Command } type cageCommands struct { - ctx context.Context - prompt *prompt.Prompter + Prompt *prompt.Prompter + cageCliProvier cageCliProvier } -func NewCageCommands(ctx context.Context, stdin io.Reader) CageCommands { - return &cageCommands{ctx: ctx, - prompt: prompt.NewPrompter(stdin), +func NewCageCommands( + stdin io.Reader, + cageCliProvier cageCliProvier, +) CageCommands { + return &cageCommands{ + Prompt: prompt.NewPrompter(stdin), + cageCliProvier: cageCliProvier, } } -func (c *cageCommands) Commands() []*cli.Command { +type cageCliProvier = func(envars *cage.Envars) (cage.Cage, error) + +func (c *cageCommands) Commands(envars *cage.Envars) []*cli.Command { return []*cli.Command{ - c.Up(), - c.RollOut(), - c.Run(), - c.Recreate(), + c.Up(envars), + c.RollOut(envars), + c.Run(envars), + c.Recreate(envars), + } +} + +func DefalutCageCliProvider(envars *cage.Envars) (cage.Cage, error) { + conf, err := config.LoadDefaultConfig( + context.Background(), + config.WithRegion(envars.Region)) + if err != nil { + return nil, xerrors.Errorf("failed to load aws config: %w", err) + } + cagecli := cage.NewCage(&cage.Input{ + Env: envars, + ECS: ecs.NewFromConfig(conf), + EC2: ec2.NewFromConfig(conf), + ALB: elasticloadbalancingv2.NewFromConfig(conf), + }) + return cagecli, nil +} + +func (c *cageCommands) requireArgs( + ctx *cli.Context, + minArgs int, + maxArgs int, +) (dir string, rest []string, err error) { + if ctx.NArg() < minArgs { + return "", nil, xerrors.Errorf("invalid number of arguments. expected at least %d", minArgs) + } else if ctx.NArg() > maxArgs { + return "", nil, xerrors.Errorf("invalid number of arguments. expected at most %d", maxArgs) + } + dir = ctx.Args().First() + rest = ctx.Args().Tail() + return +} + +func (c *cageCommands) setupCage( + envars *cage.Envars, + dir string, +) (cage.Cage, error) { + td, svc, err := cage.LoadDefinitionsFromFiles(dir) + if err != nil { + return nil, err + } + cage.MergeEnvars(envars, &cage.Envars{ + Cluster: *svc.Cluster, + Service: *svc.ServiceName, + TaskDefinitionInput: td, + ServiceDefinitionInput: svc, + }) + if err := cage.EnsureEnvars(envars); err != nil { + return nil, err + } + cagecli, err := c.cageCliProvier(envars) + if err != nil { + return nil, err } + return cagecli, nil } diff --git a/cli/cage/commands/command_test.go b/cli/cage/commands/command_test.go new file mode 100644 index 0000000..e173abe --- /dev/null +++ b/cli/cage/commands/command_test.go @@ -0,0 +1,112 @@ +package commands_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" + "github.com/loilo-inc/canarycage/cli/cage/commands" + "github.com/loilo-inc/canarycage/mocks/mock_cage" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +func TestCommands(t *testing.T) { + region := "ap-notheast-1" + cluster := "cluster" + service := "service" + stdinService := fmt.Sprintf("%s\n%s\n%s\n%s\n", region, cluster, service, "yes") + stdinTask := fmt.Sprintf("%s\n%s\n%s\n", region, cluster, "yes") + setup := func(t *testing.T, input string) (*cli.App, *mock_cage.MockCage) { + ctrl := gomock.NewController(t) + stdin := strings.NewReader(input) + cagecli := mock_cage.NewMockCage(ctrl) + app := cli.NewApp() + app.Commands = commands.NewCageCommands(stdin, func(envars *cage.Envars) (cage.Cage, error) { + return cagecli, nil + }).Commands(&cage.Envars{CI: input == ""}) + return app, cagecli + } + t.Run("rollout", func(t *testing.T) { + t.Run("basic", func(t *testing.T) { + app, cagecli := setup(t, stdinService) + cagecli.EXPECT().RollOut(gomock.Any()).Return(&cage.RollOutResult{}, nil) + err := app.Run([]string{"cage", "rollout", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.NoError(t, err) + }) + t.Run("basic/ci", func(t *testing.T) { + app, cagecli := setup(t, "") + cagecli.EXPECT().RollOut(gomock.Any()).Return(&cage.RollOutResult{}, nil) + err := app.Run([]string{"cage", "rollout", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.NoError(t, err) + }) + t.Run("error", func(t *testing.T) { + app, cagecli := setup(t, stdinService) + cagecli.EXPECT().RollOut(gomock.Any()).Return(&cage.RollOutResult{}, fmt.Errorf("error")) + err := app.Run([]string{"cage", "rollout", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.EqualError(t, err, "error") + }) + }) + t.Run("recreate", func(t *testing.T) { + t.Run("basic", func(t *testing.T) { + app, cagecli := setup(t, stdinService) + cagecli.EXPECT().Recreate(gomock.Any()).Return(&cage.RecreateResult{}, nil) + err := app.Run([]string{"cage", "recreate", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.NoError(t, err) + }) + t.Run("basic/ci", func(t *testing.T) { + app, cagecli := setup(t, "") + cagecli.EXPECT().Recreate(gomock.Any()).Return(&cage.RecreateResult{}, nil) + err := app.Run([]string{"cage", "recreate", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.NoError(t, err) + }) + t.Run("error", func(t *testing.T) { + app, cagecli := setup(t, stdinService) + cagecli.EXPECT().Recreate(gomock.Any()).Return(nil, fmt.Errorf("error")) + err := app.Run([]string{"cage", "recreate", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.EqualError(t, err, "error") + }) + }) + t.Run("up", func(t *testing.T) { + t.Run("basic", func(t *testing.T) { + app, cagecli := setup(t, stdinService) + cagecli.EXPECT().Up(gomock.Any()).Return(&cage.UpResult{}, nil) + err := app.Run([]string{"cage", "up", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.NoError(t, err) + }) + t.Run("basic/ci", func(t *testing.T) { + app, cagecli := setup(t, "") + cagecli.EXPECT().Up(gomock.Any()).Return(&cage.UpResult{}, nil) + err := app.Run([]string{"cage", "up", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.NoError(t, err) + }) + t.Run("error", func(t *testing.T) { + app, cagecli := setup(t, stdinService) + cagecli.EXPECT().Up(gomock.Any()).Return(nil, fmt.Errorf("error")) + err := app.Run([]string{"cage", "up", "--region", "ap-notheast-1", "../../../fixtures"}) + assert.EqualError(t, err, "error") + }) + }) + t.Run("run", func(t *testing.T) { + t.Run("basic", func(t *testing.T) { + app, cagecli := setup(t, stdinTask) + cagecli.EXPECT().Run(gomock.Any(), gomock.Any()).Return(&cage.RunResult{}, nil) + err := app.Run([]string{"cage", "run", "--region", "ap-notheast-1", "../../../fixtures", "container", "exec"}) + assert.NoError(t, err) + }) + t.Run("basic/ci", func(t *testing.T) { + app, cagecli := setup(t, "") + cagecli.EXPECT().Run(gomock.Any(), gomock.Any()).Return(&cage.RunResult{}, nil) + err := app.Run([]string{"cage", "run", "--region", "ap-notheast-1", "../../../fixtures", "container", "exec"}) + assert.NoError(t, err) + }) + t.Run("error", func(t *testing.T) { + app, cagecli := setup(t, stdinTask) + cagecli.EXPECT().Run(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")) + err := app.Run([]string{"cage", "run", "--region", "ap-notheast-1", "../../../fixtures", "container", "exec"}) + assert.EqualError(t, err, "error") + }) + }) +} diff --git a/cli/cage/commands/export_test.go b/cli/cage/commands/export_test.go new file mode 100644 index 0000000..09154d8 --- /dev/null +++ b/cli/cage/commands/export_test.go @@ -0,0 +1,3 @@ +package commands + +type CommandsExport = cageCommands diff --git a/cli/cage/commands/flags.go b/cli/cage/commands/flags.go index 9b95028..78e87b1 100644 --- a/cli/cage/commands/flags.go +++ b/cli/cage/commands/flags.go @@ -1,9 +1,6 @@ package commands import ( - "os" - - "github.com/apex/log" cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) @@ -14,6 +11,7 @@ func RegionFlag(dest *string) *cli.StringFlag { EnvVars: []string{cage.RegionKey}, Usage: "aws region for ecs. if not specified, try to load from aws sessions automatically", Destination: dest, + Required: true, } } func ClusterFlag(dest *string) *cli.StringFlag { @@ -50,31 +48,3 @@ func CanaryTaskIdleDurationFlag(dest *int) *cli.IntFlag { Value: 10, } } - -func (c *cageCommands) aggregateEnvars( - ctx *cli.Context, - envars *cage.Envars, -) { - if envars.Region != "" { - log.Infof("๐Ÿ—บ region was set: %s", envars.Region) - } else { - log.Fatalf("๐Ÿ™„ region must specified by --region flag or aws session") - } - envars.CI = os.Getenv("CI") == "true" - if ctx.NArg() > 0 { - dir := ctx.Args().Get(0) - td, svc, err := cage.LoadDefinitionsFromFiles(dir) - if err != nil { - log.Fatalf(err.Error()) - } - cage.MergeEnvars(envars, &cage.Envars{ - Cluster: *svc.Cluster, - Service: *svc.ServiceName, - TaskDefinitionInput: td, - ServiceDefinitionInput: svc, - }) - } - if err := cage.EnsureEnvars(envars); err != nil { - log.Fatalf(err.Error()) - } -} diff --git a/cli/cage/commands/recreate.go b/cli/cage/commands/recreate.go index ee4414a..96c2aa1 100644 --- a/cli/cage/commands/recreate.go +++ b/cli/cage/commands/recreate.go @@ -3,23 +3,19 @@ package commands import ( "context" - "github.com/apex/log" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/aws/aws-sdk-go-v2/service/ecs" - elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) -func (c *cageCommands) Recreate() *cli.Command { - envars := cage.Envars{} +func (c *cageCommands) Recreate( + envars *cage.Envars, +) *cli.Command { return &cli.Command{ Name: "recreate", Usage: "recreate ECS service with specified service/task definition", Description: "recreate ECS service with specified service/task definition", - ArgsUsage: "[directory path of service.json and task-definition.json (default=.)]", + Args: true, + ArgsUsage: "[directory path of service.json and task-definition.json]", Flags: []cli.Flag{ RegionFlag(&envars.Region), ClusterFlag(&envars.Cluster), @@ -28,27 +24,18 @@ func (c *cageCommands) Recreate() *cli.Command { CanaryTaskIdleDurationFlag(&envars.CanaryTaskIdleDuration), }, Action: func(ctx *cli.Context) error { - c.aggregateEnvars(ctx, &envars) - - if err := c.prompt.ConfirmService(&envars); err != nil { + dir, _, err := c.requireArgs(ctx, 1, 1) + if err != nil { return err } - var cfg aws.Config - if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { + cagecli, err := c.setupCage(envars, dir) + if err != nil { return err - } else { - cfg = o } - cagecli := cage.NewCage(&cage.Input{ - Env: &envars, - ECS: ecs.NewFromConfig(cfg), - EC2: ec2.NewFromConfig(cfg), - ALB: elbv2.NewFromConfig(cfg), - }) - _, err := cagecli.Recreate(context.Background()) - if err != nil { - log.Error(err.Error()) + if err := c.Prompt.ConfirmService(envars); err != nil { + return err } + _, err = cagecli.Recreate(context.Background()) return err }, } diff --git a/cli/cage/commands/rollout.go b/cli/cage/commands/rollout.go index 7bae253..d1033d5 100644 --- a/cli/cage/commands/rollout.go +++ b/cli/cage/commands/rollout.go @@ -4,22 +4,19 @@ import ( "context" "github.com/apex/log" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/aws/aws-sdk-go-v2/service/ecs" - elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) -func (c *cageCommands) RollOut() *cli.Command { - var envars = cage.Envars{} +func (c *cageCommands) RollOut( + envars *cage.Envars, +) *cli.Command { return &cli.Command{ Name: "rollout", Usage: "roll out ECS service to next task definition", Description: "start rolling out next service with current service", - ArgsUsage: "[directory path of service.json and task-definition.json (default=.)]", + Args: true, + ArgsUsage: "[directory path of service.json and task-definition.json]", Flags: []cli.Flag{ RegionFlag(&envars.Region), ClusterFlag(&envars.Cluster), @@ -34,29 +31,23 @@ func (c *cageCommands) RollOut() *cli.Command { }, }, Action: func(ctx *cli.Context) error { - c.aggregateEnvars(ctx, &envars) - if err := c.prompt.ConfirmService(&envars); err != nil { + dir, _, err := c.requireArgs(ctx, 1, 1) + if err != nil { + return err + } + cagecli, err := c.setupCage(envars, dir) + if err != nil { return err } - var cfg aws.Config - if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { + if err := c.Prompt.ConfirmService(envars); err != nil { return err - } else { - cfg = o } - cagecli := cage.NewCage(&cage.Input{ - Env: &envars, - ECS: ecs.NewFromConfig(cfg), - EC2: ec2.NewFromConfig(cfg), - ALB: elbv2.NewFromConfig(cfg), - }) - result, err := cagecli.RollOut(c.ctx) + result, err := cagecli.RollOut(context.Background()) if err != nil { - log.Error(err.Error()) if result.ServiceIntact { log.Errorf("๐Ÿค• failed to roll out new tasks but service '%s' is not changed", envars.Service) } else { - log.Errorf("๐Ÿ˜ญ failed to roll out new tasks and service '%s' might be changed. check in console!!", envars.Service) + log.Errorf("๐Ÿ˜ญ failed to roll out new tasks and service '%s' might be changed. CHECK ECS CONSOLE NOW!", envars.Service) } return err } diff --git a/cli/cage/commands/run.go b/cli/cage/commands/run.go index 2e3769f..0a1846b 100644 --- a/cli/cage/commands/run.go +++ b/cli/cage/commands/run.go @@ -2,59 +2,41 @@ package commands import ( "context" - "fmt" "github.com/apex/log" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/aws/aws-sdk-go-v2/service/ecs" ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" - elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) -func (c *cageCommands) Run() *cli.Command { - envars := cage.Envars{} +func (c *cageCommands) Run( + envars *cage.Envars, +) *cli.Command { return &cli.Command{ Name: "run", Usage: "run task with specified task definition", Description: "run task with specified task definition", + Args: true, ArgsUsage: " ...", Flags: []cli.Flag{ RegionFlag(&envars.Region), ClusterFlag(&envars.Cluster), }, Action: func(ctx *cli.Context) error { - c.aggregateEnvars(ctx, &envars) - if err := c.prompt.ConfirmTask(&envars); err != nil { + dir, rest, err := c.requireArgs(ctx, 3, 100) + if err != nil { return err } - var cfg aws.Config - if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { + cagecli, err := c.setupCage(envars, dir) + if err != nil { return err - } else { - cfg = o - } - rest := ctx.Args().Tail() - if len(rest) < 1 { - log.Error(" required") - return fmt.Errorf("") } - if len(rest) < 2 { - log.Errorf(" required") - return fmt.Errorf("") + if err := c.Prompt.ConfirmTask(envars); err != nil { + return err } container := rest[0] commands := rest[1:] - cagecli := cage.NewCage(&cage.Input{ - Env: &envars, - ECS: ecs.NewFromConfig(cfg), - EC2: ec2.NewFromConfig(cfg), - ALB: elbv2.NewFromConfig(cfg), - }) - _, err := cagecli.Run(context.Background(), &cage.RunInput{ + if _, err := cagecli.Run(context.Background(), &cage.RunInput{ Container: &container, Overrides: &ecstypes.TaskOverride{ ContainerOverrides: []ecstypes.ContainerOverride{ @@ -62,9 +44,7 @@ func (c *cageCommands) Run() *cli.Command { Name: &container}, }, }, - }) - if err != nil { - log.Error(err.Error()) + }); err != nil { return err } log.Infof("๐Ÿ‘ task successfully executed") diff --git a/cli/cage/commands/up.go b/cli/cage/commands/up.go index 1f5b251..9429815 100644 --- a/cli/cage/commands/up.go +++ b/cli/cage/commands/up.go @@ -3,23 +3,19 @@ package commands import ( "context" - "github.com/apex/log" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/aws/aws-sdk-go-v2/service/ecs" - elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" cage "github.com/loilo-inc/canarycage" "github.com/urfave/cli/v2" ) -func (c *cageCommands) Up() *cli.Command { - envars := cage.Envars{} +func (c *cageCommands) Up( + envars *cage.Envars, +) *cli.Command { return &cli.Command{ Name: "up", Usage: "create new ECS service with specified task definition", Description: "create new ECS service with specified task definition", - ArgsUsage: "[directory path of service.json and task-definition.json (default=.)]", + Args: true, + ArgsUsage: "[directory path of service.json and task-definition.json]", Flags: []cli.Flag{ RegionFlag(&envars.Region), ClusterFlag(&envars.Cluster), @@ -28,26 +24,18 @@ func (c *cageCommands) Up() *cli.Command { CanaryTaskIdleDurationFlag(&envars.CanaryTaskIdleDuration), }, Action: func(ctx *cli.Context) error { - c.aggregateEnvars(ctx, &envars) - if err := c.prompt.ConfirmService(&envars); err != nil { + dir, _, err := c.requireArgs(ctx, 1, 1) + if err != nil { return err } - var cfg aws.Config - if o, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(envars.Region)); err != nil { + cagecli, err := c.setupCage(envars, dir) + if err != nil { return err - } else { - cfg = o } - cagecli := cage.NewCage(&cage.Input{ - Env: &envars, - ECS: ecs.NewFromConfig(cfg), - EC2: ec2.NewFromConfig(cfg), - ALB: elbv2.NewFromConfig(cfg), - }) - _, err := cagecli.Up(context.Background()) - if err != nil { - log.Error(err.Error()) + if err := c.Prompt.ConfirmService(envars); err != nil { + return err } + _, err = cagecli.Up(context.Background()) return err }, } diff --git a/cli/cage/main.go b/cli/cage/main.go index 5b0340f..33952a3 100644 --- a/cli/cage/main.go +++ b/cli/cage/main.go @@ -1,10 +1,10 @@ package main import ( - "context" "log" "os" + cage "github.com/loilo-inc/canarycage" "github.com/loilo-inc/canarycage/cli/cage/commands" "github.com/urfave/cli/v2" ) @@ -12,13 +12,21 @@ import ( func main() { app := cli.NewApp() app.Name = "canarycage" - app.Version = "3.7.0" + app.Version = "4.0.0-rc1" + app.Usage = "A deployment tool for AWS ECS" app.Description = "A deployment tool for AWS ECS" - ctx := context.Background() - cmds := commands.NewCageCommands(ctx, os.Stdin) - app.Commands = cmds.Commands() - err := app.Run(os.Args) - if err != nil { + envars := cage.Envars{} + cmds := commands.NewCageCommands(os.Stdin, commands.DefalutCageCliProvider) + app.Commands = cmds.Commands(&envars) + app.Flags = []cli.Flag{ + &cli.BoolFlag{ + Name: "ci", + Usage: "CI mode. Skip all confirmations and use default values.", + EnvVars: []string{"CI"}, + Destination: &envars.CI, + }, + } + if err := app.Run(os.Args); err != nil { log.Fatal(err) } } diff --git a/codecov.yml b/codecov.yml index 5a71d2b..c14680b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,4 +2,3 @@ ignore: - "test/*" - "mocks/*" - "cmd/cli/cage" - - "cmd/cli/cage/commands/*" diff --git a/mocks/mock_cage/cage.go b/mocks/mock_cage/cage.go new file mode 100644 index 0000000..6a59210 --- /dev/null +++ b/mocks/mock_cage/cage.go @@ -0,0 +1,148 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: cage.go + +// Package mock_cage is a generated GoMock package. +package mock_cage + +import ( + context "context" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + cage "github.com/loilo-inc/canarycage" +) + +// MockCage is a mock of Cage interface. +type MockCage struct { + ctrl *gomock.Controller + recorder *MockCageMockRecorder +} + +// MockCageMockRecorder is the mock recorder for MockCage. +type MockCageMockRecorder struct { + mock *MockCage +} + +// NewMockCage creates a new mock instance. +func NewMockCage(ctrl *gomock.Controller) *MockCage { + mock := &MockCage{ctrl: ctrl} + mock.recorder = &MockCageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCage) EXPECT() *MockCageMockRecorder { + return m.recorder +} + +// Recreate mocks base method. +func (m *MockCage) Recreate(ctx context.Context) (*cage.RecreateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Recreate", ctx) + ret0, _ := ret[0].(*cage.RecreateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Recreate indicates an expected call of Recreate. +func (mr *MockCageMockRecorder) Recreate(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recreate", reflect.TypeOf((*MockCage)(nil).Recreate), ctx) +} + +// RollOut mocks base method. +func (m *MockCage) RollOut(ctx context.Context) (*cage.RollOutResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RollOut", ctx) + ret0, _ := ret[0].(*cage.RollOutResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RollOut indicates an expected call of RollOut. +func (mr *MockCageMockRecorder) RollOut(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollOut", reflect.TypeOf((*MockCage)(nil).RollOut), ctx) +} + +// Run mocks base method. +func (m *MockCage) Run(ctx context.Context, input *cage.RunInput) (*cage.RunResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Run", ctx, input) + ret0, _ := ret[0].(*cage.RunResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Run indicates an expected call of Run. +func (mr *MockCageMockRecorder) Run(ctx, input interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockCage)(nil).Run), ctx, input) +} + +// Up mocks base method. +func (m *MockCage) Up(ctx context.Context) (*cage.UpResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Up", ctx) + ret0, _ := ret[0].(*cage.UpResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Up indicates an expected call of Up. +func (mr *MockCageMockRecorder) Up(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Up", reflect.TypeOf((*MockCage)(nil).Up), ctx) +} + +// MockTime is a mock of Time interface. +type MockTime struct { + ctrl *gomock.Controller + recorder *MockTimeMockRecorder +} + +// MockTimeMockRecorder is the mock recorder for MockTime. +type MockTimeMockRecorder struct { + mock *MockTime +} + +// NewMockTime creates a new mock instance. +func NewMockTime(ctrl *gomock.Controller) *MockTime { + mock := &MockTime{ctrl: ctrl} + mock.recorder = &MockTimeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTime) EXPECT() *MockTimeMockRecorder { + return m.recorder +} + +// NewTimer mocks base method. +func (m *MockTime) NewTimer(arg0 time.Duration) *time.Timer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewTimer", arg0) + ret0, _ := ret[0].(*time.Timer) + return ret0 +} + +// NewTimer indicates an expected call of NewTimer. +func (mr *MockTimeMockRecorder) NewTimer(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTimer", reflect.TypeOf((*MockTime)(nil).NewTimer), arg0) +} + +// Now mocks base method. +func (m *MockTime) Now() time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Now") + ret0, _ := ret[0].(time.Time) + return ret0 +} + +// Now indicates an expected call of Now. +func (mr *MockTimeMockRecorder) Now() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockTime)(nil).Now)) +} diff --git a/test/setup.go b/test/setup.go index e4c7d89..20d4167 100644 --- a/test/setup.go +++ b/test/setup.go @@ -47,18 +47,20 @@ func Setup(ctrl *gomock.Controller, envars *cage.Envars, currentTaskCount int, l ec2Mock.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeSubnets).AnyTimes() ec2Mock.EXPECT().DescribeInstances(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeInstances).AnyTimes() td, _ := mocker.RegisterTaskDefinition(context.Background(), envars.TaskDefinitionInput) - a := &ecs.CreateServiceInput{ - ServiceName: &envars.Service, - LoadBalancers: envars.ServiceDefinitionInput.LoadBalancers, - TaskDefinition: td.TaskDefinition.TaskDefinitionArn, - DesiredCount: aws.Int32(int32(currentTaskCount)), - LaunchType: ecstypes.LaunchType(launchType), - } - svc, _ := mocker.CreateService(context.Background(), a) - if len(svc.Service.LoadBalancers) > 0 { - _, _ = mocker.RegisterTarget(context.Background(), &elbv2.RegisterTargetsInput{ - TargetGroupArn: svc.Service.LoadBalancers[0].TargetGroupArn, - }) + if currentTaskCount >= 0 { + a := &ecs.CreateServiceInput{ + ServiceName: &envars.Service, + LoadBalancers: envars.ServiceDefinitionInput.LoadBalancers, + TaskDefinition: td.TaskDefinition.TaskDefinitionArn, + DesiredCount: aws.Int32(int32(currentTaskCount)), + LaunchType: ecstypes.LaunchType(launchType), + } + svc, _ := mocker.CreateService(context.Background(), a) + if len(svc.Service.LoadBalancers) > 0 { + _, _ = mocker.RegisterTarget(context.Background(), &elbv2.RegisterTargetsInput{ + TargetGroupArn: svc.Service.LoadBalancers[0].TargetGroupArn, + }) + } } return mocker, ecsMock, albMock, ec2Mock } From 6b37a111ba14ef70a4c316cd0a91385971d9d0af Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Tue, 28 May 2024 23:14:24 +0900 Subject: [PATCH 08/11] Update codecov.yml --- codecov.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codecov.yml b/codecov.yml index c14680b..b515dda 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,4 @@ ignore: - - "test/*" - - "mocks/*" - - "cmd/cli/cage" + - "test/**/*" + - "mocks/**/*" + - "cli/cage" From 2e80a7fa2b58de06bcf2433c201e25d2fe75a6d1 Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Wed, 29 May 2024 19:13:34 +0900 Subject: [PATCH 09/11] Update codecov.yml --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index b515dda..8c775c1 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,4 @@ ignore: - - "test/**/*" - - "mocks/**/*" + - "test" + - "mocks" - "cli/cage" From e54adf61e68f01ba55b5e439a5cc935769cd07ff Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Wed, 29 May 2024 19:59:02 +0900 Subject: [PATCH 10/11] tests --- env.go | 7 ++--- recreate.go | 38 +++++++++-------------- recreate_test.go | 67 ++++++++++++++++++++++++++++++++++++++++- rollout.go | 19 ++++++------ rollout_test.go | 14 ++++----- run.go | 9 +++--- task_definition.go | 2 +- task_definition_test.go | 2 +- test/context.go | 24 ++++++++------- up.go | 6 ++-- 10 files changed, 122 insertions(+), 66 deletions(-) diff --git a/env.go b/env.go index 9f14cd4..3f70e58 100644 --- a/env.go +++ b/env.go @@ -2,7 +2,6 @@ package cage import ( "encoding/json" - "fmt" "os" "path/filepath" @@ -66,13 +65,13 @@ func LoadDefinitionsFromFiles(dir string) ( var service ecs.CreateServiceInput var td ecs.RegisterTaskDefinitionInput if noSvc != nil || noTd != nil { - return nil, nil, fmt.Errorf("roll out context specified at '%s' but no 'service.json' or 'task-definition.json'", dir) + return nil, nil, xerrors.Errorf("roll out context specified at '%s' but no 'service.json' or 'task-definition.json'", dir) } if _, err := ReadAndUnmarshalJson(svcPath, &service); err != nil { - return nil, nil, fmt.Errorf("failed to read and unmarshal service.json: %s", err) + return nil, nil, xerrors.Errorf("failed to read and unmarshal service.json: %s", err) } if _, err := ReadAndUnmarshalJson(tdPath, &td); err != nil { - return nil, nil, fmt.Errorf("failed to read and unmarshal task-definition.json: %s", err) + return nil, nil, xerrors.Errorf("failed to read and unmarshal task-definition.json: %s", err) } return &td, &service, nil } diff --git a/recreate.go b/recreate.go index 1197019..6141188 100644 --- a/recreate.go +++ b/recreate.go @@ -3,7 +3,6 @@ package cage import ( "context" "fmt" - "time" "github.com/apex/log" "github.com/aws/aws-sdk-go-v2/aws" @@ -29,11 +28,11 @@ func (c *cage) Recreate(ctx context.Context) (*RecreateResult, error) { }); err != nil { return nil, xerrors.Errorf("couldn't describe service: %w", err) } else if len(o.Services) == 0 { - return nil, fmt.Errorf("service '%s' does not exist. Use 'cage up' instead", c.Env.Service) + return nil, xerrors.Errorf("service '%s' does not exist. Use 'cage up' instead", c.Env.Service) } else { oldService = &o.Services[0] if *oldService.Status == "INACTIVE" { - return nil, fmt.Errorf("service '%s' is already INACTIVE. Use 'cage up' instead", c.Env.Service) + return nil, xerrors.Errorf("service '%s' is already INACTIVE. Use 'cage up' instead", c.Env.Service) } } var err error @@ -90,14 +89,14 @@ func (c *cage) createService(ctx context.Context, serviceDefinitionInput *ecs.Cr log.Infof("creating service '%s' with task-definition '%s'...", *serviceDefinitionInput.ServiceName, *serviceDefinitionInput.TaskDefinition) o, err := c.Ecs.CreateService(ctx, serviceDefinitionInput) if err != nil { - return nil, fmt.Errorf("failed to create service '%s': %s", *serviceDefinitionInput.ServiceName, err.Error()) + return nil, xerrors.Errorf("failed to create service '%s': %w", *serviceDefinitionInput.ServiceName, err) } log.Infof("waiting for service '%s' to be STABLE", *serviceDefinitionInput.ServiceName) if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ Cluster: &c.Env.Cluster, Services: []string{*serviceDefinitionInput.ServiceName}, }, WaitDuration); err != nil { - return nil, fmt.Errorf("failed to wait for service '%s' to be STABLE: %s", *serviceDefinitionInput.ServiceName, err.Error()) + return nil, xerrors.Errorf("failed to wait for service '%s' to be STABLE: %w", *serviceDefinitionInput.ServiceName, err) } return o.Service, nil } @@ -109,14 +108,14 @@ func (c *cage) updateServiceTaskCount(ctx context.Context, service string, count Service: &service, DesiredCount: &count, }); err != nil { - return fmt.Errorf("failed to update service '%s': %w", service, err) + return xerrors.Errorf("failed to update service '%s': %w", service, err) } log.Infof("waiting for service '%s' to be STABLE", service) if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ Cluster: &c.Env.Cluster, Services: []string{service}, }, WaitDuration); err != nil { - return fmt.Errorf("failed to wait for service '%s' to be STABLE: %v", service, err) + return xerrors.Errorf("failed to wait for service '%s' to be STABLE: %v", service, err) } return nil } @@ -127,23 +126,14 @@ func (c *cage) deleteService(ctx context.Context, service string) error { Cluster: &c.Env.Cluster, Service: &service, }); err != nil { - return fmt.Errorf("failed to delete service '%s': %w", service, err) - } - var retryCount int = 0 - for retryCount < 10 { - <-c.Time.NewTimer(15 * time.Second).C - log.Infof("waiting for service '%s' to be INACTIVE", service) - if o, err := c.Ecs.DescribeServices(ctx, &ecs.DescribeServicesInput{ - Cluster: &c.Env.Cluster, - Services: []string{service}, - }); err != nil { - return fmt.Errorf("failed to describe service '%s': %w", service, err) - } else if len(o.Services) == 0 { - break - } else if *o.Services[0].Status == "INACTIVE" { - break - } - retryCount++ + return xerrors.Errorf("failed to delete service '%s': %w", service, err) + } + log.Infof("waiting for service '%s' to be INACTIVE", service) + if err := ecs.NewServicesInactiveWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ + Cluster: &c.Env.Cluster, + Services: []string{service}, + }, WaitDuration); err != nil { + return xerrors.Errorf("failed to wait for service '%s' to be INACTIVE: %w", service, err) } return nil } diff --git a/recreate_test.go b/recreate_test.go index c3e9419..7ebfc9b 100644 --- a/recreate_test.go +++ b/recreate_test.go @@ -2,10 +2,15 @@ package cage_test import ( "context" + "fmt" "testing" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/golang/mock/gomock" cage "github.com/loilo-inc/canarycage" + "github.com/loilo-inc/canarycage/mocks/mock_awsiface" "github.com/loilo-inc/canarycage/test" "github.com/stretchr/testify/assert" ) @@ -28,8 +33,68 @@ func TestRecreate(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result.Service) assert.NotNil(t, result.TaskDefinition) - assert.Equal(t, len(mocker.Services), 1) + assert.Equal(t, mocker.ActiveServiceSize(), 1) + assert.Equal(t, mocker.RunningTaskSize(), 1) assert.Equal(t, len(mocker.TaskDefinitions.List()), 2) assert.Equal(t, *mocker.Services["service"].ServiceName, *result.Service.ServiceName) }) + t.Run("should error if failed to describe old service", func(t *testing.T) { + env := test.DefaultEnvars() + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + }) + result, err := cagecli.Recreate(context.Background()) + assert.EqualError(t, err, "couldn't describe service: error") + assert.Nil(t, result) + }) + t.Run("should error if old service doesn't exist", func(t *testing.T) { + env := test.DefaultEnvars() + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Services: nil}, nil, + ) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + }) + result, err := cagecli.Recreate(context.Background()) + assert.EqualError(t, err, "service 'service' does not exist. Use 'cage up' instead") + assert.Nil(t, result) + }) + t.Run("should error if old service is already INACTIVE", func(t *testing.T) { + env := test.DefaultEnvars() + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Services: []ecstypes.Service{{Status: aws.String("INACTIVE")}}}, nil, + ) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + }) + result, err := cagecli.Recreate(context.Background()) + assert.EqualError(t, err, "service 'service' is already INACTIVE. Use 'cage up' instead") + assert.Nil(t, result) + }) + t.Run("should error if failed to create next task definition", func(t *testing.T) { + env := test.DefaultEnvars() + ctrl := gomock.NewController(t) + ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Services: []ecstypes.Service{{Status: aws.String("ACTIVE")}}}, nil, + ) + ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")) + cagecli := cage.NewCage(&cage.Input{ + Env: env, + ECS: ecsMock, + }) + result, err := cagecli.Recreate(context.Background()) + assert.EqualError(t, err, "failed to register next task definition: error") + assert.Nil(t, result) + }) } diff --git a/rollout.go b/rollout.go index 28a5072..9ff7ee6 100644 --- a/rollout.go +++ b/rollout.go @@ -13,6 +13,7 @@ import ( ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + "golang.org/x/xerrors" ) type RollOutResult struct { @@ -44,18 +45,18 @@ func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { c.Env.Service, }, }); err != nil { - log.Errorf("failed to describe current service due to: %s", err.Error()) + log.Errorf("failed to describe current service due to: %w", err) return throw(err) } else if len(out.Services) == 0 { - return throw(fmt.Errorf("service '%s' doesn't exist. Run 'cage up' or create service before rolling out", c.Env.Service)) + return throw(xerrors.Errorf("service '%s' doesn't exist. Run 'cage up' or create service before rolling out", c.Env.Service)) } else { service = out.Services[0] } if *service.Status != "ACTIVE" { - return throw(fmt.Errorf("๐Ÿ˜ต '%s' status is '%s'. Stop rolling out", c.Env.Service, *service.Status)) + return throw(xerrors.Errorf("๐Ÿ˜ต '%s' status is '%s'. Stop rolling out", c.Env.Service, *service.Status)) } if service.LaunchType == ecstypes.LaunchTypeEc2 && c.Env.CanaryInstanceArn == "" { - return throw(fmt.Errorf("๐Ÿฅบ --canaryInstanceArn is required when LaunchType = 'EC2'")) + return throw(xerrors.Errorf("๐Ÿฅบ --canaryInstanceArn is required when LaunchType = 'EC2'")) } var ( targetGroupArn *string @@ -167,7 +168,7 @@ func (c *cage) EnsureTaskHealthy( } else { recentState = GetTargetIsHealthy(o, targetId, targetPort) if recentState == nil { - return fmt.Errorf("'%s' is not registered to the target group '%s'", *targetId, *tgArn) + return xerrors.Errorf("'%s' is not registered to the target group '%s'", *targetId, *tgArn) } log.Infof("canary task '%s' (%s:%d) state is: %s", *taskArn, *targetId, *targetPort, *recentState) switch *recentState { @@ -187,7 +188,7 @@ func (c *cage) EnsureTaskHealthy( } // unhealthy, draining, unused log.Errorf("๐Ÿ˜จ canary task '%s' is unhealthy", *taskArn) - return fmt.Errorf( + return xerrors.Errorf( "canary task '%s' (%s:%d) hasn't become to be healthy. The most recent state: %s", *taskArn, *targetId, *targetPort, *recentState, ) @@ -306,7 +307,7 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes } task := o.Tasks[0] if *task.LastStatus != "RUNNING" { - return nil, fmt.Errorf("๐Ÿ˜ซ canary task has stopped: %s", *task.StoppedReason) + return nil, xerrors.Errorf("๐Ÿ˜ซ canary task has stopped: %s", *task.StoppedReason) } return &StartCanaryTaskOutput{ task: task, @@ -400,7 +401,7 @@ func (c *cage) waitUntilContainersBecomeHealthy(ctx context.Context, taskArn str } else { task := o.Tasks[0] if *task.LastStatus != "RUNNING" { - return fmt.Errorf("๐Ÿ˜ซ canary task has stopped: %s", *task.StoppedReason) + return xerrors.Errorf("๐Ÿ˜ซ canary task has stopped: %s", *task.StoppedReason) } for _, container := range task.Containers { @@ -418,7 +419,7 @@ func (c *cage) waitUntilContainersBecomeHealthy(ctx context.Context, taskArn str } } } - return fmt.Errorf("๐Ÿ˜จ canary task hasn't become to be healthy") + return xerrors.Errorf("๐Ÿ˜จ canary task hasn't become to be healthy") } func (c *cage) StopCanaryTask(ctx context.Context, input *StartCanaryTaskOutput) error { diff --git a/rollout_test.go b/rollout_test.go index 504c41f..54a4dce 100644 --- a/rollout_test.go +++ b/rollout_test.go @@ -28,7 +28,7 @@ func TestCage_RollOut_FARGATE(t *testing.T) { ctrl := gomock.NewController(t) mctx, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, v, "FARGATE") - if mctx.ServiceSize() != 1 { + if mctx.ActiveServiceSize() != 1 { t.Fatalf("current service not setup") } @@ -47,7 +47,7 @@ func TestCage_RollOut_FARGATE(t *testing.T) { result, err := cagecli.RollOut(ctx) assert.NoError(t, err) assert.False(t, result.ServiceIntact) - assert.Equal(t, 1, mctx.ServiceSize()) + assert.Equal(t, 1, mctx.ActiveServiceSize()) assert.Equal(t, v, mctx.RunningTaskSize()) } }) @@ -260,7 +260,7 @@ func TestCage_RollOut_EC2(t *testing.T) { }, }, }, nil).AnyTimes() - if mctx.ServiceSize() != 1 { + if mctx.ActiveServiceSize() != 1 { t.Fatalf("current service not setup") } if taskCnt := mctx.RunningTaskSize(); taskCnt != v { @@ -279,7 +279,7 @@ func TestCage_RollOut_EC2(t *testing.T) { t.Fatalf("%s", err) } assert.False(t, result.ServiceIntact) - assert.Equal(t, 1, mctx.ServiceSize()) + assert.Equal(t, 1, mctx.ActiveServiceSize()) assert.Equal(t, v, mctx.RunningTaskSize()) } } @@ -290,7 +290,7 @@ func TestCage_RollOut_EC2_without_ContainerInstanceArn(t *testing.T) { envars := test.DefaultEnvars() ctrl := gomock.NewController(t) mctx, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, 1, "EC2") - if mctx.ServiceSize() != 1 { + if mctx.ActiveServiceSize() != 1 { t.Fatalf("current service not setup") } if taskCnt := mctx.RunningTaskSize(); taskCnt != 1 { @@ -321,7 +321,7 @@ func TestCage_RollOut_EC2_no_attribute(t *testing.T) { envars.CanaryInstanceArn = canaryInstanceArn ctrl := gomock.NewController(t) mctx, ecsMock, albMock, ec2Mock := test.Setup(ctrl, envars, 1, "EC2") - if mctx.ServiceSize() != 1 { + if mctx.ActiveServiceSize() != 1 { t.Fatalf("current service not setup") } if taskCnt := mctx.RunningTaskSize(); taskCnt != 1 { @@ -344,6 +344,6 @@ func TestCage_RollOut_EC2_no_attribute(t *testing.T) { t.Fatalf("%s", err) } assert.False(t, result.ServiceIntact) - assert.Equal(t, 1, mctx.ServiceSize()) + assert.Equal(t, 1, mctx.ActiveServiceSize()) assert.Equal(t, 1, mctx.RunningTaskSize()) } diff --git a/run.go b/run.go index 53d47e5..7d3f9fb 100644 --- a/run.go +++ b/run.go @@ -2,7 +2,6 @@ package cage import ( "context" - "fmt" "time" "github.com/apex/log" @@ -35,7 +34,7 @@ func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { input.MaxWait = 5 * time.Minute } if !containerExistsInDefinition(c.Env.TaskDefinitionInput, input.Container) { - return nil, fmt.Errorf("๐Ÿšซ '%s' not found in container definitions", *input.Container) + return nil, xerrors.Errorf("๐Ÿšซ '%s' not found in container definitions", *input.Container) } td, err := c.CreateNextTaskDefinition(ctx) if err != nil { @@ -73,14 +72,14 @@ func (c *cage) Run(ctx context.Context, input *RunInput) (*RunResult, error) { for _, c := range task.Containers { if *c.Name == *input.Container { if c.ExitCode == nil { - return nil, fmt.Errorf("container '%s' hasn't exit", *input.Container) + return nil, xerrors.Errorf("container '%s' hasn't exit", *input.Container) } else if *c.ExitCode != 0 { - return nil, fmt.Errorf("task exited with %d", *c.ExitCode) + return nil, xerrors.Errorf("task exited with %d", *c.ExitCode) } return &RunResult{ExitCode: *c.ExitCode}, nil } } // Never reached? - return nil, fmt.Errorf("task '%s' not found in result", *taskArn) + return nil, xerrors.Errorf("task '%s' not found in result", *taskArn) } } diff --git a/task_definition.go b/task_definition.go index 5e3820c..25548f0 100644 --- a/task_definition.go +++ b/task_definition.go @@ -22,7 +22,7 @@ func (c *cage) CreateNextTaskDefinition(ctx context.Context) (*ecstypes.TaskDefi } else { log.Infof("creating next task definition...") if out, err := c.Ecs.RegisterTaskDefinition(ctx, c.Env.TaskDefinitionInput); err != nil { - return nil, xerrors.Errorf("failed to register task definition: %w", err) + return nil, xerrors.Errorf("failed to register next task definition: %w", err) } else { log.Infof( "task definition '%s:%d' has been registered", diff --git a/task_definition_test.go b/task_definition_test.go index c7f447d..76c43a1 100644 --- a/task_definition_test.go +++ b/task_definition_test.go @@ -75,7 +75,7 @@ func TestCage_CreateNextTaskDefinition(t *testing.T) { } ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(nil, xerrors.New("error")) td, err := c.CreateNextTaskDefinition(context.Background()) - assert.Errorf(t, err, "failed to register task definition: error") + assert.Errorf(t, err, "failed to register next task definition: error") assert.Nil(t, td) }) } diff --git a/test/context.go b/test/context.go index c19b135..f9cc437 100644 --- a/test/context.go +++ b/test/context.go @@ -64,17 +64,24 @@ func (ctx *MockContext) GetService(id string) (*types.Service, bool) { return o, ok } -func (ctx *MockContext) ServiceSize() int { +func (ctx *MockContext) ActiveServiceSize() (count int) { ctx.mux.Lock() defer ctx.mux.Unlock() - return len(ctx.Services) + for _, v := range ctx.Services { + if v.Status != nil && *v.Status == "ACTIVE" { + count++ + } + } + return } func (ctx *MockContext) CreateService(c context.Context, input *ecs.CreateServiceInput, _ ...func(options *ecs.Options)) (*ecs.CreateServiceOutput, error) { idstr := uuid.New().String() st := "ACTIVE" - if _, ok := ctx.Services[*input.ServiceName]; ok { - return nil, fmt.Errorf("service already exists: %s", *input.ServiceName) + if old, ok := ctx.Services[*input.ServiceName]; ok { + if *old.Status == "ACTIVE" { + return nil, fmt.Errorf("service already exists: %s", *input.ServiceName) + } } ret := &types.Service{ ServiceName: input.ServiceName, @@ -176,9 +183,7 @@ func (ctx *MockContext) UpdateService(c context.Context, input *ecs.UpdateServic } func (ctx *MockContext) DeleteService(c context.Context, input *ecs.DeleteServiceInput, _ ...func(options *ecs.Options)) (*ecs.DeleteServiceOutput, error) { - ctx.mux.Lock() service := ctx.Services[*input.Service] - ctx.mux.Unlock() reg := regexp.MustCompile(fmt.Sprintf("service:%s", *service.ServiceName)) for _, v := range ctx.Tasks { if reg.MatchString(*v.Group) { @@ -193,11 +198,8 @@ func (ctx *MockContext) DeleteService(c context.Context, input *ecs.DeleteServic } ctx.mux.Lock() defer ctx.mux.Unlock() - delete(ctx.Services, *input.Service) - delete(ctx.Tasks, *service.ServiceArn) - return &ecs.DeleteServiceOutput{ - Service: service, - }, nil + service.Status = aws.String("INACTIVE") + return &ecs.DeleteServiceOutput{Service: service}, nil } func (ctx *MockContext) RegisterTaskDefinition(_ context.Context, input *ecs.RegisterTaskDefinitionInput, _ ...func(options *ecs.Options)) (*ecs.RegisterTaskDefinitionOutput, error) { diff --git a/up.go b/up.go index 6b60abb..740f951 100644 --- a/up.go +++ b/up.go @@ -2,11 +2,11 @@ package cage import ( "context" - "fmt" "github.com/apex/log" "github.com/aws/aws-sdk-go-v2/service/ecs" ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "golang.org/x/xerrors" ) type UpResult struct { @@ -24,11 +24,11 @@ func (c *cage) Up(ctx context.Context) (*UpResult, error) { Cluster: &c.Env.Cluster, Services: []string{c.Env.Service}, }); err != nil { - return nil, fmt.Errorf("couldn't describe service: %s", err.Error()) + return nil, xerrors.Errorf("couldn't describe service: %w", err) } else if len(o.Services) > 0 { svc := o.Services[0] if *svc.Status != "INACTIVE" { - return nil, fmt.Errorf("service '%s' already exists. Use 'cage rollout' instead", c.Env.Service) + return nil, xerrors.Errorf("service '%s' already exists. Use 'cage rollout' instead", c.Env.Service) } } c.Env.ServiceDefinitionInput.TaskDefinition = td.TaskDefinitionArn From 089c985cdf9a1049cd637e38da6f78d36988f972 Mon Sep 17 00:00:00 2001 From: Yusuke Sakurai Date: Wed, 29 May 2024 21:32:06 +0900 Subject: [PATCH 11/11] add tests --- cage.go | 33 ++++--- recreate.go | 6 +- recreate_test.go | 252 ++++++++++++++++++++++++++++++++++++++--------- rollout.go | 10 +- 4 files changed, 233 insertions(+), 68 deletions(-) diff --git a/cage.go b/cage.go index aa477f2..97f97b0 100644 --- a/cage.go +++ b/cage.go @@ -21,19 +21,21 @@ type Time interface { } type cage struct { - Env *Envars - Ecs awsiface.EcsClient - Alb awsiface.AlbClient - Ec2 awsiface.Ec2Client - Time Time + Env *Envars + Ecs awsiface.EcsClient + Alb awsiface.AlbClient + Ec2 awsiface.Ec2Client + Time Time + MaxWait time.Duration } type Input struct { - Env *Envars - ECS awsiface.EcsClient - ALB awsiface.AlbClient - EC2 awsiface.Ec2Client - Time Time + Env *Envars + ECS awsiface.EcsClient + ALB awsiface.AlbClient + EC2 awsiface.Ec2Client + Time Time + MaxWait time.Duration } func NewCage(input *Input) Cage { @@ -41,10 +43,11 @@ func NewCage(input *Input) Cage { input.Time = &timeImpl{} } return &cage{ - Env: input.Env, - Ecs: input.ECS, - Alb: input.ALB, - Ec2: input.EC2, - Time: input.Time, + Env: input.Env, + Ecs: input.ECS, + Alb: input.ALB, + Ec2: input.EC2, + Time: input.Time, + MaxWait: 5 * time.Minute, } } diff --git a/recreate.go b/recreate.go index 6141188..46c3f6e 100644 --- a/recreate.go +++ b/recreate.go @@ -95,7 +95,7 @@ func (c *cage) createService(ctx context.Context, serviceDefinitionInput *ecs.Cr if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ Cluster: &c.Env.Cluster, Services: []string{*serviceDefinitionInput.ServiceName}, - }, WaitDuration); err != nil { + }, c.MaxWait); err != nil { return nil, xerrors.Errorf("failed to wait for service '%s' to be STABLE: %w", *serviceDefinitionInput.ServiceName, err) } return o.Service, nil @@ -114,7 +114,7 @@ func (c *cage) updateServiceTaskCount(ctx context.Context, service string, count if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ Cluster: &c.Env.Cluster, Services: []string{service}, - }, WaitDuration); err != nil { + }, c.MaxWait); err != nil { return xerrors.Errorf("failed to wait for service '%s' to be STABLE: %v", service, err) } return nil @@ -132,7 +132,7 @@ func (c *cage) deleteService(ctx context.Context, service string) error { if err := ecs.NewServicesInactiveWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ Cluster: &c.Env.Cluster, Services: []string{service}, - }, WaitDuration); err != nil { + }, c.MaxWait); err != nil { return xerrors.Errorf("failed to wait for service '%s' to be INACTIVE: %w", service, err) } return nil diff --git a/recreate_test.go b/recreate_test.go index 7ebfc9b..ede0f54 100644 --- a/recreate_test.go +++ b/recreate_test.go @@ -16,85 +16,249 @@ import ( ) func TestRecreate(t *testing.T) { - t.Run("basic", func(t *testing.T) { + setup := func(t *testing.T, passPhase int) ( + cage.Cage, + *test.MockContext, + *mock_awsiface.MockEcsClient, + *gomock.Call, + ) { env := test.DefaultEnvars() ctrl := gomock.NewController(t) - ctx := context.TODO() - mocker, ecsMock, _, _ := test.Setup(ctrl, env, 1, "FARGATE") - mocker.CreateService(ctx, env.ServiceDefinitionInput) - cagecli := cage.NewCage(&cage.Input{ - Env: env, - ECS: ecsMock, - ALB: nil, - EC2: nil, - Time: test.NewFakeTime(), - }) + m := mock_awsiface.NewMockEcsClient(ctrl) + mocker := test.NewMockContext() + mocker.CreateService(context.TODO(), env.ServiceDefinitionInput) + phases := []func() *gomock.Call{ + func() *gomock.Call { + // describe old service + return m.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeServices) + }, + func() *gomock.Call { + // create next task definition + return m.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTaskDefinition) + }, + } + waiter := func() *gomock.Call { + return m.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeServices) + } + swapPhase := []func() *gomock.Call{ + func() *gomock.Call { + // create transition service + return m.EXPECT().CreateService(gomock.Any(), gomock.Any()).DoAndReturn(mocker.CreateService) + }, + // expect transition service to be ACTIVE + waiter, + func() *gomock.Call { + // update transition service's desired count to old service's desired count + return m.EXPECT().UpdateService(gomock.Any(), gomock.Any()).DoAndReturn(mocker.UpdateService) + }, + // expect transition service to be ACTIVE + waiter, + func() *gomock.Call { + // update old service's desired count to 0 + return m.EXPECT().UpdateService(gomock.Any(), gomock.Any()).DoAndReturn(mocker.UpdateService) + }, + // expect old service to be ACTIVE + waiter, + func() *gomock.Call { + // delete old service + return m.EXPECT().DeleteService(gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeleteService) + }, + // expect old service to be INACTIVE + waiter, + } + allPhases := append(phases, swapPhase...) + allPhases = append(allPhases, swapPhase...) + i := 0 + var prevCall *gomock.Call + for { + if i == passPhase || i == len(allPhases) { + break + } + call := allPhases[i]() + if prevCall != nil { + call.After(prevCall) + } + prevCall = call + i++ + } + return cage.NewCage(&cage.Input{ + Env: env, + ECS: m, + ALB: nil, + EC2: nil, + Time: test.NewFakeTime(), + MaxWait: 1, + }), mocker, m, prevCall + } + t.Run("basic", func(t *testing.T) { + cagecli, mocker, _, _ := setup(t, -1) result, err := cagecli.Recreate(context.Background()) assert.NoError(t, err) assert.NotNil(t, result.Service) assert.NotNil(t, result.TaskDefinition) assert.Equal(t, mocker.ActiveServiceSize(), 1) assert.Equal(t, mocker.RunningTaskSize(), 1) - assert.Equal(t, len(mocker.TaskDefinitions.List()), 2) + assert.Equal(t, len(mocker.TaskDefinitions.List()), 1) assert.Equal(t, *mocker.Services["service"].ServiceName, *result.Service.ServiceName) }) t.Run("should error if failed to describe old service", func(t *testing.T) { - env := test.DefaultEnvars() - ctrl := gomock.NewController(t) - ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + cagecli, _, ecsMock, _ := setup(t, 0) ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")) - cagecli := cage.NewCage(&cage.Input{ - Env: env, - ECS: ecsMock, - }) result, err := cagecli.Recreate(context.Background()) assert.EqualError(t, err, "couldn't describe service: error") assert.Nil(t, result) }) t.Run("should error if old service doesn't exist", func(t *testing.T) { - env := test.DefaultEnvars() - ctrl := gomock.NewController(t) - ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + cagecli, _, ecsMock, _ := setup(t, 0) ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return( &ecs.DescribeServicesOutput{Services: nil}, nil, ) - cagecli := cage.NewCage(&cage.Input{ - Env: env, - ECS: ecsMock, - }) result, err := cagecli.Recreate(context.Background()) assert.EqualError(t, err, "service 'service' does not exist. Use 'cage up' instead") assert.Nil(t, result) }) t.Run("should error if old service is already INACTIVE", func(t *testing.T) { - env := test.DefaultEnvars() - ctrl := gomock.NewController(t) - ecsMock := mock_awsiface.NewMockEcsClient(ctrl) + cagecli, _, ecsMock, _ := setup(t, 0) ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return( &ecs.DescribeServicesOutput{Services: []ecstypes.Service{{Status: aws.String("INACTIVE")}}}, nil, ) - cagecli := cage.NewCage(&cage.Input{ - Env: env, - ECS: ecsMock, - }) result, err := cagecli.Recreate(context.Background()) assert.EqualError(t, err, "service 'service' is already INACTIVE. Use 'cage up' instead") assert.Nil(t, result) }) t.Run("should error if failed to create next task definition", func(t *testing.T) { - env := test.DefaultEnvars() - ctrl := gomock.NewController(t) - ecsMock := mock_awsiface.NewMockEcsClient(ctrl) - ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any()).Return( - &ecs.DescribeServicesOutput{Services: []ecstypes.Service{{Status: aws.String("ACTIVE")}}}, nil, - ) - ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")) - cagecli := cage.NewCage(&cage.Input{ - Env: env, - ECS: ecsMock, - }) + cagecli, _, ecsMock, call := setup(t, 1) + ecsMock.EXPECT().RegisterTaskDefinition(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) result, err := cagecli.Recreate(context.Background()) assert.EqualError(t, err, "failed to register next task definition: error") assert.Nil(t, result) }) + t.Run("should error if failed to create transition service", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 2) + ecsMock.EXPECT().CreateService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to create service") + assert.Nil(t, result) + }) + t.Run("should error if transition service is not ACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 3) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) + t.Run("should error if failed to update transition service's desired count", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 4) + ecsMock.EXPECT().UpdateService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to update service") + assert.Nil(t, result) + }) + t.Run("should error if transition service is not ACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 5) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) + t.Run("should error if failed to update old service's desired count", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 6) + ecsMock.EXPECT().UpdateService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to update service") + assert.Nil(t, result) + }) + t.Run("should error if old service is not ACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 7) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) + t.Run("should error if failed to delete old service", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 8) + ecsMock.EXPECT().DeleteService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to delete service") + assert.Nil(t, result) + }) + t.Run("should error if old service is not INACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 9) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) + t.Run("should error if failed to create new service", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 10) + ecsMock.EXPECT().CreateService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to create service") + assert.Nil(t, result) + }) + t.Run("should error if new service is not ACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 11) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) + t.Run("should error if failed to update new service's desired count", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 12) + ecsMock.EXPECT().UpdateService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to update service") + assert.Nil(t, result) + }) + t.Run("should error if old service is not ACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 13) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) + t.Run("should error if failed to update transition service's desired count", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 14) + ecsMock.EXPECT().UpdateService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to update service") + assert.Nil(t, result) + }) + t.Run("should error if transition service is not ACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 15) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) + t.Run("should error if failed to delete old service", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 16) + ecsMock.EXPECT().DeleteService(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to delete service") + assert.Nil(t, result) + }) + t.Run("should error if old service is not INACTIVE", func(t *testing.T) { + cagecli, _, ecsMock, call := setup(t, 17) + ecsMock.EXPECT().DescribeServices(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &ecs.DescribeServicesOutput{Failures: []ecstypes.Failure{{Reason: aws.String("MISSING")}}}, nil, + ).After(call) + result, err := cagecli.Recreate(context.Background()) + assert.ErrorContains(t, err, "failed to wait for service") + assert.Nil(t, result) + }) } diff --git a/rollout.go b/rollout.go index 9ff7ee6..8ff7eb4 100644 --- a/rollout.go +++ b/rollout.go @@ -22,8 +22,6 @@ type RollOutResult struct { ServiceIntact bool } -var WaitDuration = 15 * time.Minute - func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { ret := &RollOutResult{ StartTime: c.Time.Now(), @@ -136,7 +134,7 @@ func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { if err := ecs.NewServicesStableWaiter(c.Ecs).Wait(ctx, &ecs.DescribeServicesInput{ Cluster: &c.Env.Cluster, Services: []string{c.Env.Service}, - }, WaitDuration); err != nil { + }, c.MaxWait); err != nil { return throw(err) } log.Infof("๐Ÿฅด service '%s' has become to be stable!", c.Env.Service) @@ -267,7 +265,7 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes if err := ecs.NewTasksRunningWaiter(c.Ecs).Wait(ctx, &ecs.DescribeTasksInput{ Cluster: &c.Env.Cluster, Tasks: []string{*taskArn}, - }, WaitDuration); err != nil { + }, c.MaxWait); err != nil { return nil, err } log.Infof("๐Ÿฃ canary task '%s' is running!๏ธ", *taskArn) @@ -444,7 +442,7 @@ func (c *cage) StopCanaryTask(ctx context.Context, input *StartCanaryTaskOutput) Id: input.targetId, Port: input.targetPort, }}, - }, WaitDuration); err != nil { + }, c.MaxWait); err != nil { return err } log.Infof( @@ -463,7 +461,7 @@ func (c *cage) StopCanaryTask(ctx context.Context, input *StartCanaryTaskOutput) if err := ecs.NewTasksStoppedWaiter(c.Ecs).Wait(ctx, &ecs.DescribeTasksInput{ Cluster: &c.Env.Cluster, Tasks: []string{*input.task.TaskArn}, - }, WaitDuration); err != nil { + }, c.MaxWait); err != nil { return err } log.Infof("canary task '%s' has successfully been stopped", *input.task.TaskArn)