AB#2474 Implement List and Self method for AWS (#229)

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2022-10-12 13:40:38 +02:00 committed by GitHub
parent dbd71eebd9
commit 23afccb975
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 944 additions and 2 deletions

View File

@ -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
View File

@ -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

View 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
View 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
}

View 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
}

View 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
}

View 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)
}

View 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
}

View File

@ -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
}
}