constellation/internal/cloud/aws/aws.go

364 lines
12 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
Implements interaction with the AWS API.
Instance metadata is retrieved from the [AWS IMDS API].
Retrieving metadata of other instances is done by using the AWS compute API, and requires AWS credentials.
[AWS IMDS API]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
*/
package aws
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/aws/aws-sdk-go-v2/aws"
"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"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
elasticloadbalancingv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
"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/constants"
"github.com/edgelesssys/constellation/v2/internal/role"
)
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)
DescribeAddresses(context.Context, *ec2.DescribeAddressesInput, ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error)
}
type imdsAPI interface {
GetInstanceIdentityDocument(context.Context, *imds.GetInstanceIdentityDocumentInput, ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error)
}
// Cloud provides AWS metadata and API access.
type Cloud struct {
ec2 ec2API
imds imdsAPI
loadbalancer loadbalancerAPI
resourceapiClient resourceAPI
}
// New initializes a new AWS Metadata client using instance default credentials.
// Default region is set up using the AWS imds api.
func New(ctx context.Context) (*Cloud, error) {
cfg, err := config.LoadDefaultConfig(ctx, config.WithEC2IMDSRegion())
if err != nil {
return nil, err
}
return &Cloud{
ec2: ec2.NewFromConfig(cfg),
imds: imds.New(imds.Options{}),
loadbalancer: elasticloadbalancingv2.NewFromConfig(cfg),
resourceapiClient: resourcegroupstaggingapi.NewFromConfig(cfg),
}, nil
}
// List retrieves all instances belonging to the current Constellation.
func (c *Cloud) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
uid, err := c.readInstanceTag(ctx, cloud.TagUID)
if err != nil {
return nil, fmt.Errorf("retrieving uid tag: %w", err)
}
ec2Instances, err := c.getAllInstancesInGroup(ctx, uid)
if err != nil {
return nil, fmt.Errorf("retrieving instances: %w", err)
}
return c.convertToMetadataInstance(ec2Instances)
}
// Self retrieves the current instance.
func (c *Cloud) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
identity, err := c.imds.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
if err != nil {
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving instance identity: %w", err)
}
instanceRole, err := c.readInstanceTag(ctx, cloud.TagRole)
if err != nil {
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving role tag: %w", err)
}
return metadata.InstanceMetadata{
Name: identity.InstanceID,
ProviderID: fmt.Sprintf("aws:///%s/%s", identity.AvailabilityZone, identity.InstanceID),
Role: role.FromString(instanceRole),
VPCIP: identity.PrivateIP,
}, nil
}
// UID returns the UID of the Constellation.
func (c *Cloud) UID(ctx context.Context) (string, error) {
return c.readInstanceTag(ctx, cloud.TagUID)
}
// InitSecretHash returns the InitSecretHash of the current instance.
func (c *Cloud) InitSecretHash(ctx context.Context) ([]byte, error) {
initSecretHash, err := c.readInstanceTag(ctx, cloud.TagInitSecretHash)
if err != nil {
return nil, fmt.Errorf("retrieving init secret hash tag: %w", err)
}
return []byte(initSecretHash), nil
}
// GetLoadBalancerEndpoint returns the endpoint of the load balancer.
// TODO(malt3): remove old infrastructure code once we have migrated to using DNS as the load balancer endpoint.
func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (host, port string, err error) {
// try new architecture first
uid, err := c.readInstanceTag(ctx, cloud.TagUID)
if err != nil {
return "", "", fmt.Errorf("retrieving uid tag: %w", err)
}
describeIPsOutput, err := c.ec2.DescribeAddresses(ctx, &ec2.DescribeAddressesInput{
Filters: []ec2Types.Filter{
{
Name: aws.String(cloud.TagUID),
Values: []string{uid},
},
{
Name: aws.String("constellation-ip-endpoint"),
Values: []string{"legacy-primary-zone"},
},
},
})
if err == nil && len(describeIPsOutput.Addresses) == 1 && describeIPsOutput.Addresses[0].PublicIp != nil {
return *describeIPsOutput.Addresses[0].PublicIp, strconv.FormatInt(constants.KubernetesPort, 10), nil
}
// fallback to old architecture
// this will be removed in the future
hostname, err := c.getLoadBalancerIPOldInfrastructure(ctx)
if err != nil {
return "", "", fmt.Errorf("retrieving load balancer ip: %w", err)
}
return hostname, strconv.FormatInt(constants.KubernetesPort, 10), nil
}
// getLoadBalancerIPOldInfrastructure returns the IP of the load balancer.
// This is only used for the old infrastructure.
// This will be removed in the future.
func (c *Cloud) getLoadBalancerIPOldInfrastructure(ctx context.Context) (string, error) {
loadbalancer, err := c.getLoadBalancer(ctx)
if err != nil {
return "", fmt.Errorf("finding Constellation load balancer: %w", err)
}
// TODO(malt3): Add support for multiple availability zones in the lb frontend.
// This can only be done after we have migrated to using DNS as the load balancer endpoint.
// At that point, we don't need to care about the number of availability zones anymore.
if len(loadbalancer.AvailabilityZones) != 1 {
return "", fmt.Errorf("%d availability zones found; expected 1", len(loadbalancer.AvailabilityZones))
}
if len(loadbalancer.AvailabilityZones[0].LoadBalancerAddresses) != 1 {
return "", fmt.Errorf("%d load balancer addresses found; expected 1", len(loadbalancer.AvailabilityZones[0].LoadBalancerAddresses))
}
if loadbalancer.AvailabilityZones[0].LoadBalancerAddresses[0].IpAddress == nil {
return "", errors.New("load balancer address is nil")
}
return *loadbalancer.AvailabilityZones[0].LoadBalancerAddresses[0].IpAddress, nil
}
/*
// TODO(malt3): uncomment and use as soon as we switch the primary endpoint to DNS.
func (c *Cloud) getLoadBalancerDNSName(ctx context.Context) (string, error) {
loadbalancer, err := c.getLoadBalancer(ctx)
if err != nil {
return "", fmt.Errorf("finding Constellation load balancer: %w", err)
}
if loadbalancer.DNSName == nil {
return "", errors.New("load balancer dns name missing")
}
return *loadbalancer.DNSName, nil
}
*/
func (c *Cloud) getLoadBalancer(ctx context.Context) (*elasticloadbalancingv2types.LoadBalancer, error) {
uid, err := c.readInstanceTag(ctx, cloud.TagUID)
if err != nil {
return nil, fmt.Errorf("retrieving uid tag: %w", err)
}
arns, err := c.getARNsByTag(ctx, uid, "elasticloadbalancing:loadbalancer")
if err != nil {
return nil, fmt.Errorf("retrieving load balancer ARNs: %w", err)
}
if len(arns) != 1 {
return nil, fmt.Errorf("%d load balancers found", len(arns))
}
output, err := c.loadbalancer.DescribeLoadBalancers(ctx, &elasticloadbalancingv2.DescribeLoadBalancersInput{
LoadBalancerArns: arns,
})
if err != nil {
return nil, fmt.Errorf("retrieving load balancer: %w", err)
}
if len(output.LoadBalancers) != 1 {
return nil, fmt.Errorf("%d load balancers found; expected 1", len(output.LoadBalancers))
}
return &output.LoadBalancers[0], nil
}
// getARNsByTag returns a list of ARNs that have the given tag.
func (c *Cloud) 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 := c.resourceapiClient.GetResources(ctx, resourcesReq); ; out, err = c.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 (c *Cloud) getAllInstancesInGroup(ctx context.Context, uid string) ([]ec2Types.Instance, error) {
var instances []ec2Types.Instance
instanceReq := &ec2.DescribeInstancesInput{
Filters: []ec2Types.Filter{
{
Name: aws.String("tag:" + cloud.TagUID),
Values: []string{uid},
},
},
}
for out, err := c.ec2.DescribeInstances(ctx, instanceReq); ; out, err = c.ec2.DescribeInstances(ctx, instanceReq) {
if err != nil {
return nil, fmt.Errorf("retrieving instances: %w", err)
}
for _, reservation := range out.Reservations {
instances = append(instances, reservation.Instances...)
}
if out.NextToken == nil {
return instances, nil
}
instanceReq.NextToken = out.NextToken
}
}
func (c *Cloud) 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 != ec2Types.InstanceStateNameRunning {
continue
}
// sanity checks to avoid panics
if ec2Instance.InstanceId == nil {
return nil, errors.New("instance id is nil")
}
if ec2Instance.PrivateIpAddress == nil {
return nil, fmt.Errorf("instance %s has no private IP address", *ec2Instance.InstanceId)
}
newInstance := metadata.InstanceMetadata{
VPCIP: *ec2Instance.PrivateIpAddress,
Name: *ec2Instance.InstanceId,
}
instanceRole, err := findTag(ec2Instance.Tags, cloud.TagRole)
if err != nil {
return nil, fmt.Errorf("retrieving tag for instance %s: %w", *ec2Instance.InstanceId, err)
}
newInstance.Role = role.FromString(instanceRole)
// Set ProviderID
if ec2Instance.Placement != nil {
// set to aws:///<region>/<instance-id>
newInstance.ProviderID = fmt.Sprintf("aws:///%s/%s", *ec2Instance.Placement.AvailabilityZone, *ec2Instance.InstanceId)
} else {
// fallback to aws:///<instance-id>
newInstance.ProviderID = fmt.Sprintf("aws:///%s", *ec2Instance.InstanceId)
}
instances = append(instances, newInstance)
}
return instances, nil
}
func (c *Cloud) readInstanceTag(ctx context.Context, tag string) (string, error) {
identity, err := c.imds.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
if err != nil {
return "", fmt.Errorf("retrieving instance identity: %w", err)
}
if identity == nil {
return "", errors.New("instance identity is nil")
}
out, err := c.ec2.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
InstanceIds: []string{identity.InstanceID},
})
if err != nil {
return "", fmt.Errorf("descibing instances: %w", err)
}
if len(out.Reservations) != 1 || len(out.Reservations[0].Instances) != 1 {
return "", fmt.Errorf("expected 1 instance, got %d", len(out.Reservations[0].Instances))
}
return findTag(out.Reservations[0].Instances[0].Tags, tag)
}
func findTag(tags []ec2Types.Tag, wantKey string) (string, error) {
for _, tag := range tags {
if tag.Key == nil || tag.Value == nil {
continue
}
if *tag.Key == wantKey {
return *tag.Value, nil
}
}
return "", fmt.Errorf("tag %q not found", wantKey)
}