Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds cloudmeta package with AWS provider #4154

Merged
merged 9 commits into from
Dec 13, 2024
50 changes: 50 additions & 0 deletions pkg/cloudmeta/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (C) 2024 ScyllaDB

package cloudmeta

import (
"context"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/pkg/errors"
)

// AWSMetadata is a wrapper around ec2 metadata client.
type AWSMetadata struct {
ec2meta *ec2metadata.EC2Metadata
}

// NewAWSMetadata is a constructor for AWSMetadata service.
// testEndpoint can be provided if you want to overwrite the default metadata endpoint, otherwise leave it empty.
func NewAWSMetadata(testEndpoint string) (*AWSMetadata, error) {
Michal-Leszczynski marked this conversation as resolved.
Show resolved Hide resolved
session, err := session.NewSession()
if err != nil {
return nil, errors.Wrap(err, "session.NewSession")
}
cfg := aws.NewConfig()
if testEndpoint != "" {
cfg = cfg.WithEndpoint(testEndpoint)
}
return &AWSMetadata{
ec2meta: ec2metadata.New(session, cfg),
}, nil
}

// Metadata return InstanceMetadata from aws if available.
func (aws *AWSMetadata) Metadata(ctx context.Context) (InstanceMetadata, error) {
if !aws.ec2meta.AvailableWithContext(ctx) {
return InstanceMetadata{}, errors.New("metadata is not available")
karol-kokoszka marked this conversation as resolved.
Show resolved Hide resolved
}

instanceData, err := aws.ec2meta.GetInstanceIdentityDocumentWithContext(ctx)
if err != nil {
return InstanceMetadata{}, errors.Wrap(err, "aws.metadataClient.GetInstanceIdentityDocument")
}

return InstanceMetadata{
CloudProvider: CloudProviderAWS,
InstanceType: instanceData.InstanceType,
}, nil
}
77 changes: 77 additions & 0 deletions pkg/cloudmeta/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (C) 2024 ScyllaDB

package cloudmeta

import (
"context"
"errors"
karol-kokoszka marked this conversation as resolved.
Show resolved Hide resolved
"time"
)

// InstanceMetadata represents metadata returned by cloud provider.
type InstanceMetadata struct {
InstanceType string
CloudProvider CloudProvider
}

// CloudProvider is enum of supported cloud providers.
type CloudProvider string

// CloudProviderAWS represents aws provider.
var CloudProviderAWS CloudProvider = "aws"

// CloudMetadataProvider interface that each metadata provider should implement.
type CloudMetadataProvider interface {
Metadata(ctx context.Context) (InstanceMetadata, error)
}

// CloudMeta is a wrapper around various cloud metadata providers.
type CloudMeta struct {
providers []CloudMetadataProvider

providerTimeout time.Duration
}

// NewCloudMeta creates new CloudMeta provider.
func NewCloudMeta() (*CloudMeta, error) {
const defaultTimeout = 5 * time.Second

awsMeta, err := NewAWSMetadata("")
if err != nil {
return nil, err
}

return &CloudMeta{
providers: []CloudMetadataProvider{
awsMeta,
},
providerTimeout: defaultTimeout,
}, nil
}

// GetInstanceMetadata tries to fetch instance metadata from AWS, GCP, Azure providers in order.
Michal-Leszczynski marked this conversation as resolved.
Show resolved Hide resolved
func (cloud *CloudMeta) GetInstanceMetadata(ctx context.Context) (InstanceMetadata, error) {
var mErr error
for _, provider := range cloud.providers {
meta, err := cloud.runWithTimeout(ctx, provider)
if err != nil {
mErr = errors.Join(mErr, err)
continue
}
return meta, nil
}

return InstanceMetadata{}, mErr
karol-kokoszka marked this conversation as resolved.
Show resolved Hide resolved
}

func (cloud *CloudMeta) runWithTimeout(ctx context.Context, provider CloudMetadataProvider) (InstanceMetadata, error) {
ctx, cancel := context.WithTimeout(ctx, cloud.providerTimeout)
defer cancel()
karol-kokoszka marked this conversation as resolved.
Show resolved Hide resolved

return provider.Metadata(ctx)
}

// WithProviderTimeout sets per provider timeout.
func (cloud *CloudMeta) WithProviderTimeout(providerTimeout time.Duration) {
cloud.providerTimeout = providerTimeout
}
140 changes: 140 additions & 0 deletions pkg/cloudmeta/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (C) 2017 ScyllaDB

package cloudmeta

import (
"context"
"fmt"
"testing"
)

func TestGetInstanceMetadata(t *testing.T) {
t.Run("when there is no active providers", func(t *testing.T) {
cloudmeta := &CloudMeta{}

meta, err := cloudmeta.GetInstanceMetadata(context.Background())
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if meta.InstanceType != "" {
t.Fatalf("meta.InstanceType should be empty, got %v", meta.InstanceType)
}

if meta.CloudProvider != "" {
t.Fatalf("meta.CloudProvider should be empty, got %v", meta.CloudProvider)
}
})
Michal-Leszczynski marked this conversation as resolved.
Show resolved Hide resolved

t.Run("when there is only one active provider", func(t *testing.T) {
cloudmeta := &CloudMeta{
providers: []CloudMetadataProvider{newTestProvider(t, "test_provider_1", "x-test-1", nil)},
}

meta, err := cloudmeta.GetInstanceMetadata(context.Background())
if err != nil {
t.Fatalf("unexpected err: %v", err)
}

if meta.InstanceType != "x-test-1" {
t.Fatalf("meta.InstanceType should be 'x-test-1', got %v", meta.InstanceType)
}

if meta.CloudProvider != "test_provider_1" {
t.Fatalf("meta.CloudProvider should be 'test_provider_1', got %v", meta.CloudProvider)
}
})

t.Run("when there is more than one active provider", func(t *testing.T) {
cloudmeta := &CloudMeta{
providers: []CloudMetadataProvider{
newTestProvider(t, "test_provider_1", "x-test-1", nil),
newTestProvider(t, "test_provider_2", "x-test-2", nil),
},
}

// Only first one should be returned.
meta, err := cloudmeta.GetInstanceMetadata(context.Background())
if err != nil {
t.Fatalf("unexpected err: %v", err)
}

if meta.InstanceType != "x-test-1" {
t.Fatalf("meta.InstanceType should be 'x-test-1', got %v", meta.InstanceType)
}

if meta.CloudProvider != "test_provider_1" {
t.Fatalf("meta.CloudProvider should be 'test_provider_1', got %v", meta.CloudProvider)
}
})
t.Run("when there is more than one active provider, but first returns err", func(t *testing.T) {
cloudmeta := &CloudMeta{
providers: []CloudMetadataProvider{
newTestProvider(t, "test_provider_1", "x-test-1", fmt.Errorf("'test_provider_1' err")),
newTestProvider(t, "test_provider_2", "x-test-2", nil),
},
}

// Only first succesfull one should be returned.
meta, err := cloudmeta.GetInstanceMetadata(context.Background())
if err != nil {
t.Fatalf("unexpected err: %v", err)
}

if meta.InstanceType != "x-test-2" {
t.Fatalf("meta.InstanceType should be 'x-test-2', got %v", meta.InstanceType)
}

if meta.CloudProvider != "test_provider_2" {
t.Fatalf("meta.CloudProvider should be 'test_provider_2', got %v", meta.CloudProvider)
}
})

t.Run("when there is more than one active provider, but all returns err", func(t *testing.T) {
cloudmeta := &CloudMeta{
providers: []CloudMetadataProvider{
newTestProvider(t, "test_provider_1", "x-test-1", fmt.Errorf("'test_provider_1' err")),
newTestProvider(t, "test_provider_2", "x-test-2", fmt.Errorf("'test_provider_2' err")),
},
}

// Only first succesfull one should be returned.
meta, err := cloudmeta.GetInstanceMetadata(context.Background())
if err == nil {
t.Fatalf("expected err, but got: %v", err)
}

if meta.InstanceType != "" {
t.Fatalf("meta.InstanceType should be empty, got %v", meta.InstanceType)
}

if meta.CloudProvider != "" {
t.Fatalf("meta.CloudProvider should be empty, got %v", meta.CloudProvider)
}
})
}

func newTestProvider(t *testing.T, providerName, instanceType string, err error) *testProvider {
t.Helper()

return &testProvider{
name: CloudProvider(providerName),
instanceType: instanceType,
err: err,
}
}

type testProvider struct {
name CloudProvider
instanceType string
err error
}

func (tp testProvider) Metadata(ctx context.Context) (InstanceMetadata, error) {
if tp.err != nil {
return InstanceMetadata{}, tp.err
}
return InstanceMetadata{
CloudProvider: tp.name,
InstanceType: tp.instanceType,
}, nil
}
Loading