Skip to content

Commit

Permalink
feat: adds cloudmeta package
Browse files Browse the repository at this point in the history
This adds cloudmeta package. For now it's only a basic interface for
working with different cloud metadata providers, like aws, gcp, azure.
  • Loading branch information
VAveryanov8 committed Dec 11, 2024
1 parent c12903d commit c2dcb17
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 0 deletions.
72 changes: 72 additions & 0 deletions pkg/cloudmeta/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (C) 2024 ScyllaDB

package cloudmeta

import (
"context"
"errors"
"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) {
// providers will initialized here and added to CloudMeta.providers.

const defaultTimeout = 5 * time.Second

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

// GetInstanceMetadata tries to fetch instance metadata from AWS, GCP, Azure providers in order.
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
}

func (cloud *CloudMeta) runWithTimeout(ctx context.Context, provider CloudMetadataProvider) (InstanceMetadata, error) {
ctx, cancel := context.WithTimeout(ctx, cloud.providerTimeout)
defer cancel()

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

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
}

0 comments on commit c2dcb17

Please sign in to comment.