@@ -18,6 +18,7 @@ import (
18
18
"context"
19
19
"fmt"
20
20
"sort"
21
+ "strings"
21
22
22
23
"github.com/aws/aws-sdk-go/aws"
23
24
"github.com/aws/aws-sdk-go/aws/request"
@@ -33,6 +34,10 @@ const (
33
34
// Based on existing number from cloudwatch-agent
34
35
ec2CacheSize = 2000
35
36
describeContainerInstanceLimit = 100
37
+ describeServiceLimit = 10
38
+ // NOTE: these constants are not defined in go sdk, there are three values for deployment status.
39
+ deploymentStatusActive = "ACTIVE"
40
+ deploymentStatusPrimary = "PRIMARY"
36
41
)
37
42
38
43
// ecsClient includes API required by taskFetcher.
@@ -41,6 +46,8 @@ type ecsClient interface {
41
46
DescribeTasksWithContext (ctx context.Context , input * ecs.DescribeTasksInput , opts ... request.Option ) (* ecs.DescribeTasksOutput , error )
42
47
DescribeTaskDefinitionWithContext (ctx context.Context , input * ecs.DescribeTaskDefinitionInput , opts ... request.Option ) (* ecs.DescribeTaskDefinitionOutput , error )
43
48
DescribeContainerInstancesWithContext (ctx context.Context , input * ecs.DescribeContainerInstancesInput , opts ... request.Option ) (* ecs.DescribeContainerInstancesOutput , error )
49
+ ListServicesWithContext (ctx context.Context , input * ecs.ListServicesInput , opts ... request.Option ) (* ecs.ListServicesOutput , error )
50
+ DescribeServicesWithContext (ctx context.Context , input * ecs.DescribeServicesInput , opts ... request.Option ) (* ecs.DescribeServicesOutput , error )
44
51
}
45
52
46
53
// ec2Client includes API required by TaskFetcher.
@@ -49,12 +56,13 @@ type ec2Client interface {
49
56
}
50
57
51
58
type taskFetcher struct {
52
- logger * zap.Logger
53
- ecs ecsClient
54
- ec2 ec2Client
55
- cluster string
56
- taskDefCache simplelru.LRUCache
57
- ec2Cache simplelru.LRUCache
59
+ logger * zap.Logger
60
+ ecs ecsClient
61
+ ec2 ec2Client
62
+ cluster string
63
+ taskDefCache simplelru.LRUCache
64
+ ec2Cache simplelru.LRUCache
65
+ serviceNameFilter serviceNameFilter
58
66
}
59
67
60
68
type taskFetcherOptions struct {
@@ -85,6 +93,11 @@ func newTaskFetcher(opts taskFetcherOptions) (*taskFetcher, error) {
85
93
cluster : opts .Cluster ,
86
94
taskDefCache : taskDefCache ,
87
95
ec2Cache : ec2Cache ,
96
+ // TODO: after the service matcher PR is merged, use actual service name filter here.
97
+ // For now, describe all the services
98
+ serviceNameFilter : func (name string ) bool {
99
+ return true
100
+ },
88
101
}
89
102
// Return early if any clients are mocked, caller should overrides all the clients when mocking.
90
103
if fetcher .ecs != nil || fetcher .ec2 != nil {
@@ -105,9 +118,16 @@ func (f *taskFetcher) fetchAndDecorate(ctx context.Context) ([]*Task, error) {
105
118
}
106
119
107
120
// EC2
108
- if err : = f .attachContainerInstance (ctx , tasks ); err != nil {
121
+ if err = f .attachContainerInstance (ctx , tasks ); err != nil {
109
122
return nil , fmt .Errorf ("attachContainerInstance failed: %w" , err )
110
123
}
124
+
125
+ // Services
126
+ services , err := f .getAllServices (ctx )
127
+ if err != nil {
128
+ return nil , fmt .Errorf ("getAllServices failed: %w" , err )
129
+ }
130
+ f .attachService (tasks , services )
111
131
return tasks , nil
112
132
}
113
133
@@ -246,7 +266,7 @@ func (f *taskFetcher) describeContainerInstances(ctx context.Context, instanceLi
246
266
ContainerInstances : instanceList ,
247
267
})
248
268
if err != nil {
249
- return fmt .Errorf ("ecs.DescribeContainerInstance faile : %w" , err )
269
+ return fmt .Errorf ("ecs.DescribeContainerInstance failed : %w" , err )
250
270
}
251
271
252
272
// Create the index to map ec2 id back to container instance id.
@@ -282,6 +302,85 @@ func (f *taskFetcher) describeContainerInstances(ctx context.Context, instanceLi
282
302
return nil
283
303
}
284
304
305
+ // serviceNameFilter decides if we should get detail info for a service, i.e. make the describe API call.
306
+ type serviceNameFilter func (name string ) bool
307
+
308
+ // getAllServices does not have cache like task definition or ec2 instances
309
+ // because we need to get the deployment id to map service to task, which changes frequently.
310
+ func (f * taskFetcher ) getAllServices (ctx context.Context ) ([]* ecs.Service , error ) {
311
+ svc := f .ecs
312
+ cluster := aws .String (f .cluster )
313
+ // List and filter out services we need to desribe.
314
+ listReq := ecs.ListServicesInput {Cluster : cluster }
315
+ var servicesToDescribe []* string
316
+ for {
317
+ res , err := svc .ListServicesWithContext (ctx , & listReq )
318
+ if err != nil {
319
+ return nil , err
320
+ }
321
+ for _ , arn := range res .ServiceArns {
322
+ segs := strings .Split (aws .StringValue (arn ), "/" )
323
+ name := segs [len (segs )- 1 ]
324
+ if f .serviceNameFilter (name ) {
325
+ servicesToDescribe = append (servicesToDescribe , arn )
326
+ }
327
+ }
328
+ if res .NextToken == nil {
329
+ break
330
+ }
331
+ listReq .NextToken = res .NextToken
332
+ }
333
+
334
+ // DescribeServices size limit is 10 so we need to do paging on client side.
335
+ var services []* ecs.Service
336
+ for i := 0 ; i < len (servicesToDescribe ); i += describeServiceLimit {
337
+ end := minInt (i + describeServiceLimit , len (servicesToDescribe ))
338
+ desc := & ecs.DescribeServicesInput {
339
+ Cluster : cluster ,
340
+ Services : servicesToDescribe [i :end ],
341
+ }
342
+ res , err := svc .DescribeServicesWithContext (ctx , desc )
343
+ if err != nil {
344
+ return nil , fmt .Errorf ("ecs.DescribeServices failed %w" , err )
345
+ }
346
+ services = append (services , res .Services ... )
347
+ }
348
+ return services , nil
349
+ }
350
+
351
+ // attachService map service to task using deployment id.
352
+ // Each service can have multiple deployment and each task keep track of the deployment in task.StartedBy.
353
+ func (f * taskFetcher ) attachService (tasks []* Task , services []* ecs.Service ) {
354
+ // Map deployment ID to service name
355
+ idToService := make (map [string ]* ecs.Service )
356
+ for _ , svc := range services {
357
+ for _ , deployment := range svc .Deployments {
358
+ status := aws .StringValue (deployment .Status )
359
+ if status == deploymentStatusActive || status == deploymentStatusPrimary {
360
+ idToService [aws .StringValue (deployment .Id )] = svc
361
+ break
362
+ }
363
+ }
364
+ }
365
+
366
+ // Attach service to task
367
+ for _ , t := range tasks {
368
+ // Task is created using RunTask i.e. not manged by a service.
369
+ if t .Task .StartedBy == nil {
370
+ continue
371
+ }
372
+ deploymentID := aws .StringValue (t .Task .StartedBy )
373
+ svc := idToService [deploymentID ]
374
+ // Service not found happen a lot because we only fetch services defined in ServiceConfig.
375
+ // However, we fetch all the tasks, which could be started by other services no mentioned in config
376
+ // or started using RunTasks API directly.
377
+ if svc == nil {
378
+ continue
379
+ }
380
+ t .Service = svc
381
+ }
382
+ }
383
+
285
384
// Util Start
286
385
287
386
func sortStringPointers (ps []* string ) {
0 commit comments