mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-08 23:12:18 -04:00
AB#2474 Implement List and Self method for AWS (#229)
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
dbd71eebd9
commit
23afccb975
9 changed files with 944 additions and 2 deletions
|
@ -20,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -37,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Internal
|
### Internal
|
||||||
|
|
||||||
|
- Support for AWS metadata operations
|
||||||
|
|
||||||
## [2.1.0] - 2022-10-07
|
## [2.1.0] - 2022-10-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -55,6 +55,7 @@ require (
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.0
|
github.com/Azure/go-autorest/autorest/to v0.4.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.16.16
|
github.com/aws/aws-sdk-go-v2 v1.16.16
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.17.7
|
github.com/aws/aws-sdk-go-v2/config v1.17.7
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.2
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.2
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.32.0
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.32.0
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.18.10
|
github.com/aws/aws-sdk-go-v2/service/kms v1.18.10
|
||||||
|
@ -142,7 +143,6 @@ require (
|
||||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.20 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.12.20 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect
|
||||||
|
|
47
internal/cloud/aws/autoscaler.go
Normal file
47
internal/cloud/aws/autoscaler.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/kubernetes"
|
||||||
|
k8s "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Implement for AWS.
|
||||||
|
|
||||||
|
// Autoscaler holds the AWS cluster-autoscaler configuration.
|
||||||
|
type Autoscaler struct{}
|
||||||
|
|
||||||
|
// Name returns the cloud-provider name as used by k8s cluster-autoscaler.
|
||||||
|
func (a Autoscaler) Name() string {
|
||||||
|
return "aws"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secrets returns a list of secrets to deploy together with the k8s cluster-autoscaler.
|
||||||
|
func (a Autoscaler) Secrets(providerID, cloudServiceAccountURI string) (kubernetes.Secrets, error) {
|
||||||
|
return kubernetes.Secrets{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volumes returns a list of volumes to deploy together with the k8s cluster-autoscaler.
|
||||||
|
func (a Autoscaler) Volumes() []k8s.Volume {
|
||||||
|
return []k8s.Volume{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolumeMounts returns a list of volume mounts to deploy together with the k8s cluster-autoscaler.
|
||||||
|
func (a Autoscaler) VolumeMounts() []k8s.VolumeMount {
|
||||||
|
return []k8s.VolumeMount{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cluster-autoscaler.
|
||||||
|
func (a Autoscaler) Env() []k8s.EnvVar {
|
||||||
|
return []k8s.EnvVar{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported is used to determine if we support autoscaling for the cloud provider.
|
||||||
|
func (a Autoscaler) Supported() bool {
|
||||||
|
return false
|
||||||
|
}
|
81
internal/cloud/aws/ccm.go
Normal file
81
internal/cloud/aws/ccm.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/kubernetes"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/versions"
|
||||||
|
k8s "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Implement for AWS.
|
||||||
|
|
||||||
|
// CloudControllerManager holds the AWS cloud-controller-manager configuration.
|
||||||
|
type CloudControllerManager struct{}
|
||||||
|
|
||||||
|
// Image returns the container image used to provide cloud-controller-manager for the cloud-provider.
|
||||||
|
func (c CloudControllerManager) Image(k8sVersion versions.ValidK8sVersion) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the path used by cloud-controller-manager executable within the container image.
|
||||||
|
func (c CloudControllerManager) Path() string {
|
||||||
|
return "/aws-cloud-controller-manager"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the cloud-provider name as used by k8s cloud-controller-manager (k8s.gcr.io/cloud-controller-manager).
|
||||||
|
func (c CloudControllerManager) Name() string {
|
||||||
|
return "aws"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtraArgs returns a list of arguments to append to the cloud-controller-manager command.
|
||||||
|
func (c CloudControllerManager) ExtraArgs() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigMaps returns a list of ConfigMaps to deploy together with the k8s cloud-controller-manager
|
||||||
|
// Reference: https://kubernetes.io/docs/concepts/configuration/configmap/ .
|
||||||
|
func (c CloudControllerManager) ConfigMaps() (kubernetes.ConfigMaps, error) {
|
||||||
|
return kubernetes.ConfigMaps{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secrets returns a list of secrets to deploy together with the k8s cloud-controller-manager.
|
||||||
|
// Reference: https://kubernetes.io/docs/concepts/configuration/secret/ .
|
||||||
|
func (c CloudControllerManager) Secrets(ctx context.Context, providerID, cloudServiceAccountURI string) (kubernetes.Secrets, error) {
|
||||||
|
return kubernetes.Secrets{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volumes returns a list of volumes to deploy together with the k8s cloud-controller-manager.
|
||||||
|
// Reference: https://kubernetes.io/docs/concepts/storage/volumes/ .
|
||||||
|
func (c CloudControllerManager) Volumes() []k8s.Volume {
|
||||||
|
return []k8s.Volume{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolumeMounts a list of of volume mounts to deploy together with the k8s cloud-controller-manager.
|
||||||
|
func (c CloudControllerManager) VolumeMounts() []k8s.VolumeMount {
|
||||||
|
return []k8s.VolumeMount{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cloud-controller-manager.
|
||||||
|
func (c CloudControllerManager) Env() []k8s.EnvVar {
|
||||||
|
return []k8s.EnvVar{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareInstance is called on every instance before deploying the cloud-controller-manager.
|
||||||
|
// Allows for cloud-provider specific hooks.
|
||||||
|
func (c CloudControllerManager) PrepareInstance(instance metadata.InstanceMetadata, vpnIP string) error {
|
||||||
|
// no specific hook required.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported is used to determine if cloud controller manager is implemented for this cloud provider.
|
||||||
|
func (c CloudControllerManager) Supported() bool {
|
||||||
|
return false
|
||||||
|
}
|
34
internal/cloud/aws/cloudnodemanager.go
Normal file
34
internal/cloud/aws/cloudnodemanager.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import "github.com/edgelesssys/constellation/v2/internal/versions"
|
||||||
|
|
||||||
|
// TODO: Implement for AWS.
|
||||||
|
|
||||||
|
// CloudNodeManager holds the AWS cloud-node-manager configuration.
|
||||||
|
type CloudNodeManager struct{}
|
||||||
|
|
||||||
|
// Image returns the container image used to provide cloud-node-manager for the cloud-provider.
|
||||||
|
func (c *CloudNodeManager) Image(k8sVersion versions.ValidK8sVersion) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the path used by cloud-node-manager executable within the container image.
|
||||||
|
func (c *CloudNodeManager) Path() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtraArgs returns a list of arguments to append to the cloud-node-manager command.
|
||||||
|
func (c *CloudNodeManager) ExtraArgs() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported is used to determine if cloud node manager is implemented for this cloud provider.
|
||||||
|
func (c *CloudNodeManager) Supported() bool {
|
||||||
|
return false
|
||||||
|
}
|
25
internal/cloud/aws/logger.go
Normal file
25
internal/cloud/aws/logger.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
// TODO: Implement for AWS.
|
||||||
|
|
||||||
|
// Logger is a Cloud Logger for AWS.
|
||||||
|
type Logger struct{}
|
||||||
|
|
||||||
|
// NewLogger creates a new Cloud Logger for AWS.
|
||||||
|
func NewLogger() *Logger {
|
||||||
|
return &Logger{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disclose is not implemented for AWS.
|
||||||
|
func (l *Logger) Disclose(msg string) {}
|
||||||
|
|
||||||
|
// Close is a no-op.
|
||||||
|
func (l *Logger) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
197
internal/cloud/aws/metadata.go
Normal file
197
internal/cloud/aws/metadata.go
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tagName = "Name"
|
||||||
|
tagRole = "constellation-role"
|
||||||
|
tagUID = "constellation-uid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ec2API interface {
|
||||||
|
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type imdsAPI interface {
|
||||||
|
GetInstanceIdentityDocument(context.Context, *imds.GetInstanceIdentityDocumentInput, ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error)
|
||||||
|
GetMetadata(context.Context, *imds.GetMetadataInput, ...func(*imds.Options)) (*imds.GetMetadataOutput, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata implements core.ProviderMetadata interface for AWS.
|
||||||
|
type Metadata struct {
|
||||||
|
ec2 ec2API
|
||||||
|
imds imdsAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (*Metadata, error) {
|
||||||
|
cfg, err := config.LoadDefaultConfig(ctx, config.WithEC2IMDSRegion())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Metadata{
|
||||||
|
ec2: ec2.NewFromConfig(cfg),
|
||||||
|
imds: imds.New(imds.Options{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported is used to determine if metadata API is implemented for this cloud provider.
|
||||||
|
func (m *Metadata) Supported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// List retrieves all instances belonging to the current Constellation.
|
||||||
|
func (m *Metadata) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
||||||
|
uid, err := m.readInstanceTag(ctx, tagUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("retrieving uid tag: %w", err)
|
||||||
|
}
|
||||||
|
ec2Instances, err := m.getAllInstancesInGroup(ctx, uid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("retrieving instances: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.convertToMetadataInstance(ec2Instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self retrieves the current instance.
|
||||||
|
func (m *Metadata) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||||
|
identity, err := m.imds.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
|
||||||
|
if err != nil {
|
||||||
|
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving instance identity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := m.readInstanceTag(ctx, tagName)
|
||||||
|
if err != nil {
|
||||||
|
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving name tag: %w", err)
|
||||||
|
}
|
||||||
|
instanceRole, err := m.readInstanceTag(ctx, tagRole)
|
||||||
|
if err != nil {
|
||||||
|
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving role tag: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.InstanceMetadata{
|
||||||
|
Name: name,
|
||||||
|
ProviderID: fmt.Sprintf("aws:///%s/%s", identity.AvailabilityZone, identity.InstanceID),
|
||||||
|
Role: role.FromString(instanceRole),
|
||||||
|
VPCIP: identity.PrivateIP,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Metadata) getAllInstancesInGroup(ctx context.Context, uid string) ([]types.Instance, error) {
|
||||||
|
var instances []types.Instance
|
||||||
|
instanceReq := &ec2.DescribeInstancesInput{
|
||||||
|
Filters: []types.Filter{
|
||||||
|
{
|
||||||
|
Name: aws.String("tag:" + tagUID),
|
||||||
|
Values: []string{uid},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for out, err := m.ec2.DescribeInstances(ctx, instanceReq); ; out, err = m.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 (m *Metadata) convertToMetadataInstance(ec2Instances []types.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 {
|
||||||
|
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, err := findTag(ec2Instance.Tags, tagName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("retrieving tag for instance %s: %w", *ec2Instance.InstanceId, err)
|
||||||
|
}
|
||||||
|
newInstance.Name = name
|
||||||
|
|
||||||
|
instanceRole, err := findTag(ec2Instance.Tags, 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 (m *Metadata) readInstanceTag(ctx context.Context, tag string) (string, error) {
|
||||||
|
reader, err := m.imds.GetMetadata(ctx, &imds.GetMetadataInput{
|
||||||
|
Path: "/tags/instance/" + tag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer reader.Content.Close()
|
||||||
|
instanceTag, err := io.ReadAll(reader.Content)
|
||||||
|
return string(instanceTag), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func findTag(tags []types.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)
|
||||||
|
}
|
543
internal/cloud/aws/metadata_test.go
Normal file
543
internal/cloud/aws/metadata_test.go
Normal file
|
@ -0,0 +1,543 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelf(t *testing.T) {
|
||||||
|
someErr := errors.New("failed")
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
imds *stubIMDS
|
||||||
|
ec2 *stubEC2
|
||||||
|
wantSelf metadata.InstanceMetadata
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"success control-plane": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
instanceDocumentResp: &imds.GetInstanceIdentityDocumentOutput{
|
||||||
|
InstanceIdentityDocument: imds.InstanceIdentityDocument{
|
||||||
|
InstanceID: "test-instance-id",
|
||||||
|
AvailabilityZone: "test-zone",
|
||||||
|
PrivateIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: map[string]string{
|
||||||
|
tagName: "test-instance",
|
||||||
|
tagRole: "controlplane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSelf: metadata.InstanceMetadata{
|
||||||
|
Name: "test-instance",
|
||||||
|
ProviderID: "aws:///test-zone/test-instance-id",
|
||||||
|
Role: role.ControlPlane,
|
||||||
|
VPCIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"success worker": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
instanceDocumentResp: &imds.GetInstanceIdentityDocumentOutput{
|
||||||
|
InstanceIdentityDocument: imds.InstanceIdentityDocument{
|
||||||
|
InstanceID: "test-instance-id",
|
||||||
|
AvailabilityZone: "test-zone",
|
||||||
|
PrivateIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: map[string]string{
|
||||||
|
tagName: "test-instance",
|
||||||
|
tagRole: "worker",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSelf: metadata.InstanceMetadata{
|
||||||
|
Name: "test-instance",
|
||||||
|
ProviderID: "aws:///test-zone/test-instance-id",
|
||||||
|
Role: role.Worker,
|
||||||
|
VPCIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"get instance document error": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
getInstanceIdentityDocumentErr: someErr,
|
||||||
|
tags: map[string]string{
|
||||||
|
tagName: "test-instance",
|
||||||
|
tagRole: "controlplane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"get metadata error": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
instanceDocumentResp: &imds.GetInstanceIdentityDocumentOutput{
|
||||||
|
InstanceIdentityDocument: imds.InstanceIdentityDocument{
|
||||||
|
InstanceID: "test-instance-id",
|
||||||
|
AvailabilityZone: "test-zone",
|
||||||
|
PrivateIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getMetadataErr: someErr,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"name not set": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
instanceDocumentResp: &imds.GetInstanceIdentityDocumentOutput{
|
||||||
|
InstanceIdentityDocument: imds.InstanceIdentityDocument{
|
||||||
|
InstanceID: "test-instance-id",
|
||||||
|
AvailabilityZone: "test-zone",
|
||||||
|
PrivateIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: map[string]string{
|
||||||
|
tagRole: "controlplane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"role not set": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
instanceDocumentResp: &imds.GetInstanceIdentityDocumentOutput{
|
||||||
|
InstanceIdentityDocument: imds.InstanceIdentityDocument{
|
||||||
|
InstanceID: "test-instance-id",
|
||||||
|
AvailabilityZone: "test-zone",
|
||||||
|
PrivateIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: map[string]string{
|
||||||
|
tagName: "test-instance",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
m := &Metadata{imds: tc.imds, ec2: &stubEC2{}}
|
||||||
|
|
||||||
|
self, err := m.Self(context.Background())
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(tc.wantSelf, self)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
someErr := errors.New("failed")
|
||||||
|
|
||||||
|
successfulResp := &ec2.DescribeInstancesOutput{
|
||||||
|
Reservations: []types.Reservation{
|
||||||
|
{
|
||||||
|
Instances: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-1"),
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.1"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("controlplane"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagUID),
|
||||||
|
Value: aws.String("uid"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-2"),
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.2"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("worker"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagUID),
|
||||||
|
Value: aws.String("uid"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
imds *stubIMDS
|
||||||
|
ec2 *stubEC2
|
||||||
|
wantList []metadata.InstanceMetadata
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"success single page": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
tags: map[string]string{
|
||||||
|
tagUID: "uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ec2: &stubEC2{
|
||||||
|
describeInstancesResp1: successfulResp,
|
||||||
|
},
|
||||||
|
wantList: []metadata.InstanceMetadata{
|
||||||
|
{
|
||||||
|
Name: "name-1",
|
||||||
|
Role: role.ControlPlane,
|
||||||
|
ProviderID: "aws:///test-zone/id-1",
|
||||||
|
VPCIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "name-2",
|
||||||
|
Role: role.Worker,
|
||||||
|
ProviderID: "aws:///test-zone/id-2",
|
||||||
|
VPCIP: "192.0.2.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"success multiple pages": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
tags: map[string]string{
|
||||||
|
tagUID: "uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ec2: &stubEC2{
|
||||||
|
describeInstancesResp1: &ec2.DescribeInstancesOutput{
|
||||||
|
Reservations: []types.Reservation{
|
||||||
|
{
|
||||||
|
Instances: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-3"),
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.3"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone-2"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-3"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("worker"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagUID),
|
||||||
|
Value: aws.String("uid"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NextToken: aws.String("next-token"),
|
||||||
|
},
|
||||||
|
describeInstancesResp2: successfulResp,
|
||||||
|
},
|
||||||
|
wantList: []metadata.InstanceMetadata{
|
||||||
|
{
|
||||||
|
Name: "name-3",
|
||||||
|
Role: role.Worker,
|
||||||
|
ProviderID: "aws:///test-zone-2/id-3",
|
||||||
|
VPCIP: "192.0.2.3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "name-1",
|
||||||
|
Role: role.ControlPlane,
|
||||||
|
ProviderID: "aws:///test-zone/id-1",
|
||||||
|
VPCIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "name-2",
|
||||||
|
Role: role.Worker,
|
||||||
|
ProviderID: "aws:///test-zone/id-2",
|
||||||
|
VPCIP: "192.0.2.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"fail to get UID": {
|
||||||
|
imds: &stubIMDS{},
|
||||||
|
ec2: &stubEC2{
|
||||||
|
describeInstancesResp1: successfulResp,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"describe instances fails": {
|
||||||
|
imds: &stubIMDS{
|
||||||
|
tags: map[string]string{
|
||||||
|
tagUID: "uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ec2: &stubEC2{
|
||||||
|
describeInstancesErr: someErr,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
m := &Metadata{ec2: tc.ec2, imds: tc.imds}
|
||||||
|
|
||||||
|
list, err := m.List(context.Background())
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(tc.wantList, list)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertToMetadataInstance(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
in []types.Instance
|
||||||
|
wantInstances []metadata.InstanceMetadata
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
in: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-1"),
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.1"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("controlplane"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantInstances: []metadata.InstanceMetadata{
|
||||||
|
{
|
||||||
|
Name: "name-1",
|
||||||
|
Role: role.ControlPlane,
|
||||||
|
ProviderID: "aws:///test-zone/id-1",
|
||||||
|
VPCIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"fallback to instance ID": {
|
||||||
|
in: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-1"),
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.1"),
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("controlplane"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
wantInstances: []metadata.InstanceMetadata{
|
||||||
|
{
|
||||||
|
Name: "name-1",
|
||||||
|
Role: role.ControlPlane,
|
||||||
|
ProviderID: "aws:///id-1",
|
||||||
|
VPCIP: "192.0.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"non running instances are ignored": {
|
||||||
|
in: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameStopped},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameTerminated},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"no instance ID": {
|
||||||
|
in: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.1"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("controlplane"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"no private IP": {
|
||||||
|
in: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-1"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("controlplane"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"missing name tag": {
|
||||||
|
in: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-1"),
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.1"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagRole),
|
||||||
|
Value: aws.String("controlplane"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"missing role tag": {
|
||||||
|
in: []types.Instance{
|
||||||
|
{
|
||||||
|
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
|
||||||
|
InstanceId: aws.String("id-1"),
|
||||||
|
PrivateIpAddress: aws.String("192.0.2.1"),
|
||||||
|
Placement: &types.Placement{
|
||||||
|
AvailabilityZone: aws.String("test-zone"),
|
||||||
|
},
|
||||||
|
Tags: []types.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String(tagName),
|
||||||
|
Value: aws.String("name-1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
m := &Metadata{}
|
||||||
|
|
||||||
|
instances, err := m.convertToMetadataInstance(tc.in)
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(tc.wantInstances, instances)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubIMDS struct {
|
||||||
|
getInstanceIdentityDocumentErr error
|
||||||
|
getMetadataErr error
|
||||||
|
instanceDocumentResp *imds.GetInstanceIdentityDocumentOutput
|
||||||
|
tags map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubIMDS) GetInstanceIdentityDocument(context.Context, *imds.GetInstanceIdentityDocumentInput, ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error) {
|
||||||
|
return s.instanceDocumentResp, s.getInstanceIdentityDocumentErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubIMDS) GetMetadata(_ context.Context, in *imds.GetMetadataInput, _ ...func(*imds.Options)) (*imds.GetMetadataOutput, error) {
|
||||||
|
tag, ok := s.tags[strings.TrimPrefix(in.Path, "/tags/instance/")]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
return &imds.GetMetadataOutput{
|
||||||
|
Content: io.NopCloser(
|
||||||
|
strings.NewReader(
|
||||||
|
tag,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}, s.getMetadataErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubEC2 struct {
|
||||||
|
describeInstancesErr error
|
||||||
|
describeInstancesResp1 *ec2.DescribeInstancesOutput
|
||||||
|
describeInstancesResp2 *ec2.DescribeInstancesOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
|
||||||
|
if in.NextToken == nil {
|
||||||
|
return s.describeInstancesResp1, s.describeInstancesErr
|
||||||
|
}
|
||||||
|
return s.describeInstancesResp2, s.describeInstancesErr
|
||||||
|
}
|
|
@ -46,3 +46,17 @@ func (r *Role) UnmarshalJSON(b []byte) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromString converts a string to a Role.
|
||||||
|
func FromString(s string) Role {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "controlplane":
|
||||||
|
return ControlPlane
|
||||||
|
case "worker":
|
||||||
|
return Worker
|
||||||
|
case "admin":
|
||||||
|
return Admin
|
||||||
|
default:
|
||||||
|
return Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue