metadata: implement GetLoadBalancerEndpoint for AWS

This commit is contained in:
Leonard Cohnen 2022-10-25 00:51:24 +02:00 committed by 3u13r
parent 58d083a433
commit 0430336fdf
6 changed files with 382 additions and 57 deletions

View file

@ -16,7 +16,10 @@ import (
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
tagType "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
"github.com/edgelesssys/constellation/v2/internal/cloud"
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
"github.com/edgelesssys/constellation/v2/internal/role"
@ -26,6 +29,15 @@ const (
tagName = "Name"
)
type resourceAPI interface {
GetResources(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error)
}
type loadbalancerAPI interface {
DescribeLoadBalancers(ctx context.Context, params *elasticloadbalancingv2.DescribeLoadBalancersInput,
optFns ...func(*elasticloadbalancingv2.Options)) (*elasticloadbalancingv2.DescribeLoadBalancersOutput, error)
}
type ec2API interface {
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
}
@ -37,8 +49,10 @@ type imdsAPI interface {
// Metadata implements core.ProviderMetadata interface for AWS.
type Metadata struct {
ec2 ec2API
imds imdsAPI
ec2 ec2API
imds imdsAPI
loadbalancer loadbalancerAPI
resourceapiClient resourceAPI
}
// New initializes a new AWS Metadata client using instance default credentials.
@ -49,8 +63,10 @@ func New(ctx context.Context) (*Metadata, error) {
return nil, err
}
return &Metadata{
ec2: ec2.NewFromConfig(cfg),
imds: imds.New(imds.Options{}),
ec2: ec2.NewFromConfig(cfg),
imds: imds.New(imds.Options{}),
loadbalancer: elasticloadbalancingv2.NewFromConfig(cfg),
resourceapiClient: resourcegroupstaggingapi.NewFromConfig(cfg),
}, nil
}
@ -132,23 +148,81 @@ func (m *Metadata) UID(ctx context.Context) (string, error) {
// SupportsLoadBalancer returns true if the cloud provider supports load balancers.
func (m *Metadata) SupportsLoadBalancer() bool {
return false
return true
}
// GetLoadBalancerEndpoint returns the endpoint of the load balancer.
func (m *Metadata) GetLoadBalancerEndpoint(ctx context.Context) (string, error) {
panic("function *Metadata.GetLoadBalancerEndpoint not implemented")
uid, err := readInstanceTag(ctx, m.imds, cloud.TagUID)
if err != nil {
return "", fmt.Errorf("retrieving uid tag: %w", err)
}
arns, err := m.getARNsByTag(ctx, uid, "elasticloadbalancing:loadbalancer")
if err != nil {
return "", fmt.Errorf("retrieving load balancer ARNs: %w", err)
}
if len(arns) != 1 {
return "", fmt.Errorf("%d load balancers found", len(arns))
}
output, err := m.loadbalancer.DescribeLoadBalancers(ctx, &elasticloadbalancingv2.DescribeLoadBalancersInput{
LoadBalancerArns: arns,
})
if err != nil {
return "", fmt.Errorf("retrieving load balancer: %w", err)
}
if len(output.LoadBalancers) != 1 {
return "", fmt.Errorf("%d load balancers found; expected 1", len(output.LoadBalancers))
}
if len(output.LoadBalancers[0].AvailabilityZones) != 1 {
return "", fmt.Errorf("%d availability zones found; expected 1", len(output.LoadBalancers[0].AvailabilityZones))
}
if len(output.LoadBalancers[0].AvailabilityZones[0].LoadBalancerAddresses) != 1 {
return "", fmt.Errorf("%d load balancer addresses found; expected 1", len(output.LoadBalancers[0].AvailabilityZones[0].LoadBalancerAddresses))
}
if output.LoadBalancers[0].AvailabilityZones[0].LoadBalancerAddresses[0].IpAddress == nil {
return "", errors.New("load balancer address is nil")
}
return *output.LoadBalancers[0].AvailabilityZones[0].LoadBalancerAddresses[0].IpAddress, nil
}
// GetSubnetworkCIDR retrieves the subnetwork CIDR from cloud provider metadata.
func (m *Metadata) GetSubnetworkCIDR(ctx context.Context) (string, error) {
panic("function *Metadata.GetSubnetworkCIDR not implemented")
// getARNsByTag returns a list of ARNs that have the given tag.
func (m *Metadata) getARNsByTag(ctx context.Context, uid, resourceType string) ([]string, error) {
var ARNs []string
resourcesReq := &resourcegroupstaggingapi.GetResourcesInput{
TagFilters: []tagType.TagFilter{
{
Key: aws.String(cloud.TagUID),
Values: []string{uid},
},
},
ResourceTypeFilters: []string{resourceType},
}
for out, err := m.resourceapiClient.GetResources(ctx, resourcesReq); ; out, err = m.resourceapiClient.GetResources(ctx, resourcesReq) {
if err != nil {
return nil, fmt.Errorf("retrieving resources: %w", err)
}
for _, resource := range out.ResourceTagMappingList {
if resource.ResourceARN != nil {
ARNs = append(ARNs, *resource.ResourceARN)
}
}
if out.PaginationToken == nil || *out.PaginationToken == "" {
return ARNs, nil
}
resourcesReq.PaginationToken = out.PaginationToken
}
}
func (m *Metadata) getAllInstancesInGroup(ctx context.Context, uid string) ([]types.Instance, error) {
var instances []types.Instance
func (m *Metadata) getAllInstancesInGroup(ctx context.Context, uid string) ([]ec2Types.Instance, error) {
var instances []ec2Types.Instance
instanceReq := &ec2.DescribeInstancesInput{
Filters: []types.Filter{
Filters: []ec2Types.Filter{
{
Name: aws.String("tag:" + cloud.TagUID),
Values: []string{uid},
@ -172,11 +246,11 @@ func (m *Metadata) getAllInstancesInGroup(ctx context.Context, uid string) ([]ty
}
}
func (m *Metadata) convertToMetadataInstance(ec2Instances []types.Instance) ([]metadata.InstanceMetadata, error) {
func (m *Metadata) convertToMetadataInstance(ec2Instances []ec2Types.Instance) ([]metadata.InstanceMetadata, error) {
var instances []metadata.InstanceMetadata
for _, ec2Instance := range ec2Instances {
// ignore not running instances
if ec2Instance.State == nil || ec2Instance.State.Name != types.InstanceStateNameRunning {
if ec2Instance.State == nil || ec2Instance.State.Name != ec2Types.InstanceStateNameRunning {
continue
}
@ -231,7 +305,7 @@ func readInstanceTag(ctx context.Context, api imdsAPI, tag string) (string, erro
return string(instanceTag), err
}
func findTag(tags []types.Tag, wantKey string) (string, error) {
func findTag(tags []ec2Types.Tag, wantKey string) (string, error) {
for _, tag := range tags {
if tag.Key == nil || tag.Value == nil {
continue

View file

@ -16,7 +16,12 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
elbTypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
tagTypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
"github.com/edgelesssys/constellation/v2/internal/cloud"
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
"github.com/edgelesssys/constellation/v2/internal/role"
@ -150,17 +155,17 @@ func TestList(t *testing.T) {
someErr := errors.New("failed")
successfulResp := &ec2.DescribeInstancesOutput{
Reservations: []types.Reservation{
Reservations: []ec2Types.Reservation{
{
Instances: []types.Instance{
Instances: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-1"),
PrivateIpAddress: aws.String("192.0.2.1"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-1"),
@ -176,13 +181,13 @@ func TestList(t *testing.T) {
},
},
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-2"),
PrivateIpAddress: aws.String("192.0.2.2"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-2"),
@ -240,17 +245,17 @@ func TestList(t *testing.T) {
},
ec2: &stubEC2{
describeInstancesResp1: &ec2.DescribeInstancesOutput{
Reservations: []types.Reservation{
Reservations: []ec2Types.Reservation{
{
Instances: []types.Instance{
Instances: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-3"),
PrivateIpAddress: aws.String("192.0.2.3"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone-2"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-3"),
@ -330,22 +335,223 @@ func TestList(t *testing.T) {
}
}
func TestGetLoadBalancerEndpoint(t *testing.T) {
lbAddr := "192.0.2.1"
someErr := errors.New("some error")
testCases := map[string]struct {
imds *stubIMDS
loadbalancer *stubLoadbalancer
resourceapi *stubResourceGroupTagging
wantAddr string
wantErr bool
}{
"success retrieving loadbalancer endpoint": {
imds: &stubIMDS{
tags: map[string]string{
cloud.TagUID: "uid",
},
},
loadbalancer: &stubLoadbalancer{
describeLoadBalancersOut: &elasticloadbalancingv2.DescribeLoadBalancersOutput{
LoadBalancers: []elbTypes.LoadBalancer{
{
AvailabilityZones: []elbTypes.AvailabilityZone{
{
LoadBalancerAddresses: []elbTypes.LoadBalancerAddress{
{
IpAddress: aws.String(lbAddr),
},
},
},
},
},
},
},
},
resourceapi: &stubResourceGroupTagging{
getResourcesOut1: &resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: []tagTypes.ResourceTagMapping{
{
ResourceARN: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-loadbalancer/50dc6c495c0c9188"),
},
},
},
},
wantAddr: lbAddr,
},
"too many ARNs": {
imds: &stubIMDS{
tags: map[string]string{
cloud.TagUID: "uid",
},
},
loadbalancer: &stubLoadbalancer{
describeLoadBalancersOut: &elasticloadbalancingv2.DescribeLoadBalancersOutput{
LoadBalancers: []elbTypes.LoadBalancer{
{
AvailabilityZones: []elbTypes.AvailabilityZone{
{
LoadBalancerAddresses: []elbTypes.LoadBalancerAddress{
{
IpAddress: aws.String(lbAddr),
},
},
},
},
},
},
},
},
resourceapi: &stubResourceGroupTagging{
getResourcesOut1: &resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: []tagTypes.ResourceTagMapping{
{
ResourceARN: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-loadbalancer/50dc6c495c0c9188"),
},
{
ResourceARN: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-loadbalancer/50dc6c495c0c9188"),
},
},
},
},
wantErr: true,
},
"too many ARNs (paged)": {
imds: &stubIMDS{
tags: map[string]string{
cloud.TagUID: "uid",
},
},
loadbalancer: &stubLoadbalancer{
describeLoadBalancersOut: &elasticloadbalancingv2.DescribeLoadBalancersOutput{
LoadBalancers: []elbTypes.LoadBalancer{
{
AvailabilityZones: []elbTypes.AvailabilityZone{
{
LoadBalancerAddresses: []elbTypes.LoadBalancerAddress{
{
IpAddress: aws.String(lbAddr),
},
},
},
},
},
},
},
},
resourceapi: &stubResourceGroupTagging{
getResourcesOut1: &resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: []tagTypes.ResourceTagMapping{
{
ResourceARN: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-loadbalancer/50dc6c495c0c9188"),
},
},
PaginationToken: aws.String("token"),
},
getResourcesOut2: &resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: []tagTypes.ResourceTagMapping{
{
ResourceARN: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-loadbalancer/50dc6c495c0c9188"),
},
},
},
},
wantErr: true,
},
"loadbalancer has no availability zones": {
imds: &stubIMDS{
tags: map[string]string{
cloud.TagUID: "uid",
},
},
loadbalancer: &stubLoadbalancer{
describeLoadBalancersOut: &elasticloadbalancingv2.DescribeLoadBalancersOutput{
LoadBalancers: []elbTypes.LoadBalancer{
{
AvailabilityZones: []elbTypes.AvailabilityZone{},
},
},
},
},
resourceapi: &stubResourceGroupTagging{
getResourcesOut1: &resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: []tagTypes.ResourceTagMapping{
{
ResourceARN: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-loadbalancer/50dc6c495c0c9188"),
},
},
},
},
wantErr: true,
},
"failure to get resources by tag": {
imds: &stubIMDS{
tags: map[string]string{
cloud.TagUID: "uid",
},
},
loadbalancer: &stubLoadbalancer{
describeLoadBalancersOut: &elasticloadbalancingv2.DescribeLoadBalancersOutput{
LoadBalancers: []elbTypes.LoadBalancer{
{
AvailabilityZones: []elbTypes.AvailabilityZone{
{
LoadBalancerAddresses: []elbTypes.LoadBalancerAddress{
{
IpAddress: aws.String(lbAddr),
},
},
},
},
},
},
},
},
resourceapi: &stubResourceGroupTagging{
getResourcesErr: someErr,
},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
m := &Metadata{
imds: tc.imds,
loadbalancer: tc.loadbalancer,
resourceapiClient: tc.resourceapi,
}
endpoint, err := m.GetLoadBalancerEndpoint(context.Background())
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.wantAddr, endpoint)
})
}
}
func TestConvertToMetadataInstance(t *testing.T) {
testCases := map[string]struct {
in []types.Instance
in []ec2Types.Instance
wantInstances []metadata.InstanceMetadata
wantErr bool
}{
"success": {
in: []types.Instance{
in: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-1"),
PrivateIpAddress: aws.String("192.0.2.1"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-1"),
@ -367,12 +573,12 @@ func TestConvertToMetadataInstance(t *testing.T) {
},
},
"fallback to instance ID": {
in: []types.Instance{
in: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-1"),
PrivateIpAddress: aws.String("192.0.2.1"),
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-1"),
@ -395,24 +601,24 @@ func TestConvertToMetadataInstance(t *testing.T) {
},
},
"non running instances are ignored": {
in: []types.Instance{
in: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameStopped},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameStopped},
},
{
State: &types.InstanceState{Name: types.InstanceStateNameTerminated},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameTerminated},
},
},
},
"no instance ID": {
in: []types.Instance{
in: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
PrivateIpAddress: aws.String("192.0.2.1"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-1"),
@ -427,14 +633,14 @@ func TestConvertToMetadataInstance(t *testing.T) {
wantErr: true,
},
"no private IP": {
in: []types.Instance{
in: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-1"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-1"),
@ -449,15 +655,15 @@ func TestConvertToMetadataInstance(t *testing.T) {
wantErr: true,
},
"missing name tag": {
in: []types.Instance{
in: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-1"),
PrivateIpAddress: aws.String("192.0.2.1"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(cloud.TagRole),
Value: aws.String("controlplane"),
@ -468,15 +674,15 @@ func TestConvertToMetadataInstance(t *testing.T) {
wantErr: true,
},
"missing role tag": {
in: []types.Instance{
in: []ec2Types.Instance{
{
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
State: &ec2Types.InstanceState{Name: ec2Types.InstanceStateNameRunning},
InstanceId: aws.String("id-1"),
PrivateIpAddress: aws.String("192.0.2.1"),
Placement: &types.Placement{
Placement: &ec2Types.Placement{
AvailabilityZone: aws.String("test-zone"),
},
Tags: []types.Tag{
Tags: []ec2Types.Tag{
{
Key: aws.String(tagName),
Value: aws.String("name-1"),
@ -542,3 +748,33 @@ func (s *stubEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstances
}
return s.describeInstancesResp2, s.describeInstancesErr
}
type stubLoadbalancer struct {
describeLoadBalancersErr error
describeLoadBalancersOut *elasticloadbalancingv2.DescribeLoadBalancersOutput
}
func (s *stubLoadbalancer) DescribeLoadBalancers(_ context.Context,
in *elasticloadbalancingv2.DescribeLoadBalancersInput,
_ ...func(*elasticloadbalancingv2.Options)) (
*elasticloadbalancingv2.DescribeLoadBalancersOutput, error,
) {
return s.describeLoadBalancersOut, s.describeLoadBalancersErr
}
type stubResourceGroupTagging struct {
getResourcesErr error
getResourcesOut1 *resourcegroupstaggingapi.GetResourcesOutput
getResourcesOut2 *resourcegroupstaggingapi.GetResourcesOutput
}
func (s *stubResourceGroupTagging) GetResources(_ context.Context,
in *resourcegroupstaggingapi.GetResourcesInput,
_ ...func(*resourcegroupstaggingapi.Options)) (
*resourcegroupstaggingapi.GetResourcesOutput, error,
) {
if in.PaginationToken == nil {
return s.getResourcesOut1, s.getResourcesErr
}
return s.getResourcesOut2, s.getResourcesErr
}