Skip to content

Commit d004b2c

Browse files
authored
chore: show target health check in svc status (#2381)
This PR shows targets health when running `svc status`. To break down the changes, this PR: - implements a client to make calls to `elbv2` to retrieve target health information - modifies `aws/ecs` package, specifically on `service` in order to support getting information on target group ARNs; and on `task` to retrieve task's private IP address (which is used to match against `target`) - modifies `describe` package to show the target health status along with their matched task - some other minor refactoring Along with #2355, this resolves #1947. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent bd2c70f commit d004b2c

22 files changed

+2389
-597
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ gen-mocks: tools
178178
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/cloudformation/stackset/mocks/mock_stackset.go -source=./internal/pkg/aws/cloudformation/stackset/stackset.go
179179
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/ssm/mocks/mock_ssm.go -source=./internal/pkg/aws/ssm/ssm.go
180180
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/stepfunctions/mocks/mock_stepfunctions.go -source=./internal/pkg/aws/stepfunctions/stepfunctions.go
181-
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/addon/mocks/mock_addons.go -source=./internal/pkg/addon/addons.go
181+
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/apprunner/mocks/mock_apprunner.go -source=./internal/pkg/aws/apprunner/apprunner.go
182+
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/elbv2/mocks/mock_elbv2.go -source=./internal/pkg/aws/elbv2/elbv2.go
182183
${GOBIN}/mockgen -package=exec -source=./internal/pkg/exec/exec.go -destination=./internal/pkg/exec/mock_exec.go
183184
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/deploy/mocks/mock_deploy.go -source=./internal/pkg/deploy/deploy.go
184185
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/deploy/cloudformation/mocks/mock_cloudformation.go -source=./internal/pkg/deploy/cloudformation/cloudformation.go
@@ -196,4 +197,3 @@ gen-mocks: tools
196197
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/initialize/mocks/mock_workload.go -source=./internal/pkg/initialize/workload.go
197198
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/ecs/mocks/mock_ecs.go -source=./internal/pkg/ecs/ecs.go
198199
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/ecs/mocks/mock_run_task_request.go -source=./internal/pkg/ecs/run_task_request.go
199-
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/apprunner/mocks/mock_apprunner.go -source=./internal/pkg/aws/apprunner/apprunner.go

internal/pkg/aws/ecs/ecs.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ func (e *ECS) Service(clusterName, serviceName string) (*Service, error) {
9999
return nil, fmt.Errorf("cannot find service %s", serviceName)
100100
}
101101

102-
// ServiceTasks calls ECS API and returns ECS tasks running by a service.
103-
func (e *ECS) ServiceTasks(cluster, service string) ([]*Task, error) {
104-
return e.listTasks(cluster, withService(service))
102+
// ServiceRunningTasks calls ECS API and returns the ECS tasks spun up by the service, with the desired status to be set to be RUNNING.
103+
func (e *ECS) ServiceRunningTasks(cluster, service string) ([]*Task, error) {
104+
return e.listTasks(cluster, withService(service), withRunningTasks())
105105
}
106106

107107
// StoppedServiceTasks calls ECS API and returns stopped ECS tasks in a service.

internal/pkg/aws/ecs/ecs_test.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,9 @@ func TestECS_Tasks(t *testing.T) {
201201
serviceName: "mockService",
202202
mockECSClient: func(m *mocks.Mockapi) {
203203
m.EXPECT().ListTasks(&ecs.ListTasksInput{
204-
Cluster: aws.String("mockCluster"),
205-
ServiceName: aws.String("mockService"),
204+
Cluster: aws.String("mockCluster"),
205+
ServiceName: aws.String("mockService"),
206+
DesiredStatus: aws.String("RUNNING"),
206207
}).Return(nil, errors.New("some error"))
207208
},
208209
wantErr: fmt.Errorf("list running tasks: some error"),
@@ -212,8 +213,9 @@ func TestECS_Tasks(t *testing.T) {
212213
serviceName: "mockService",
213214
mockECSClient: func(m *mocks.Mockapi) {
214215
m.EXPECT().ListTasks(&ecs.ListTasksInput{
215-
Cluster: aws.String("mockCluster"),
216-
ServiceName: aws.String("mockService"),
216+
Cluster: aws.String("mockCluster"),
217+
ServiceName: aws.String("mockService"),
218+
DesiredStatus: aws.String("RUNNING"),
217219
}).Return(&ecs.ListTasksOutput{
218220
NextToken: nil,
219221
TaskArns: aws.StringSlice([]string{"mockTaskArn"}),
@@ -231,8 +233,9 @@ func TestECS_Tasks(t *testing.T) {
231233
serviceName: "mockService",
232234
mockECSClient: func(m *mocks.Mockapi) {
233235
m.EXPECT().ListTasks(&ecs.ListTasksInput{
234-
Cluster: aws.String("mockCluster"),
235-
ServiceName: aws.String("mockService"),
236+
Cluster: aws.String("mockCluster"),
237+
ServiceName: aws.String("mockService"),
238+
DesiredStatus: aws.String("RUNNING"),
236239
}).Return(&ecs.ListTasksOutput{
237240
NextToken: nil,
238241
TaskArns: aws.StringSlice([]string{"mockTaskArn"}),
@@ -260,8 +263,9 @@ func TestECS_Tasks(t *testing.T) {
260263
serviceName: "mockService",
261264
mockECSClient: func(m *mocks.Mockapi) {
262265
m.EXPECT().ListTasks(&ecs.ListTasksInput{
263-
Cluster: aws.String("mockCluster"),
264-
ServiceName: aws.String("mockService"),
266+
Cluster: aws.String("mockCluster"),
267+
ServiceName: aws.String("mockService"),
268+
DesiredStatus: aws.String("RUNNING"),
265269
}).Return(&ecs.ListTasksOutput{
266270
NextToken: aws.String("mockNextToken"),
267271
TaskArns: aws.StringSlice([]string{"mockTaskArn1"}),
@@ -278,9 +282,10 @@ func TestECS_Tasks(t *testing.T) {
278282
},
279283
}, nil)
280284
m.EXPECT().ListTasks(&ecs.ListTasksInput{
281-
Cluster: aws.String("mockCluster"),
282-
ServiceName: aws.String("mockService"),
283-
NextToken: aws.String("mockNextToken"),
285+
Cluster: aws.String("mockCluster"),
286+
ServiceName: aws.String("mockService"),
287+
DesiredStatus: aws.String("RUNNING"),
288+
NextToken: aws.String("mockNextToken"),
284289
}).Return(&ecs.ListTasksOutput{
285290
NextToken: nil,
286291
TaskArns: aws.StringSlice([]string{"mockTaskArn2"}),
@@ -321,7 +326,7 @@ func TestECS_Tasks(t *testing.T) {
321326
client: mockECSClient,
322327
}
323328

324-
gotTasks, gotErr := service.ServiceTasks(tc.clusterName, tc.serviceName)
329+
gotTasks, gotErr := service.ServiceRunningTasks(tc.clusterName, tc.serviceName)
325330

326331
if gotErr != nil {
327332
require.EqualError(t, tc.wantErr, gotErr.Error())

internal/pkg/aws/ecs/errors.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@ func (e *ErrExecuteCommand) Error() string {
6565
}
6666

6767
const (
68-
missingFieldAttachment = "attachment"
69-
missingFieldDetailENIID = "detailENIID"
68+
missingFieldAttachment = "attachment"
69+
missingFieldDetailENIID = "detailENIID"
70+
missingFieldPrivateIPv4Address = "privateIPv4"
7071
)
7172

72-
// ErrTaskENIInfoNotFound when the ENI information is not found in a ECS task.
73+
// ErrTaskENIInfoNotFound when some ENI information is not found in a ECS task.
7374
type ErrTaskENIInfoNotFound struct {
7475
MissingField string
7576
TaskARN string
@@ -81,6 +82,8 @@ func (e *ErrTaskENIInfoNotFound) Error() string {
8182
return fmt.Sprintf("cannot find network interface attachment for task %s", e.TaskARN)
8283
case missingFieldDetailENIID:
8384
return fmt.Sprintf("cannot find network interface ID for task %s", e.TaskARN)
85+
case missingFieldPrivateIPv4Address:
86+
return fmt.Sprintf("cannot find private IPv4 address for task %s", e.TaskARN)
8487
}
8588
return ""
8689
}

internal/pkg/aws/ecs/service.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,71 @@ import (
1414
"github.com/aws/aws-sdk-go/service/ecs"
1515
)
1616

17+
const (
18+
// ServiceDeploymentStatusPrimary is the status PRIMARY of an ECS service deployment.
19+
ServiceDeploymentStatusPrimary = "PRIMARY"
20+
// ServiceDeploymentStatusActive is the status ACTIVE of an ECS service deployment.
21+
ServiceDeploymentStatusActive = "ACTIVE"
22+
)
23+
1724
// Service wraps up ECS Service struct.
1825
type Service ecs.Service
1926

27+
// Deployment contains information of a ECS service Deployment.
28+
type Deployment struct {
29+
Id string `json:"id"`
30+
DesiredCount int64 `json:"desiredCount"`
31+
RunningCount int64 `json:"runningCount"`
32+
UpdatedAt time.Time `json:"updatedAt"`
33+
LaunchType string `json:"launchType"`
34+
TaskDefinition string `json:"taskDefinition"`
35+
Status string `json:"status"`
36+
}
37+
2038
// ServiceStatus contains the status info of a service.
2139
type ServiceStatus struct {
22-
DesiredCount int64 `json:"desiredCount"`
23-
RunningCount int64 `json:"runningCount"`
24-
Status string `json:"status"`
25-
LastDeploymentAt time.Time `json:"lastDeploymentAt"`
26-
TaskDefinition string `json:"taskDefinition"`
40+
DesiredCount int64 `json:"desiredCount"`
41+
RunningCount int64 `json:"runningCount"`
42+
Status string `json:"status"`
43+
Deployments []Deployment `json:"deployments"`
44+
LastDeploymentAt time.Time `json:"lastDeploymentAt"` // kept to avoid breaking change
45+
TaskDefinition string `json:"taskDefinition"` // kept to avoid breaking change
2746
}
2847

2948
// ServiceStatus returns the status of the running service.
3049
func (s *Service) ServiceStatus() ServiceStatus {
50+
var deployments []Deployment
51+
for _, dp := range s.Deployments {
52+
deployments = append(deployments, Deployment{
53+
Id: aws.StringValue(dp.Id),
54+
DesiredCount: aws.Int64Value(dp.DesiredCount),
55+
RunningCount: aws.Int64Value(dp.RunningCount),
56+
UpdatedAt: aws.TimeValue(dp.UpdatedAt),
57+
LaunchType: aws.StringValue(dp.LaunchType),
58+
TaskDefinition: aws.StringValue(dp.TaskDefinition),
59+
Status: aws.StringValue(dp.Status),
60+
})
61+
}
62+
3163
return ServiceStatus{
3264
Status: aws.StringValue(s.Status),
3365
DesiredCount: aws.Int64Value(s.DesiredCount),
3466
RunningCount: aws.Int64Value(s.RunningCount),
35-
LastDeploymentAt: *s.Deployments[0].UpdatedAt, // FIXME Service assumed to have at least one deployment
67+
Deployments: deployments,
68+
LastDeploymentAt: aws.TimeValue(s.Deployments[0].UpdatedAt), // FIXME Service assumed to have at least one deployment
3669
TaskDefinition: aws.StringValue(s.Deployments[0].TaskDefinition),
3770
}
3871
}
3972

73+
// TargetGroups returns the ARNs of target groups attached to the service.
74+
func (s *Service) TargetGroups() []string {
75+
var targetGroupARNs []string
76+
for _, lb := range s.LoadBalancers {
77+
targetGroupARNs = append(targetGroupARNs, aws.StringValue(lb.TargetGroupArn))
78+
}
79+
return targetGroupARNs
80+
}
81+
4082
// ServiceArn is the arn of an ECS service.
4183
type ServiceArn string
4284

internal/pkg/aws/ecs/service_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ecs
5+
6+
import (
7+
"testing"
8+
"time"
9+
10+
"github.com/aws/aws-sdk-go/aws"
11+
"github.com/aws/aws-sdk-go/service/ecs"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestService_TargetGroups(t *testing.T) {
16+
t.Run("should return correct ARNs", func(t *testing.T) {
17+
s := Service{
18+
LoadBalancers: []*ecs.LoadBalancer{
19+
{
20+
TargetGroupArn: aws.String("group-1"),
21+
},
22+
{
23+
TargetGroupArn: aws.String("group-2"),
24+
},
25+
},
26+
}
27+
got := s.TargetGroups()
28+
expected := []string{"group-1", "group-2"}
29+
require.Equal(t, expected, got)
30+
})
31+
}
32+
33+
func TestService_ServiceStatus(t *testing.T) {
34+
t.Run("should include active and primary deployments in status", func(t *testing.T) {
35+
inService := Service{
36+
Deployments: []*ecs.Deployment{
37+
{
38+
Status: aws.String("ACTIVE"),
39+
Id: aws.String("id-1"),
40+
DesiredCount: aws.Int64(3),
41+
RunningCount: aws.Int64(3),
42+
},
43+
{
44+
Status: aws.String("ACTIVE"),
45+
Id: aws.String("id-3"),
46+
DesiredCount: aws.Int64(4),
47+
RunningCount: aws.Int64(2),
48+
},
49+
{
50+
Status: aws.String("PRIMARY"),
51+
Id: aws.String("id-4"),
52+
DesiredCount: aws.Int64(10),
53+
RunningCount: aws.Int64(1),
54+
},
55+
{
56+
Status: aws.String("INACTIVE"),
57+
Id: aws.String("id-5"),
58+
},
59+
},
60+
}
61+
wanted := ServiceStatus{
62+
Status: "",
63+
DesiredCount: 0,
64+
RunningCount: 0,
65+
Deployments: []Deployment{
66+
{
67+
Id: "id-1",
68+
DesiredCount: 3,
69+
RunningCount: 3,
70+
Status: "ACTIVE",
71+
},
72+
{
73+
Id: "id-3",
74+
DesiredCount: 4,
75+
RunningCount: 2,
76+
Status: "ACTIVE",
77+
},
78+
{
79+
Id: "id-4",
80+
DesiredCount: 10,
81+
RunningCount: 1,
82+
Status: "PRIMARY",
83+
},
84+
{
85+
Id: "id-5",
86+
Status: "INACTIVE",
87+
},
88+
},
89+
LastDeploymentAt: time.Time{},
90+
TaskDefinition: "",
91+
}
92+
got := inService.ServiceStatus()
93+
require.Equal(t, got, wanted)
94+
})
95+
}

0 commit comments

Comments
 (0)