mirror of
https://github.com/prometheus/prometheus.git
synced 2026-02-13 11:51:03 +01:00
Previously the AWS SD ECS Role only discovered instances that used `awsvpc` network mode, which attaches a dedicated Elastic Network Interface (ENI). This change adds in additional logic so that we discover instances that are using `host` and `bridge` networking modes, where the IP address is that of the EC2 instance that is hosting the container. Also this change exposes a number of additional labels that relate to the EC2 instance when the launch type is `EC2`. Signed-off-by: matt-gp <small_minority@hotmail.com>
1384 lines
52 KiB
Go
1384 lines
52 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/ec2"
|
|
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
|
"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
|
|
containerInstances []ecsTypes.ContainerInstance
|
|
ec2Instances map[string]ec2InstanceInfo // EC2 instance ID to instance info
|
|
eniPublicIPs map[string]string // ENI ID to public IP
|
|
}
|
|
|
|
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")},
|
|
{Name: strptr("networkInterfaceId"), Value: strptr("eni-fargate-123")},
|
|
},
|
|
},
|
|
},
|
|
Tags: []ecsTypes.Tag{
|
|
{Key: strptr("Version"), Value: strptr("v1.0")},
|
|
},
|
|
},
|
|
},
|
|
eniPublicIPs: map[string]string{
|
|
"eni-fargate-123": "52.1.2.3",
|
|
},
|
|
},
|
|
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_network_mode": model.LabelValue("awsvpc"),
|
|
"__meta_ecs_public_ip": model.LabelValue("52.1.2.3"),
|
|
"__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",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "TaskWithBridgeNetworking",
|
|
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("bridge-service"),
|
|
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/bridge-service"),
|
|
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-bridge"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/test-cluster"),
|
|
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
|
|
Group: strptr("service:bridge-service"),
|
|
LaunchType: ecsTypes.LaunchTypeEc2,
|
|
LastStatus: strptr("RUNNING"),
|
|
DesiredStatus: strptr("RUNNING"),
|
|
HealthStatus: ecsTypes.HealthStatusHealthy,
|
|
AvailabilityZone: strptr("us-west-2a"),
|
|
ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
|
|
Attachments: []ecsTypes.Attachment{},
|
|
},
|
|
},
|
|
containerInstances: []ecsTypes.ContainerInstance{
|
|
{
|
|
ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
|
|
Ec2InstanceId: strptr("i-1234567890abcdef0"),
|
|
Status: strptr("ACTIVE"),
|
|
},
|
|
},
|
|
ec2Instances: map[string]ec2InstanceInfo{
|
|
"i-1234567890abcdef0": {
|
|
privateIP: "10.0.1.50",
|
|
publicIP: "54.1.2.3",
|
|
subnetID: "subnet-bridge-1",
|
|
instanceType: "t3.medium",
|
|
tags: map[string]string{
|
|
"Name": "ecs-host-1",
|
|
"Environment": "production",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: []*targetgroup.Group{
|
|
{
|
|
Source: "us-west-2",
|
|
Targets: []model.LabelSet{
|
|
{
|
|
model.AddressLabel: model.LabelValue("10.0.1.50: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("bridge-service"),
|
|
"__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/test-cluster/bridge-service"),
|
|
"__meta_ecs_service_status": model.LabelValue("ACTIVE"),
|
|
"__meta_ecs_task_group": model.LabelValue("service:bridge-service"),
|
|
"__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/test-cluster/task-bridge"),
|
|
"__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
|
|
"__meta_ecs_region": model.LabelValue("us-west-2"),
|
|
"__meta_ecs_availability_zone": model.LabelValue("us-west-2a"),
|
|
"__meta_ecs_ip_address": model.LabelValue("10.0.1.50"),
|
|
"__meta_ecs_subnet_id": model.LabelValue("subnet-bridge-1"),
|
|
"__meta_ecs_launch_type": model.LabelValue("EC2"),
|
|
"__meta_ecs_desired_status": model.LabelValue("RUNNING"),
|
|
"__meta_ecs_last_status": model.LabelValue("RUNNING"),
|
|
"__meta_ecs_health_status": model.LabelValue("HEALTHY"),
|
|
"__meta_ecs_network_mode": model.LabelValue("bridge"),
|
|
"__meta_ecs_container_instance_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:container-instance/test-cluster/abc123"),
|
|
"__meta_ecs_ec2_instance_id": model.LabelValue("i-1234567890abcdef0"),
|
|
"__meta_ecs_ec2_instance_type": model.LabelValue("t3.medium"),
|
|
"__meta_ecs_ec2_instance_private_ip": model.LabelValue("10.0.1.50"),
|
|
"__meta_ecs_ec2_instance_public_ip": model.LabelValue("54.1.2.3"),
|
|
"__meta_ecs_public_ip": model.LabelValue("54.1.2.3"),
|
|
"__meta_ecs_tag_ec2_Name": model.LabelValue("ecs-host-1"),
|
|
"__meta_ecs_tag_ec2_Environment": model.LabelValue("production"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "MixedNetworkingModes",
|
|
ecsData: &ecsDataStore{
|
|
region: "us-west-2",
|
|
clusters: []ecsTypes.Cluster{
|
|
{
|
|
ClusterName: strptr("mixed-cluster"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
|
|
Status: strptr("ACTIVE"),
|
|
},
|
|
},
|
|
services: []ecsTypes.Service{
|
|
{
|
|
ServiceName: strptr("mixed-service"),
|
|
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/mixed-cluster/mixed-service"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
|
|
Status: strptr("ACTIVE"),
|
|
},
|
|
},
|
|
tasks: []ecsTypes.Task{
|
|
{
|
|
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-awsvpc"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
|
|
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/awsvpc-task:1"),
|
|
Group: strptr("service:mixed-service"),
|
|
LaunchType: ecsTypes.LaunchTypeFargate,
|
|
LastStatus: strptr("RUNNING"),
|
|
DesiredStatus: strptr("RUNNING"),
|
|
HealthStatus: ecsTypes.HealthStatusHealthy,
|
|
AvailabilityZone: strptr("us-west-2a"),
|
|
Attachments: []ecsTypes.Attachment{
|
|
{
|
|
Type: strptr("ElasticNetworkInterface"),
|
|
Details: []ecsTypes.KeyValuePair{
|
|
{Name: strptr("subnetId"), Value: strptr("subnet-12345")},
|
|
{Name: strptr("privateIPv4Address"), Value: strptr("10.0.2.100")},
|
|
{Name: strptr("networkInterfaceId"), Value: strptr("eni-mixed-awsvpc")},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-bridge"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
|
|
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
|
|
Group: strptr("service:mixed-service"),
|
|
LaunchType: ecsTypes.LaunchTypeEc2,
|
|
LastStatus: strptr("RUNNING"),
|
|
DesiredStatus: strptr("RUNNING"),
|
|
HealthStatus: ecsTypes.HealthStatusHealthy,
|
|
AvailabilityZone: strptr("us-west-2b"),
|
|
ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/mixed-cluster/xyz789"),
|
|
Attachments: []ecsTypes.Attachment{},
|
|
},
|
|
},
|
|
containerInstances: []ecsTypes.ContainerInstance{
|
|
{
|
|
ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/mixed-cluster/xyz789"),
|
|
Ec2InstanceId: strptr("i-0987654321fedcba0"),
|
|
Status: strptr("ACTIVE"),
|
|
},
|
|
},
|
|
ec2Instances: map[string]ec2InstanceInfo{
|
|
"i-0987654321fedcba0": {
|
|
privateIP: "10.0.1.75",
|
|
publicIP: "54.2.3.4",
|
|
subnetID: "subnet-bridge-2",
|
|
instanceType: "t3.large",
|
|
tags: map[string]string{
|
|
"Name": "mixed-host",
|
|
"Team": "platform",
|
|
},
|
|
},
|
|
},
|
|
eniPublicIPs: map[string]string{
|
|
"eni-mixed-awsvpc": "52.2.3.4",
|
|
},
|
|
},
|
|
expected: []*targetgroup.Group{
|
|
{
|
|
Source: "us-west-2",
|
|
Targets: []model.LabelSet{
|
|
{
|
|
model.AddressLabel: model.LabelValue("10.0.2.100:80"),
|
|
"__meta_ecs_cluster": model.LabelValue("mixed-cluster"),
|
|
"__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
|
|
"__meta_ecs_service": model.LabelValue("mixed-service"),
|
|
"__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/mixed-cluster/mixed-service"),
|
|
"__meta_ecs_service_status": model.LabelValue("ACTIVE"),
|
|
"__meta_ecs_task_group": model.LabelValue("service:mixed-service"),
|
|
"__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-awsvpc"),
|
|
"__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/awsvpc-task:1"),
|
|
"__meta_ecs_region": model.LabelValue("us-west-2"),
|
|
"__meta_ecs_availability_zone": model.LabelValue("us-west-2a"),
|
|
"__meta_ecs_ip_address": model.LabelValue("10.0.2.100"),
|
|
"__meta_ecs_subnet_id": model.LabelValue("subnet-12345"),
|
|
"__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_network_mode": model.LabelValue("awsvpc"),
|
|
"__meta_ecs_public_ip": model.LabelValue("52.2.3.4"),
|
|
},
|
|
{
|
|
model.AddressLabel: model.LabelValue("10.0.1.75:80"),
|
|
"__meta_ecs_cluster": model.LabelValue("mixed-cluster"),
|
|
"__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/mixed-cluster"),
|
|
"__meta_ecs_service": model.LabelValue("mixed-service"),
|
|
"__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/mixed-cluster/mixed-service"),
|
|
"__meta_ecs_service_status": model.LabelValue("ACTIVE"),
|
|
"__meta_ecs_task_group": model.LabelValue("service:mixed-service"),
|
|
"__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/mixed-cluster/task-bridge"),
|
|
"__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/bridge-task:1"),
|
|
"__meta_ecs_region": model.LabelValue("us-west-2"),
|
|
"__meta_ecs_availability_zone": model.LabelValue("us-west-2b"),
|
|
"__meta_ecs_ip_address": model.LabelValue("10.0.1.75"),
|
|
"__meta_ecs_subnet_id": model.LabelValue("subnet-bridge-2"),
|
|
"__meta_ecs_launch_type": model.LabelValue("EC2"),
|
|
"__meta_ecs_desired_status": model.LabelValue("RUNNING"),
|
|
"__meta_ecs_last_status": model.LabelValue("RUNNING"),
|
|
"__meta_ecs_health_status": model.LabelValue("HEALTHY"),
|
|
"__meta_ecs_network_mode": model.LabelValue("bridge"),
|
|
"__meta_ecs_container_instance_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:container-instance/mixed-cluster/xyz789"),
|
|
"__meta_ecs_ec2_instance_id": model.LabelValue("i-0987654321fedcba0"),
|
|
"__meta_ecs_ec2_instance_type": model.LabelValue("t3.large"),
|
|
"__meta_ecs_ec2_instance_private_ip": model.LabelValue("10.0.1.75"),
|
|
"__meta_ecs_ec2_instance_public_ip": model.LabelValue("54.2.3.4"),
|
|
"__meta_ecs_public_ip": model.LabelValue("54.2.3.4"),
|
|
"__meta_ecs_tag_ec2_Name": model.LabelValue("mixed-host"),
|
|
"__meta_ecs_tag_ec2_Team": model.LabelValue("platform"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "EC2WithAwsvpcNetworking",
|
|
ecsData: &ecsDataStore{
|
|
region: "us-west-2",
|
|
clusters: []ecsTypes.Cluster{
|
|
{
|
|
ClusterName: strptr("ec2-awsvpc-cluster"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
|
|
Status: strptr("ACTIVE"),
|
|
},
|
|
},
|
|
services: []ecsTypes.Service{
|
|
{
|
|
ServiceName: strptr("ec2-awsvpc-service"),
|
|
ServiceArn: strptr("arn:aws:ecs:us-west-2:123456789012:service/ec2-awsvpc-cluster/ec2-awsvpc-service"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
|
|
Status: strptr("ACTIVE"),
|
|
},
|
|
},
|
|
tasks: []ecsTypes.Task{
|
|
{
|
|
TaskArn: strptr("arn:aws:ecs:us-west-2:123456789012:task/ec2-awsvpc-cluster/task-ec2-awsvpc"),
|
|
ClusterArn: strptr("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
|
|
TaskDefinitionArn: strptr("arn:aws:ecs:us-west-2:123456789012:task-definition/ec2-awsvpc-task:1"),
|
|
Group: strptr("service:ec2-awsvpc-service"),
|
|
LaunchType: ecsTypes.LaunchTypeEc2,
|
|
LastStatus: strptr("RUNNING"),
|
|
DesiredStatus: strptr("RUNNING"),
|
|
HealthStatus: ecsTypes.HealthStatusHealthy,
|
|
AvailabilityZone: strptr("us-west-2c"),
|
|
ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/ec2-awsvpc-cluster/def456"),
|
|
// Has BOTH ENI attachment AND container instance ARN - should use ENI
|
|
Attachments: []ecsTypes.Attachment{
|
|
{
|
|
Type: strptr("ElasticNetworkInterface"),
|
|
Details: []ecsTypes.KeyValuePair{
|
|
{Name: strptr("subnetId"), Value: strptr("subnet-99999")},
|
|
{Name: strptr("privateIPv4Address"), Value: strptr("10.0.3.200")},
|
|
{Name: strptr("networkInterfaceId"), Value: strptr("eni-ec2-awsvpc")},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
eniPublicIPs: map[string]string{
|
|
"eni-ec2-awsvpc": "52.3.4.5",
|
|
},
|
|
// Container instance data - IP should NOT be used, but instance type SHOULD be used
|
|
containerInstances: []ecsTypes.ContainerInstance{
|
|
{
|
|
ContainerInstanceArn: strptr("arn:aws:ecs:us-west-2:123456789012:container-instance/ec2-awsvpc-cluster/def456"),
|
|
Ec2InstanceId: strptr("i-ec2awsvpcinstance"),
|
|
Status: strptr("ACTIVE"),
|
|
},
|
|
},
|
|
ec2Instances: map[string]ec2InstanceInfo{
|
|
"i-ec2awsvpcinstance": {
|
|
privateIP: "10.0.9.99", // This IP should NOT be used (ENI IP is used instead)
|
|
publicIP: "54.3.4.5", // This public IP SHOULD be exposed
|
|
subnetID: "subnet-wrong", // This subnet should NOT be used (ENI subnet is used instead)
|
|
instanceType: "c5.2xlarge", // This instance type SHOULD be used
|
|
tags: map[string]string{
|
|
"Name": "ec2-awsvpc-host",
|
|
"Owner": "team-a",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: []*targetgroup.Group{
|
|
{
|
|
Source: "us-west-2",
|
|
Targets: []model.LabelSet{
|
|
{
|
|
model.AddressLabel: model.LabelValue("10.0.3.200:80"),
|
|
"__meta_ecs_cluster": model.LabelValue("ec2-awsvpc-cluster"),
|
|
"__meta_ecs_cluster_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:cluster/ec2-awsvpc-cluster"),
|
|
"__meta_ecs_service": model.LabelValue("ec2-awsvpc-service"),
|
|
"__meta_ecs_service_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:service/ec2-awsvpc-cluster/ec2-awsvpc-service"),
|
|
"__meta_ecs_service_status": model.LabelValue("ACTIVE"),
|
|
"__meta_ecs_task_group": model.LabelValue("service:ec2-awsvpc-service"),
|
|
"__meta_ecs_task_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task/ec2-awsvpc-cluster/task-ec2-awsvpc"),
|
|
"__meta_ecs_task_definition": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:task-definition/ec2-awsvpc-task:1"),
|
|
"__meta_ecs_region": model.LabelValue("us-west-2"),
|
|
"__meta_ecs_availability_zone": model.LabelValue("us-west-2c"),
|
|
"__meta_ecs_ip_address": model.LabelValue("10.0.3.200"),
|
|
"__meta_ecs_subnet_id": model.LabelValue("subnet-99999"),
|
|
"__meta_ecs_launch_type": model.LabelValue("EC2"),
|
|
"__meta_ecs_desired_status": model.LabelValue("RUNNING"),
|
|
"__meta_ecs_last_status": model.LabelValue("RUNNING"),
|
|
"__meta_ecs_health_status": model.LabelValue("HEALTHY"),
|
|
"__meta_ecs_network_mode": model.LabelValue("awsvpc"),
|
|
"__meta_ecs_container_instance_arn": model.LabelValue("arn:aws:ecs:us-west-2:123456789012:container-instance/ec2-awsvpc-cluster/def456"),
|
|
"__meta_ecs_ec2_instance_id": model.LabelValue("i-ec2awsvpcinstance"),
|
|
"__meta_ecs_ec2_instance_type": model.LabelValue("c5.2xlarge"),
|
|
"__meta_ecs_ec2_instance_private_ip": model.LabelValue("10.0.9.99"),
|
|
"__meta_ecs_ec2_instance_public_ip": model.LabelValue("54.3.4.5"),
|
|
"__meta_ecs_public_ip": model.LabelValue("52.3.4.5"),
|
|
"__meta_ecs_tag_ec2_Name": model.LabelValue("ec2-awsvpc-host"),
|
|
"__meta_ecs_tag_ec2_Owner": model.LabelValue("team-a"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ecsClient := newMockECSClient(tt.ecsData)
|
|
ec2Client := newMockECSEC2Client(tt.ecsData.ec2Instances, tt.ecsData.eniPublicIPs)
|
|
|
|
d := &ECSDiscovery{
|
|
ecs: ecsClient,
|
|
ec2: ec2Client,
|
|
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
|
|
}
|
|
|
|
func (m *mockECSClient) DescribeContainerInstances(_ context.Context, input *ecs.DescribeContainerInstancesInput, _ ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) {
|
|
var containerInstances []ecsTypes.ContainerInstance
|
|
for _, ciArn := range input.ContainerInstances {
|
|
for _, ci := range m.ecsData.containerInstances {
|
|
if *ci.ContainerInstanceArn == ciArn {
|
|
containerInstances = append(containerInstances, ci)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return &ecs.DescribeContainerInstancesOutput{
|
|
ContainerInstances: containerInstances,
|
|
}, nil
|
|
}
|
|
|
|
// Mock EC2 client wrapper for ECS tests.
|
|
type mockECSEC2Client struct {
|
|
ec2Instances map[string]ec2InstanceInfo
|
|
eniPublicIPs map[string]string
|
|
}
|
|
|
|
func newMockECSEC2Client(ec2Instances map[string]ec2InstanceInfo, eniPublicIPs map[string]string) *mockECSEC2Client {
|
|
return &mockECSEC2Client{
|
|
ec2Instances: ec2Instances,
|
|
eniPublicIPs: eniPublicIPs,
|
|
}
|
|
}
|
|
|
|
func (m *mockECSEC2Client) DescribeInstances(_ context.Context, input *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
|
|
var reservations []ec2Types.Reservation
|
|
|
|
for _, instanceID := range input.InstanceIds {
|
|
if info, ok := m.ec2Instances[instanceID]; ok {
|
|
instance := ec2Types.Instance{
|
|
InstanceId: &instanceID,
|
|
PrivateIpAddress: &info.privateIP,
|
|
}
|
|
if info.publicIP != "" {
|
|
instance.PublicIpAddress = &info.publicIP
|
|
}
|
|
if info.subnetID != "" {
|
|
instance.SubnetId = &info.subnetID
|
|
}
|
|
if info.instanceType != "" {
|
|
instance.InstanceType = ec2Types.InstanceType(info.instanceType)
|
|
}
|
|
// Add tags
|
|
for tagKey, tagValue := range info.tags {
|
|
instance.Tags = append(instance.Tags, ec2Types.Tag{
|
|
Key: &tagKey,
|
|
Value: &tagValue,
|
|
})
|
|
}
|
|
reservation := ec2Types.Reservation{
|
|
Instances: []ec2Types.Instance{instance},
|
|
}
|
|
reservations = append(reservations, reservation)
|
|
}
|
|
}
|
|
|
|
return &ec2.DescribeInstancesOutput{
|
|
Reservations: reservations,
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockECSEC2Client) DescribeNetworkInterfaces(_ context.Context, input *ec2.DescribeNetworkInterfacesInput, _ ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
|
|
var networkInterfaces []ec2Types.NetworkInterface
|
|
|
|
for _, eniID := range input.NetworkInterfaceIds {
|
|
if publicIP, ok := m.eniPublicIPs[eniID]; ok {
|
|
eni := ec2Types.NetworkInterface{
|
|
NetworkInterfaceId: &eniID,
|
|
}
|
|
if publicIP != "" {
|
|
eni.Association = &ec2Types.NetworkInterfaceAssociation{
|
|
PublicIp: &publicIP,
|
|
}
|
|
}
|
|
networkInterfaces = append(networkInterfaces, eni)
|
|
}
|
|
}
|
|
|
|
return &ec2.DescribeNetworkInterfacesOutput{
|
|
NetworkInterfaces: networkInterfaces,
|
|
}, nil
|
|
}
|