prometheus/discovery/aws/ecs_test.go
matt-gp 1b52ab9e3b
feat: AWS ECS Service Discovery
Signed-off-by: matt-gp <small_minority@hotmail.com>
2025-11-06 22:48:07 +00:00

954 lines
31 KiB
Go

// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package aws
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/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
// Struct for test data.
type ecsDataStore struct {
region string
clusters []ecsTypes.Cluster
services []ecsTypes.Service
tasks []ecsTypes.Task
}
func TestECSDiscoveryListClusterARNs(t *testing.T) {
ctx := context.Background()
// iterate through the test cases
for _, tt := range []struct {
name string
ecsData *ecsDataStore
expected []string
}{
{
name: "MultipleClusters",
ecsData: &ecsDataStore{
region: "us-west-2",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("test-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
},
{
ClusterName: strptr("prod-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/prod-cluster"),
Status: strptr("ACTIVE"),
},
},
},
expected: []string{
"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
"arn:aws:ecs:us-west-2:123456789012:cluster/prod-cluster",
},
},
{
name: "SingleCluster",
ecsData: &ecsDataStore{
region: "us-east-1",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("single-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/single-cluster"),
Status: strptr("ACTIVE"),
},
},
},
expected: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/single-cluster",
},
},
{
name: "NoClusters",
ecsData: &ecsDataStore{
region: "us-east-1",
clusters: []ecsTypes.Cluster{},
},
expected: nil,
},
} {
t.Run(tt.name, func(t *testing.T) {
client := newMockECSClient(tt.ecsData)
d := &ECSDiscovery{
ecs: client,
cfg: &ECSSDConfig{
Region: tt.ecsData.region,
RequestConcurrency: 10,
},
}
clusters, err := d.listClusterARNs(ctx)
require.NoError(t, err)
require.Equal(t, tt.expected, clusters)
})
}
}
func TestECSDiscoveryDescribeClusters(t *testing.T) {
ctx := context.Background()
// iterate through the test cases
for _, tt := range []struct {
name string
ecsData *ecsDataStore
clusterARNs []string
expected map[string]ecsTypes.Cluster
}{
{
name: "SingleClusterWithTags",
ecsData: &ecsDataStore{
region: "us-west-2",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("test-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
Tags: []ecsTypes.Tag{
{Key: strptr("Environment"), Value: strptr("test")},
{Key: strptr("Team"), Value: strptr("backend")},
},
},
},
},
clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"},
expected: map[string]ecsTypes.Cluster{
"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": {
ClusterName: strptr("test-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
Tags: []ecsTypes.Tag{
{Key: strptr("Environment"), Value: strptr("test")},
{Key: strptr("Team"), Value: strptr("backend")},
},
},
},
},
{
name: "MultipleClusters",
ecsData: &ecsDataStore{
region: "us-east-1",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("cluster-1"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
Status: strptr("ACTIVE"),
},
{
ClusterName: strptr("cluster-2"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
Status: strptr("DRAINING"),
Tags: []ecsTypes.Tag{
{Key: strptr("Stage"), Value: strptr("prod")},
},
},
},
},
clusterARNs: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1",
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2",
},
expected: map[string]ecsTypes.Cluster{
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1": {
ClusterName: strptr("cluster-1"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
Status: strptr("ACTIVE"),
},
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2": {
ClusterName: strptr("cluster-2"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
Status: strptr("DRAINING"),
Tags: []ecsTypes.Tag{
{Key: strptr("Stage"), Value: strptr("prod")},
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
client := newMockECSClient(tt.ecsData)
d := &ECSDiscovery{
ecs: client,
cfg: &ECSSDConfig{
Region: tt.ecsData.region,
RequestConcurrency: 10,
},
}
clusterMap, err := d.describeClusters(ctx, tt.clusterARNs)
require.NoError(t, err)
require.Equal(t, tt.expected, clusterMap)
})
}
}
func TestECSDiscoveryListServiceARNs(t *testing.T) {
ctx := context.Background()
// iterate through the test cases
for _, tt := range []struct {
name string
ecsData *ecsDataStore
clusterARNs []string
expected map[string][]string
}{
{
name: "SingleClusterWithServices",
ecsData: &ecsDataStore{
region: "us-west-2",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("test-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
},
},
services: []ecsTypes.Service{
{
ServiceName: strptr("web-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
},
{
ServiceName: strptr("api-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
},
{
// this is to test the old arn format without the cluster name in the service arn
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-arn-migration.html
ServiceName: strptr("old-api-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/old-api-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
},
},
},
clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"},
expected: map[string][]string{
"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": {
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service",
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service",
"arn:aws:ecs:us-west-2:123456789012:service/old-api-service",
},
},
},
{
name: "MultipleClustesWithServices",
ecsData: &ecsDataStore{
region: "us-east-1",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("cluster-1"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
Status: strptr("ACTIVE"),
},
{
ClusterName: strptr("cluster-2"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
Status: strptr("ACTIVE"),
},
},
services: []ecsTypes.Service{
{
ServiceName: strptr("service-1"),
ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/cluster-1/service-1"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
Status: strptr("RUNNING"),
},
{
ServiceName: strptr("service-2"),
ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/cluster-2/service-2"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
Status: strptr("RUNNING"),
},
},
},
clusterARNs: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1",
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2",
},
expected: map[string][]string{
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1": {
"arn:aws:ecs:us-east-1:123456789012:service/cluster-1/service-1",
},
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2": {
"arn:aws:ecs:us-east-1:123456789012:service/cluster-2/service-2",
},
},
},
{
name: "ClusterWithNoServices",
ecsData: &ecsDataStore{
region: "us-west-2",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("empty-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/empty-cluster"),
Status: strptr("ACTIVE"),
},
},
services: []ecsTypes.Service{},
},
clusterARNs: []string{"arn:aws:ecs:us-west-2:123456789012:cluster/empty-cluster"},
expected: map[string][]string{
"arn:aws:ecs:us-west-2:123456789012:cluster/empty-cluster": nil,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
client := newMockECSClient(tt.ecsData)
d := &ECSDiscovery{
ecs: client,
cfg: &ECSSDConfig{
Region: tt.ecsData.region,
RequestConcurrency: 1,
},
}
serviceMap, err := d.listServiceARNs(ctx, tt.clusterARNs)
require.NoError(t, err)
require.Equal(t, tt.expected, serviceMap)
})
}
}
func TestECSDiscoveryDescribeServices(t *testing.T) {
ctx := context.Background()
// iterate through the test cases
for _, tt := range []struct {
name string
ecsData *ecsDataStore
clusterServiceARNsMap map[string][]string
expected map[string][]ecsTypes.Service
}{
{
name: "SingleClusterServices",
ecsData: &ecsDataStore{
region: "us-west-2",
services: []ecsTypes.Service{
{
ServiceName: strptr("web-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
TaskDefinition: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
Tags: []ecsTypes.Tag{
{Key: strptr("Environment"), Value: strptr("production")},
},
},
{
ServiceName: strptr("api-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
TaskDefinition: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
},
},
},
clusterServiceARNsMap: map[string][]string{
"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": {
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service",
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service",
},
},
expected: map[string][]ecsTypes.Service{
"arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster": {
{
ServiceName: strptr("web-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
TaskDefinition: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
Tags: []ecsTypes.Tag{
{Key: strptr("Environment"), Value: strptr("production")},
},
},
{
ServiceName: strptr("api-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
TaskDefinition: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
},
},
},
},
{
name: "MultipleClustersServices",
ecsData: &ecsDataStore{
region: "us-east-1",
services: []ecsTypes.Service{
{
ServiceName: strptr("service-1"),
ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/cluster-1/service-1"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
Status: strptr("RUNNING"),
TaskDefinition: strptr("arn:aws:ecs:us-east-1:123456789012:task-definition/task-1:1"),
},
{
ServiceName: strptr("service-2"),
ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/cluster-2/service-2"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
Status: strptr("DRAINING"),
TaskDefinition: strptr("arn:aws:ecs:us-east-1:123456789012:task-definition/task-2:1"),
},
},
},
clusterServiceARNsMap: map[string][]string{
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1": {
"arn:aws:ecs:us-east-1:123456789012:service/cluster-1/service-1",
},
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2": {
"arn:aws:ecs:us-east-1:123456789012:service/cluster-2/service-2",
},
},
expected: map[string][]ecsTypes.Service{
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1": {
{
ServiceName: strptr("service-1"),
ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/cluster-1/service-1"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-1"),
Status: strptr("RUNNING"),
TaskDefinition: strptr("arn:aws:ecs:us-east-1:123456789012:task-definition/task-1:1"),
},
},
"arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2": {
{
ServiceName: strptr("service-2"),
ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/cluster-2/service-2"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/cluster-2"),
Status: strptr("DRAINING"),
TaskDefinition: strptr("arn:aws:ecs:us-east-1:123456789012:task-definition/task-2:1"),
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
client := newMockECSClient(tt.ecsData)
d := &ECSDiscovery{
ecs: client,
cfg: &ECSSDConfig{
Region: tt.ecsData.region,
RequestConcurrency: 1,
},
}
serviceMap, err := d.describeServices(ctx, tt.clusterServiceARNsMap)
require.NoError(t, err)
require.Equal(t, tt.expected, serviceMap)
})
}
}
func TestECSDiscoveryListTaskARNs(t *testing.T) {
ctx := context.Background()
// iterate through the test cases
for _, tt := range []struct {
name string
ecsData *ecsDataStore
services []ecsTypes.Service
expected map[string][]string
}{
{
name: "ServicesWithTasks",
ecsData: &ecsDataStore{
region: "us-west-2",
tasks: []ecsTypes.Task{
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Group: strptr("service:web-service"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
},
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Group: strptr("service:web-service"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
},
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-3"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Group: strptr("service:api-service"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
},
},
},
services: []ecsTypes.Service{
{
ServiceName: strptr("web-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
},
{
ServiceName: strptr("api-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
},
},
expected: map[string][]string{
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service": {
"arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1",
"arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2",
},
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service": {
"arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-3",
},
},
},
{
name: "ServiceWithNoTasks",
ecsData: &ecsDataStore{
region: "us-west-2",
tasks: []ecsTypes.Task{},
},
services: []ecsTypes.Service{
{
ServiceName: strptr("empty-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/empty-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("RUNNING"),
},
},
expected: map[string][]string{
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/empty-service": nil,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
client := newMockECSClient(tt.ecsData)
d := &ECSDiscovery{
ecs: client,
cfg: &ECSSDConfig{
Region: tt.ecsData.region,
RequestConcurrency: 1,
},
}
taskMap, err := d.listTaskARNs(ctx, tt.services)
require.NoError(t, err)
require.Equal(t, tt.expected, taskMap)
})
}
}
func TestECSDiscoveryDescribeTasks(t *testing.T) {
ctx := context.Background()
// iterate through the test cases
for _, tt := range []struct {
name string
ecsData *ecsDataStore
clusterARN string
taskARNsMap map[string][]string
expected map[string][]ecsTypes.Task
}{
{
name: "TasksInCluster",
ecsData: &ecsDataStore{
region: "us-west-2",
tasks: []ecsTypes.Task{
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Group: strptr("service:web-service"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
LastStatus: strptr("RUNNING"),
Tags: []ecsTypes.Tag{
{Key: strptr("Environment"), Value: strptr("production")},
},
},
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Group: strptr("service:api-service"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
LastStatus: strptr("RUNNING"),
},
},
},
clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
taskARNsMap: map[string][]string{
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service": {
"arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1",
},
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service": {
"arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2",
},
},
expected: map[string][]ecsTypes.Task{
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service": {
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Group: strptr("service:web-service"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
LastStatus: strptr("RUNNING"),
Tags: []ecsTypes.Tag{
{Key: strptr("Environment"), Value: strptr("production")},
},
},
},
"arn:aws:ecs:us-west-2:123456789012:service/test-cluster/api-service": {
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-2"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Group: strptr("service:api-service"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/api-task:2"),
LastStatus: strptr("RUNNING"),
},
},
},
},
{
name: "EmptyTaskARNsMap",
ecsData: &ecsDataStore{
region: "us-west-2",
tasks: []ecsTypes.Task{},
},
clusterARN: "arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster",
taskARNsMap: map[string][]string{},
expected: map[string][]ecsTypes.Task{},
},
} {
t.Run(tt.name, func(t *testing.T) {
client := newMockECSClient(tt.ecsData)
d := &ECSDiscovery{
ecs: client,
cfg: &ECSSDConfig{
Region: tt.ecsData.region,
RequestConcurrency: 1,
},
}
taskMap, err := d.describeTasks(ctx, tt.clusterARN, tt.taskARNsMap)
require.NoError(t, err)
require.Equal(t, tt.expected, taskMap)
})
}
}
func TestECSDiscoveryRefresh(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
ecsData *ecsDataStore
expected []*targetgroup.Group
}{
{
name: "SingleClusterWithTasks",
ecsData: &ecsDataStore{
region: "us-west-2",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("test-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
Tags: []ecsTypes.Tag{
{Key: strptr("Environment"), Value: strptr("test")},
},
},
},
services: []ecsTypes.Service{
{
ServiceName: strptr("web-service"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
Tags: []ecsTypes.Tag{
{Key: strptr("App"), Value: strptr("web")},
},
},
},
tasks: []ecsTypes.Task{
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
Group: strptr("service:web-service"),
LaunchType: ecsTypes.LaunchTypeFargate,
LastStatus: strptr("RUNNING"),
DesiredStatus: strptr("RUNNING"),
HealthStatus: ecsTypes.HealthStatusHealthy,
AvailabilityZone: strptr("us-west-2a"),
PlatformFamily: strptr("Linux"),
PlatformVersion: strptr("1.4.0"),
Attachments: []ecsTypes.Attachment{
{
Type: strptr("ElasticNetworkInterface"),
Details: []ecsTypes.KeyValuePair{
{Name: strptr("subnetId"), Value: strptr("subnet-12345")},
{Name: strptr("privateIPv4Address"), Value: strptr("10.0.1.100")},
},
},
},
Tags: []ecsTypes.Tag{
{Key: strptr("Version"), Value: strptr("v1.0")},
},
},
},
},
expected: []*targetgroup.Group{
{
Source: "us-west-2",
Targets: []model.LabelSet{
{
model.AddressLabel: model.LabelValue("10.0.1.100:80"),
"__meta_ecs_cluster": model.LabelValue("test-cluster"),
"__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
"__meta_ecs_service": model.LabelValue("web-service"),
"__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/web-service"),
"__meta_ecs_service_status": model.LabelValue("ACTIVE"),
"__meta_ecs_task_group": model.LabelValue("service:web-service"),
"__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
"__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/web-task:1"),
"__meta_ecs_region": model.LabelValue("us-west-2"),
"__meta_ecs_availability_zone": model.LabelValue("us-west-2a"),
"__meta_ecs_subnet_id": model.LabelValue("subnet-12345"),
"__meta_ecs_ip_address": model.LabelValue("10.0.1.100"),
"__meta_ecs_launch_type": model.LabelValue("FARGATE"),
"__meta_ecs_desired_status": model.LabelValue("RUNNING"),
"__meta_ecs_last_status": model.LabelValue("RUNNING"),
"__meta_ecs_health_status": model.LabelValue("HEALTHY"),
"__meta_ecs_platform_family": model.LabelValue("Linux"),
"__meta_ecs_platform_version": model.LabelValue("1.4.0"),
"__meta_ecs_tag_cluster_Environment": model.LabelValue("test"),
"__meta_ecs_tag_service_App": model.LabelValue("web"),
"__meta_ecs_tag_task_Version": model.LabelValue("v1.0"),
},
},
},
},
},
{
name: "NoTasks",
ecsData: &ecsDataStore{
region: "us-east-1",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("empty-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/empty-cluster"),
Status: strptr("ACTIVE"),
},
},
services: []ecsTypes.Service{
{
ServiceName: strptr("empty-service"),
ServiceArn: strptr("arn:aws:ecs:us-east-1:123456789012:service/empty-cluster/empty-service"),
ClusterArn: strptr("arn:aws:ecs:us-east-1:123456789012:cluster/empty-cluster"),
Status: strptr("ACTIVE"),
},
},
tasks: []ecsTypes.Task{},
},
expected: []*targetgroup.Group{
{
Source: "us-east-1",
},
},
},
{
name: "TaskWithoutENI",
ecsData: &ecsDataStore{
region: "us-west-2",
clusters: []ecsTypes.Cluster{
{
ClusterName: strptr("test-cluster"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
},
},
services: []ecsTypes.Service{
{
ServiceName: strptr("service-1"),
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/service-1"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
Status: strptr("ACTIVE"),
},
},
tasks: []ecsTypes.Task{
{
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-1"),
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/task-def:1"),
Group: strptr("service:service-1"),
LaunchType: ecsTypes.LaunchTypeEc2,
LastStatus: strptr("RUNNING"),
DesiredStatus: strptr("RUNNING"),
HealthStatus: ecsTypes.HealthStatusHealthy,
AvailabilityZone: strptr("us-west-2a"),
// No attachments - should be skipped
Attachments: []ecsTypes.Attachment{},
},
},
},
expected: []*targetgroup.Group{
{
Source: "us-west-2",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := newMockECSClient(tt.ecsData)
d := &ECSDiscovery{
ecs: client,
cfg: &ECSSDConfig{
Region: tt.ecsData.region,
Port: 80,
RequestConcurrency: 1,
},
}
groups, err := d.refresh(ctx)
require.NoError(t, err)
require.Equal(t, tt.expected, groups)
})
}
}
// ECS client mock.
type mockECSClient struct {
ecsData ecsDataStore
}
func newMockECSClient(ecsData *ecsDataStore) *mockECSClient {
client := mockECSClient{
ecsData: *ecsData,
}
return &client
}
func (m *mockECSClient) ListClusters(_ context.Context, _ *ecs.ListClustersInput, _ ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
clusterArns := make([]string, 0, len(m.ecsData.clusters))
for _, cluster := range m.ecsData.clusters {
clusterArns = append(clusterArns, *cluster.ClusterArn)
}
return &ecs.ListClustersOutput{
ClusterArns: clusterArns,
}, nil
}
func (m *mockECSClient) DescribeClusters(_ context.Context, input *ecs.DescribeClustersInput, _ ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) {
var clusters []ecsTypes.Cluster
for _, clusterArn := range input.Clusters {
for _, cluster := range m.ecsData.clusters {
if *cluster.ClusterArn == clusterArn {
clusters = append(clusters, cluster)
break
}
}
}
return &ecs.DescribeClustersOutput{
Clusters: clusters,
}, nil
}
func (m *mockECSClient) ListServices(_ context.Context, input *ecs.ListServicesInput, _ ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) {
var serviceArns []string
for _, service := range m.ecsData.services {
if *service.ClusterArn == *input.Cluster {
serviceArns = append(serviceArns, *service.ServiceArn)
}
}
return &ecs.ListServicesOutput{
ServiceArns: serviceArns,
}, nil
}
func (m *mockECSClient) DescribeServices(_ context.Context, input *ecs.DescribeServicesInput, _ ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) {
var services []ecsTypes.Service
for _, serviceArn := range input.Services {
for _, service := range m.ecsData.services {
if *service.ServiceArn == serviceArn && *service.ClusterArn == *input.Cluster {
services = append(services, service)
break
}
}
}
return &ecs.DescribeServicesOutput{
Services: services,
}, nil
}
func (m *mockECSClient) ListTasks(_ context.Context, input *ecs.ListTasksInput, _ ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
var taskArns []string
for _, task := range m.ecsData.tasks {
if *task.ClusterArn == *input.Cluster {
// If ServiceName is specified, filter by service
if input.ServiceName != nil {
expectedGroup := "service:" + *input.ServiceName
if task.Group != nil && *task.Group == expectedGroup {
taskArns = append(taskArns, *task.TaskArn)
}
} else {
taskArns = append(taskArns, *task.TaskArn)
}
}
}
return &ecs.ListTasksOutput{
TaskArns: taskArns,
}, nil
}
func (m *mockECSClient) DescribeTasks(_ context.Context, input *ecs.DescribeTasksInput, _ ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
var tasks []ecsTypes.Task
for _, taskArn := range input.Tasks {
for _, task := range m.ecsData.tasks {
if *task.TaskArn == taskArn && *task.ClusterArn == *input.Cluster {
tasks = append(tasks, task)
break
}
}
}
return &ecs.DescribeTasksOutput{
Tasks: tasks,
}, nil
}