mirror of https://github.com/prometheus/prometheus
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
434 lines
13 KiB
434 lines
13 KiB
// Copyright 2024 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" |
|
"errors" |
|
"testing" |
|
|
|
"github.com/aws/aws-sdk-go/aws" |
|
"github.com/aws/aws-sdk-go/aws/request" |
|
"github.com/aws/aws-sdk-go/service/ec2" |
|
"github.com/aws/aws-sdk-go/service/ec2/ec2iface" |
|
"github.com/prometheus/common/model" |
|
"github.com/stretchr/testify/require" |
|
"go.uber.org/goleak" |
|
|
|
"github.com/prometheus/prometheus/discovery/targetgroup" |
|
) |
|
|
|
// Helper function to get pointers on literals. |
|
// NOTE: this is common between a few tests. In the future it might worth to move this out into a separate package. |
|
func strptr(str string) *string { |
|
return &str |
|
} |
|
|
|
func boolptr(b bool) *bool { |
|
return &b |
|
} |
|
|
|
func int64ptr(i int64) *int64 { |
|
return &i |
|
} |
|
|
|
// Struct for test data. |
|
type ec2DataStore struct { |
|
region string |
|
|
|
azToAZID map[string]string |
|
|
|
ownerID string |
|
|
|
instances []*ec2.Instance |
|
} |
|
|
|
// The tests itself. |
|
func TestMain(m *testing.M) { |
|
goleak.VerifyTestMain(m) |
|
} |
|
|
|
func TestEC2DiscoveryRefreshAZIDs(t *testing.T) { |
|
ctx := context.Background() |
|
|
|
// iterate through the test cases |
|
for _, tt := range []struct { |
|
name string |
|
shouldFail bool |
|
ec2Data *ec2DataStore |
|
}{ |
|
{ |
|
name: "Normal", |
|
shouldFail: false, |
|
ec2Data: &ec2DataStore{ |
|
azToAZID: map[string]string{ |
|
"azname-a": "azid-1", |
|
"azname-b": "azid-2", |
|
"azname-c": "azid-3", |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "HandleError", |
|
shouldFail: true, |
|
ec2Data: &ec2DataStore{}, |
|
}, |
|
} { |
|
t.Run(tt.name, func(t *testing.T) { |
|
client := newMockEC2Client(tt.ec2Data) |
|
|
|
d := &EC2Discovery{ |
|
ec2: client, |
|
} |
|
|
|
err := d.refreshAZIDs(ctx) |
|
if tt.shouldFail { |
|
require.Error(t, err) |
|
} else { |
|
require.NoError(t, err) |
|
require.Equal(t, client.ec2Data.azToAZID, d.azToAZID) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
func TestEC2DiscoveryRefresh(t *testing.T) { |
|
ctx := context.Background() |
|
|
|
// iterate through the test cases |
|
for _, tt := range []struct { |
|
name string |
|
ec2Data *ec2DataStore |
|
expected []*targetgroup.Group |
|
}{ |
|
{ |
|
name: "NoPrivateIp", |
|
ec2Data: &ec2DataStore{ |
|
region: "region-noprivateip", |
|
azToAZID: map[string]string{ |
|
"azname-a": "azid-1", |
|
"azname-b": "azid-2", |
|
"azname-c": "azid-3", |
|
}, |
|
instances: []*ec2.Instance{ |
|
{ |
|
InstanceId: strptr("instance-id-noprivateip"), |
|
}, |
|
}, |
|
}, |
|
expected: []*targetgroup.Group{ |
|
{ |
|
Source: "region-noprivateip", |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "NoVpc", |
|
ec2Data: &ec2DataStore{ |
|
region: "region-novpc", |
|
azToAZID: map[string]string{ |
|
"azname-a": "azid-1", |
|
"azname-b": "azid-2", |
|
"azname-c": "azid-3", |
|
}, |
|
ownerID: "owner-id-novpc", |
|
instances: []*ec2.Instance{ |
|
{ |
|
// set every possible options and test them here |
|
Architecture: strptr("architecture-novpc"), |
|
ImageId: strptr("ami-novpc"), |
|
InstanceId: strptr("instance-id-novpc"), |
|
InstanceLifecycle: strptr("instance-lifecycle-novpc"), |
|
InstanceType: strptr("instance-type-novpc"), |
|
Placement: &ec2.Placement{AvailabilityZone: strptr("azname-b")}, |
|
Platform: strptr("platform-novpc"), |
|
PrivateDnsName: strptr("private-dns-novpc"), |
|
PrivateIpAddress: strptr("1.2.3.4"), |
|
PublicDnsName: strptr("public-dns-novpc"), |
|
PublicIpAddress: strptr("42.42.42.2"), |
|
State: &ec2.InstanceState{Name: strptr("running")}, |
|
// test tags once and for all |
|
Tags: []*ec2.Tag{ |
|
{Key: strptr("tag-1-key"), Value: strptr("tag-1-value")}, |
|
{Key: strptr("tag-2-key"), Value: strptr("tag-2-value")}, |
|
nil, |
|
{Value: strptr("tag-4-value")}, |
|
{Key: strptr("tag-5-key")}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
expected: []*targetgroup.Group{ |
|
{ |
|
Source: "region-novpc", |
|
Targets: []model.LabelSet{ |
|
{ |
|
"__address__": model.LabelValue("1.2.3.4:4242"), |
|
"__meta_ec2_ami": model.LabelValue("ami-novpc"), |
|
"__meta_ec2_architecture": model.LabelValue("architecture-novpc"), |
|
"__meta_ec2_availability_zone": model.LabelValue("azname-b"), |
|
"__meta_ec2_availability_zone_id": model.LabelValue("azid-2"), |
|
"__meta_ec2_instance_id": model.LabelValue("instance-id-novpc"), |
|
"__meta_ec2_instance_lifecycle": model.LabelValue("instance-lifecycle-novpc"), |
|
"__meta_ec2_instance_type": model.LabelValue("instance-type-novpc"), |
|
"__meta_ec2_instance_state": model.LabelValue("running"), |
|
"__meta_ec2_owner_id": model.LabelValue("owner-id-novpc"), |
|
"__meta_ec2_platform": model.LabelValue("platform-novpc"), |
|
"__meta_ec2_private_dns_name": model.LabelValue("private-dns-novpc"), |
|
"__meta_ec2_private_ip": model.LabelValue("1.2.3.4"), |
|
"__meta_ec2_public_dns_name": model.LabelValue("public-dns-novpc"), |
|
"__meta_ec2_public_ip": model.LabelValue("42.42.42.2"), |
|
"__meta_ec2_region": model.LabelValue("region-novpc"), |
|
"__meta_ec2_tag_tag_1_key": model.LabelValue("tag-1-value"), |
|
"__meta_ec2_tag_tag_2_key": model.LabelValue("tag-2-value"), |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "Ipv4", |
|
ec2Data: &ec2DataStore{ |
|
region: "region-ipv4", |
|
azToAZID: map[string]string{ |
|
"azname-a": "azid-1", |
|
"azname-b": "azid-2", |
|
"azname-c": "azid-3", |
|
}, |
|
instances: []*ec2.Instance{ |
|
{ |
|
// just the minimum needed for the refresh work |
|
ImageId: strptr("ami-ipv4"), |
|
InstanceId: strptr("instance-id-ipv4"), |
|
InstanceType: strptr("instance-type-ipv4"), |
|
Placement: &ec2.Placement{AvailabilityZone: strptr("azname-c")}, |
|
PrivateIpAddress: strptr("5.6.7.8"), |
|
State: &ec2.InstanceState{Name: strptr("running")}, |
|
SubnetId: strptr("azid-3"), |
|
VpcId: strptr("vpc-ipv4"), |
|
// network intefaces |
|
NetworkInterfaces: []*ec2.InstanceNetworkInterface{ |
|
// interface without subnet -> should be ignored |
|
{ |
|
Ipv6Addresses: []*ec2.InstanceIpv6Address{ |
|
{ |
|
Ipv6Address: strptr("2001:db8:1::1"), |
|
IsPrimaryIpv6: boolptr(true), |
|
}, |
|
}, |
|
}, |
|
// interface with subnet, no IPv6 |
|
{ |
|
Ipv6Addresses: []*ec2.InstanceIpv6Address{}, |
|
SubnetId: strptr("azid-3"), |
|
}, |
|
// interface with another subnet, no IPv6 |
|
{ |
|
Ipv6Addresses: []*ec2.InstanceIpv6Address{}, |
|
SubnetId: strptr("azid-1"), |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
expected: []*targetgroup.Group{ |
|
{ |
|
Source: "region-ipv4", |
|
Targets: []model.LabelSet{ |
|
{ |
|
"__address__": model.LabelValue("5.6.7.8:4242"), |
|
"__meta_ec2_ami": model.LabelValue("ami-ipv4"), |
|
"__meta_ec2_availability_zone": model.LabelValue("azname-c"), |
|
"__meta_ec2_availability_zone_id": model.LabelValue("azid-3"), |
|
"__meta_ec2_instance_id": model.LabelValue("instance-id-ipv4"), |
|
"__meta_ec2_instance_state": model.LabelValue("running"), |
|
"__meta_ec2_instance_type": model.LabelValue("instance-type-ipv4"), |
|
"__meta_ec2_owner_id": model.LabelValue(""), |
|
"__meta_ec2_primary_subnet_id": model.LabelValue("azid-3"), |
|
"__meta_ec2_private_ip": model.LabelValue("5.6.7.8"), |
|
"__meta_ec2_region": model.LabelValue("region-ipv4"), |
|
"__meta_ec2_subnet_id": model.LabelValue(",azid-3,azid-1,"), |
|
"__meta_ec2_vpc_id": model.LabelValue("vpc-ipv4"), |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "Ipv6", |
|
ec2Data: &ec2DataStore{ |
|
region: "region-ipv6", |
|
azToAZID: map[string]string{ |
|
"azname-a": "azid-1", |
|
"azname-b": "azid-2", |
|
"azname-c": "azid-3", |
|
}, |
|
instances: []*ec2.Instance{ |
|
{ |
|
// just the minimum needed for the refresh work |
|
ImageId: strptr("ami-ipv6"), |
|
InstanceId: strptr("instance-id-ipv6"), |
|
InstanceType: strptr("instance-type-ipv6"), |
|
Placement: &ec2.Placement{AvailabilityZone: strptr("azname-b")}, |
|
PrivateIpAddress: strptr("9.10.11.12"), |
|
State: &ec2.InstanceState{Name: strptr("running")}, |
|
SubnetId: strptr("azid-2"), |
|
VpcId: strptr("vpc-ipv6"), |
|
// network intefaces |
|
NetworkInterfaces: []*ec2.InstanceNetworkInterface{ |
|
// interface without primary IPv6, index 2 |
|
{ |
|
Attachment: &ec2.InstanceNetworkInterfaceAttachment{ |
|
DeviceIndex: int64ptr(3), |
|
}, |
|
Ipv6Addresses: []*ec2.InstanceIpv6Address{ |
|
{ |
|
Ipv6Address: strptr("2001:db8:2::1:1"), |
|
IsPrimaryIpv6: boolptr(false), |
|
}, |
|
}, |
|
SubnetId: strptr("azid-2"), |
|
}, |
|
// interface with primary IPv6, index 1 |
|
{ |
|
Attachment: &ec2.InstanceNetworkInterfaceAttachment{ |
|
DeviceIndex: int64ptr(1), |
|
}, |
|
Ipv6Addresses: []*ec2.InstanceIpv6Address{ |
|
{ |
|
Ipv6Address: strptr("2001:db8:2::2:1"), |
|
IsPrimaryIpv6: boolptr(false), |
|
}, |
|
{ |
|
Ipv6Address: strptr("2001:db8:2::2:2"), |
|
IsPrimaryIpv6: boolptr(true), |
|
}, |
|
}, |
|
SubnetId: strptr("azid-2"), |
|
}, |
|
// interface with primary IPv6, index 3 |
|
{ |
|
Attachment: &ec2.InstanceNetworkInterfaceAttachment{ |
|
DeviceIndex: int64ptr(3), |
|
}, |
|
Ipv6Addresses: []*ec2.InstanceIpv6Address{ |
|
{ |
|
Ipv6Address: strptr("2001:db8:2::3:1"), |
|
IsPrimaryIpv6: boolptr(true), |
|
}, |
|
}, |
|
SubnetId: strptr("azid-1"), |
|
}, |
|
// interface without primary IPv6, index 0 |
|
{ |
|
Attachment: &ec2.InstanceNetworkInterfaceAttachment{ |
|
DeviceIndex: int64ptr(0), |
|
}, |
|
Ipv6Addresses: []*ec2.InstanceIpv6Address{}, |
|
SubnetId: strptr("azid-3"), |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
expected: []*targetgroup.Group{ |
|
{ |
|
Source: "region-ipv6", |
|
Targets: []model.LabelSet{ |
|
{ |
|
"__address__": model.LabelValue("9.10.11.12:4242"), |
|
"__meta_ec2_ami": model.LabelValue("ami-ipv6"), |
|
"__meta_ec2_availability_zone": model.LabelValue("azname-b"), |
|
"__meta_ec2_availability_zone_id": model.LabelValue("azid-2"), |
|
"__meta_ec2_instance_id": model.LabelValue("instance-id-ipv6"), |
|
"__meta_ec2_instance_state": model.LabelValue("running"), |
|
"__meta_ec2_instance_type": model.LabelValue("instance-type-ipv6"), |
|
"__meta_ec2_ipv6_addresses": model.LabelValue(",2001:db8:2::1:1,2001:db8:2::2:1,2001:db8:2::2:2,2001:db8:2::3:1,"), |
|
"__meta_ec2_owner_id": model.LabelValue(""), |
|
"__meta_ec2_primary_ipv6_addresses": model.LabelValue(",,2001:db8:2::2:2,,2001:db8:2::3:1,"), |
|
"__meta_ec2_primary_subnet_id": model.LabelValue("azid-2"), |
|
"__meta_ec2_private_ip": model.LabelValue("9.10.11.12"), |
|
"__meta_ec2_region": model.LabelValue("region-ipv6"), |
|
"__meta_ec2_subnet_id": model.LabelValue(",azid-2,azid-1,azid-3,"), |
|
"__meta_ec2_vpc_id": model.LabelValue("vpc-ipv6"), |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
} { |
|
t.Run(tt.name, func(t *testing.T) { |
|
client := newMockEC2Client(tt.ec2Data) |
|
|
|
d := &EC2Discovery{ |
|
ec2: client, |
|
cfg: &EC2SDConfig{ |
|
Port: 4242, |
|
Region: client.ec2Data.region, |
|
}, |
|
} |
|
|
|
g, err := d.refresh(ctx) |
|
require.NoError(t, err) |
|
require.Equal(t, tt.expected, g) |
|
}) |
|
} |
|
} |
|
|
|
// EC2 client mock. |
|
type mockEC2Client struct { |
|
ec2iface.EC2API |
|
ec2Data ec2DataStore |
|
} |
|
|
|
func newMockEC2Client(ec2Data *ec2DataStore) *mockEC2Client { |
|
client := mockEC2Client{ |
|
ec2Data: *ec2Data, |
|
} |
|
return &client |
|
} |
|
|
|
func (m *mockEC2Client) DescribeAvailabilityZonesWithContext(ctx aws.Context, input *ec2.DescribeAvailabilityZonesInput, opts ...request.Option) (*ec2.DescribeAvailabilityZonesOutput, error) { |
|
if len(m.ec2Data.azToAZID) == 0 { |
|
return nil, errors.New("No AZs found") |
|
} |
|
|
|
azs := make([]*ec2.AvailabilityZone, len(m.ec2Data.azToAZID)) |
|
|
|
i := 0 |
|
for k, v := range m.ec2Data.azToAZID { |
|
azs[i] = &ec2.AvailabilityZone{ |
|
ZoneName: strptr(k), |
|
ZoneId: strptr(v), |
|
} |
|
i++ |
|
} |
|
|
|
return &ec2.DescribeAvailabilityZonesOutput{ |
|
AvailabilityZones: azs, |
|
}, nil |
|
} |
|
|
|
func (m *mockEC2Client) DescribeInstancesPagesWithContext(ctx aws.Context, input *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool, opts ...request.Option) error { |
|
r := ec2.Reservation{} |
|
r.SetInstances(m.ec2Data.instances) |
|
r.SetOwnerId(m.ec2Data.ownerID) |
|
|
|
o := ec2.DescribeInstancesOutput{} |
|
o.SetReservations([]*ec2.Reservation{&r}) |
|
|
|
_ = fn(&o, true) |
|
|
|
return nil |
|
}
|
|
|