mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -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
@ -20,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
@ -37,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Internal
|
||||
|
||||
- Support for AWS metadata operations
|
||||
|
||||
## [2.1.0] - 2022-10-07
|
||||
|
||||
### Added
|
||||
|
2
go.mod
2
go.mod
@ -55,6 +55,7 @@ require (
|
||||
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/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/service/ec2 v1.32.0
|
||||
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/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/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/endpoints/v2 v2.4.17 // 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
|
||||
}
|
||||
|
||||
// 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…
Reference in New Issue
Block a user