From 2267068b3f16b811778ea495575705181838b37b Mon Sep 17 00:00:00 2001 From: Takafumi SEKIGUCHI Date: Mon, 15 Apr 2024 14:00:51 +0900 Subject: [PATCH] feat: Add canary task container health checks (#68) --- fixtures/task-definition.json | 112 +++++++++++ rollout.go | 47 +++++ rollout_test.go | 338 +++++++++++++++++++--------------- test/context.go | 53 ++++-- 4 files changed, 388 insertions(+), 162 deletions(-) diff --git a/fixtures/task-definition.json b/fixtures/task-definition.json index f098324..a1267f7 100644 --- a/fixtures/task-definition.json +++ b/fixtures/task-definition.json @@ -124,6 +124,118 @@ "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": [ + { + "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": "" + } + } } ], "volumes": [ diff --git a/rollout.go b/rollout.go index 78145c2..b9e74db 100644 --- a/rollout.go +++ b/rollout.go @@ -96,6 +96,13 @@ func (c *cage) RollOut(ctx context.Context) (*RollOutResult, error) { ) } }(canaryTask, ret) + + log.Infof("😷 ensuring canary task container(s) to become healthy...") + if err := c.waitUntilContainersBecomeHealthy(ctx, *canaryTask.task.TaskArn, nextTaskDefinition); err != nil { + return throw(err) + } + log.Info("🤩 canary task container(s) is healthy!") + log.Infof("canary task '%s' ensured.", *canaryTask.task.TaskArn) if targetGroupArn != nil { log.Infof("😷 ensuring canary task to become healthy...") @@ -398,6 +405,46 @@ func (c *cage) StartCanaryTask(ctx context.Context, nextTaskDefinition *ecstypes }, nil } +func (c *cage) waitUntilContainersBecomeHealthy(ctx context.Context, taskArn string, nextTaskDefinition *ecstypes.TaskDefinition) error { + containerHasHealthChecks := map[string]struct{}{} + for _, definition := range nextTaskDefinition.ContainerDefinitions { + if definition.HealthCheck != nil { + containerHasHealthChecks[*definition.Name] = struct{}{} + } + } + + for count := 0; count < 10; count++ { + <-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, + Tasks: []string{taskArn}, + }); err != nil { + return err + } else { + task := o.Tasks[0] + if *task.LastStatus != "RUNNING" { + return fmt.Errorf("😫 canary task has stopped: %s", *task.StoppedReason) + } + + for _, container := range task.Containers { + if _, ok := containerHasHealthChecks[*container.Name]; !ok { + continue + } + if container.HealthStatus != ecstypes.HealthStatusHealthy { + log.Infof("container '%s' is not healthy: %s", *container.Name, container.HealthStatus) + continue + } + delete(containerHasHealthChecks, *container.Name) + } + if len(containerHasHealthChecks) == 0 { + return nil + } + } + } + return fmt.Errorf("😨 canary task hasn't become to be healthy") +} + func (c *cage) StopCanaryTask(ctx context.Context, input *StartCanaryTaskOutput) error { if input.registrationSkipped { log.Info("no load balancer is attached to service. Skip deregisteration.") diff --git a/rollout_test.go b/rollout_test.go index ac43966..1417633 100644 --- a/rollout_test.go +++ b/rollout_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "io/ioutil" "regexp" + "strings" "testing" "github.com/apex/log" @@ -91,24 +92,67 @@ func Setup(ctrl *gomock.Controller, envars *Envars, currentTaskCount int, launch return mocker, ecsMock, albMock, ec2Mock } -func TestCage_RollOut(t *testing.T) { +func TestCage_RollOut_FARGATE(t *testing.T) { log.SetLevel(log.DebugLevel) newTimer = fakeTimer - defer recoverTimer() - for _, v := range []int{1, 2, 15} { - log.Info("====") - envars := DefaultEnvars() - ctrl := gomock.NewController(t) - mctx, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, v, "FARGATE") + t.Cleanup(recoverTimer) - if mctx.ServiceSize() != 1 { - t.Fatalf("current service not setup") - } + t.Run("basic", func(t *testing.T) { + for _, v := range []int{1, 2, 15} { + log.Info("====") + envars := DefaultEnvars() + ctrl := gomock.NewController(t) + mctx, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, v, "FARGATE") - if taskCnt := mctx.RunningTaskSize(); taskCnt != v { - t.Fatalf("current tasks not setup: %d/%d", v, taskCnt) + if mctx.ServiceSize() != 1 { + t.Fatalf("current service not setup") + } + + if taskCnt := mctx.RunningTaskSize(); taskCnt != v { + t.Fatalf("current tasks not setup: %d/%d", v, taskCnt) + } + + cagecli := NewCage(&Input{ + Env: envars, + ECS: ecsMock, + ALB: albMock, + EC2: ec2Mock, + }) + ctx := context.Background() + result, err := cagecli.RollOut(ctx) + if err != nil { + t.Fatalf("%s", 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() + ctrl := gomock.NewController(t) + mocker, ecsMock, _, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + + albMock := mock_awsiface.NewMockAlbClient(ctrl) + albMock.EXPECT().RegisterTargets(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTarget).AnyTimes() + albMock.EXPECT().DeregisterTargets(gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeregisterTarget).AnyTimes() + gomock.InOrder( + albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any()).Return(&elbv2.DescribeTargetHealthOutput{ + TargetHealthDescriptions: []elbv2types.TargetHealthDescription{ + { + Target: &elbv2types.TargetDescription{ + Id: aws.String("127.0.0.1"), + Port: aws.Int32(80), + AvailabilityZone: aws.String("us-west-2"), + }, + TargetHealth: &elbv2types.TargetHealth{ + State: elbv2types.TargetHealthStateEnumUnused, + }, + }}, + }, nil).Times(2), + albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(mocker.DescribeTargetHealth).AnyTimes(), + ) cagecli := NewCage(&Input{ Env: envars, ECS: ecsMock, @@ -118,160 +162,160 @@ func TestCage_RollOut(t *testing.T) { ctx := context.Background() result, err := cagecli.RollOut(ctx) if err != nil { - t.Fatalf("%s", err) + t.Fatalf(err.Error()) } - assert.False(t, result.ServiceIntact) - assert.Equal(t, 1, mctx.ServiceSize()) - assert.Equal(t, v, mctx.RunningTaskSize()) - } -} - -func TestCage_RollOut2(t *testing.T) { - // canary taskがtgに登録されるまで少し待つ - newTimer = fakeTimer - defer recoverTimer() - envars := DefaultEnvars() - ctrl := gomock.NewController(t) - mocker, ecsMock, _, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + assert.NotNil(t, result) + }) - albMock := mock_awsiface.NewMockAlbClient(ctrl) - albMock.EXPECT().RegisterTargets(gomock.Any(), gomock.Any()).DoAndReturn(mocker.RegisterTarget).AnyTimes() - albMock.EXPECT().DeregisterTargets(gomock.Any(), gomock.Any()).DoAndReturn(mocker.DeregisterTarget).AnyTimes() - gomock.InOrder( - albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any()).Return(&elbv2.DescribeTargetHealthOutput{ - TargetHealthDescriptions: []elbv2types.TargetHealthDescription{ - { + t.Run("canary taskがtgに登録されない場合は打ち切る", func(t *testing.T) { + envars := DefaultEnvars() + ctrl := gomock.NewController(t) + mocker, ecsMock, _, ec2Mock := 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() + gomock.InOrder( + albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any(), gomock.Any()).Return(&elbv2.DescribeTargetHealthOutput{ + TargetHealthDescriptions: []elbv2types.TargetHealthDescription{{ + Target: &elbv2types.TargetDescription{ + Id: aws.String("192.0.0.1"), + Port: aws.Int32(8000), + AvailabilityZone: aws.String("us-west-2"), + }, + TargetHealth: &elbv2types.TargetHealth{ + State: elbv2types.TargetHealthStateEnumUnhealthy, + }, + }, { Target: &elbv2types.TargetDescription{ Id: aws.String("127.0.0.1"), - Port: aws.Int32(80), + Port: aws.Int32(8000), AvailabilityZone: aws.String("us-west-2"), }, TargetHealth: &elbv2types.TargetHealth{ State: elbv2types.TargetHealthStateEnumUnused, }, }}, - }, 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, + }, 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, + }) + ctx := context.Background() + _, err := cagecli.RollOut(ctx) + assert.NotNil(t, err) }) - ctx := context.Background() - result, err := cagecli.RollOut(ctx) - if err != nil { - t.Fatalf(err.Error()) - } - assert.NotNil(t, result) -} -func TestCage_RollOut3(t *testing.T) { - // canary taskがtgに登録されない場合は打ち切る - newTimer = fakeTimer - defer recoverTimer() - envars := DefaultEnvars() - ctrl := gomock.NewController(t) - mocker, ecsMock, _, ec2Mock := 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() - gomock.InOrder( - albMock.EXPECT().DescribeTargetHealth(gomock.Any(), gomock.Any(), gomock.Any()).Return(&elbv2.DescribeTargetHealthOutput{ - TargetHealthDescriptions: []elbv2types.TargetHealthDescription{{ - Target: &elbv2types.TargetDescription{ - Id: aws.String("192.0.0.1"), - Port: aws.Int32(8000), - AvailabilityZone: aws.String("us-west-2"), - }, - TargetHealth: &elbv2types.TargetHealth{ - State: elbv2types.TargetHealthStateEnumUnhealthy, - }, - }, { - Target: &elbv2types.TargetDescription{ - Id: aws.String("127.0.0.1"), - Port: aws.Int32(8000), - AvailabilityZone: aws.String("us-west-2"), - }, - TargetHealth: &elbv2types.TargetHealth{ - State: elbv2types.TargetHealthStateEnumUnused, - }, - }}, - }, 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, + + t.Run("Show error if service doesn't exist", func(t *testing.T) { + envars := DefaultEnvars() + ctrl := gomock.NewController(t) + mocker, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + delete(mocker.Services, envars.Service) + cagecli := NewCage(&Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + }) + ctx := context.Background() + _, err := cagecli.RollOut(ctx) + assert.EqualError(t, err, "service 'service' doesn't exist. Run 'cage up' or create service before rolling out") }) - ctx := context.Background() - _, err := cagecli.RollOut(ctx) - assert.NotNil(t, err) -} -// Show error if service doesn't exist -func TestCage_RollOut4(t *testing.T) { - newTimer = fakeTimer - defer recoverTimer() - envars := DefaultEnvars() - ctrl := gomock.NewController(t) - mocker, ecsMock, albMock, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") - delete(mocker.Services, envars.Service) - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, - EC2: ec2Mock, - ALB: albMock, + t.Run("lbがないサービスの場合もロールアウトする", func(t *testing.T) { + envars := 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, + }) + ctx := context.Background() + if res, err := cagecli.RollOut(ctx); err != nil { + t.Fatalf(err.Error()) + } else if res.ServiceIntact { + t.Fatalf("no") + } }) - ctx := context.Background() - _, err := cagecli.RollOut(ctx) - assert.EqualError(t, err, "service 'service' doesn't exist. Run 'cage up' or create service before rolling out") -} -func TestCage_StartGradualRollOut5(t *testing.T) { - // lbがないサービスの場合もロールアウトする - envars := DefaultEnvars() - newTimer = fakeTimer - defer recoverTimer() - 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, + t.Run("stop rolloing out when service status is inactive", func(t *testing.T) { + envars := 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 := NewCage(&Input{ + Env: envars, + ECS: ecsMock, + }) + _, err := cagecli.RollOut(context.Background()) + assert.EqualError(t, err, "😵 'service' status is 'INACTIVE'. Stop rolling out") }) - ctx := context.Background() - if res, err := cagecli.RollOut(ctx); err != nil { - t.Fatalf(err.Error()) - } else if res.ServiceIntact { - t.Fatalf("no") - } -} -func TestCage_RollOut5(t *testing.T) { - envars := DefaultEnvars() - newTimer = fakeTimer - defer recoverTimer() - 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")}, + t.Run("canary task container が healthy にならない場合は打ち切る", func(t *testing.T) { + envars := DefaultEnvars() + ctrl := gomock.NewController(t) + mocker, _, albMock, ec2Mock := Setup(ctrl, envars, 2, "FARGATE") + + 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( + func(ctx context.Context, input *ecs.DescribeTasksInput, opts ...func(options *ecs.Options)) (*ecs.DescribeTasksOutput, error) { + out, err := mocker.DescribeTasks(ctx, input, opts...) + if err != nil { + return out, err + } + + task := mocker.Tasks[input.Tasks[0]] + if strings.Contains(*task.Group, "canary-task") { + for i := range out.Tasks { + for i2 := range out.Tasks[i].Containers { + out.Tasks[i].Containers[i2].HealthStatus = ecstypes.HealthStatusUnknown + } + } + } + return out, err }, - }, nil, - ) - cagecli := NewCage(&Input{ - Env: envars, - ECS: ecsMock, + ).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() + + cagecli := NewCage(&Input{ + Env: envars, + ECS: ecsMock, + EC2: ec2Mock, + ALB: albMock, + }) + ctx := context.Background() + res, err := cagecli.RollOut(ctx) + assert.NotNil(t, res) + assert.NotNil(t, err) + + for _, task := range mocker.Tasks { + if strings.Contains(*task.Group, "canary-task") { + assert.Equal(t, "STOPPED", *task.LastStatus) + } + } }) - _, err := cagecli.RollOut(context.Background()) - assert.EqualError(t, err, "😵 'service' status is 'INACTIVE'. Stop rolling out") } func TestCage_RollOut_EC2(t *testing.T) { diff --git a/test/context.go b/test/context.go index 8e24e63..43d660a 100644 --- a/test/context.go +++ b/test/context.go @@ -18,17 +18,19 @@ import ( ) type MockContext struct { - Services map[string]*types.Service - Tasks map[string]*types.Task - TargetGroups map[string]struct{} - mux sync.Mutex + Services map[string]*types.Service + Tasks map[string]*types.Task + TaskDefinitions map[string]*types.TaskDefinition + TargetGroups map[string]struct{} + mux sync.Mutex } func NewMockContext() *MockContext { return &MockContext{ - Services: make(map[string]*types.Service), - Tasks: make(map[string]*types.Task), - TargetGroups: make(map[string]struct{}), + Services: make(map[string]*types.Service), + Tasks: make(map[string]*types.Task), + TaskDefinitions: make(map[string]*types.TaskDefinition), + TargetGroups: make(map[string]struct{}), } } @@ -187,18 +189,25 @@ 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, + } return &ecs.RegisterTaskDefinitionOutput{ - TaskDefinition: &types.TaskDefinition{ - TaskDefinitionArn: &idstr, - Family: aws.String("family"), - Revision: 1, - ContainerDefinitions: input.ContainerDefinitions, - }, + TaskDefinition: ctx.TaskDefinitions[idstr], }, 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() attachments := []types.Attachment{{ @@ -213,14 +222,28 @@ 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[i] = types.Container{ + Name: v.Name, + Image: v.Image, + LastStatus: aws.String("RUNNING"), + } + if v.HealthCheck != nil { + containers[i].HealthStatus = "HEALTHY" + } else { + containers[i].HealthStatus = "UNKNOWN" + } + } + ret := types.Task{ TaskArn: &idstr, ClusterArn: input.Cluster, TaskDefinitionArn: input.TaskDefinition, Group: input.Group, + Containers: containers, } - ctx.mux.Lock() - defer ctx.mux.Unlock() ctx.Tasks[idstr] = &ret s, ok := ctx.Services[*input.Group] var launchType types.LaunchType