diff --git a/pkg/cloudmeta/metadata.go b/pkg/cloudmeta/metadata.go new file mode 100644 index 000000000..23822b708 --- /dev/null +++ b/pkg/cloudmeta/metadata.go @@ -0,0 +1,51 @@ +// Copyright (C) 2024 ScyllaDB + +package cloudmeta + +import ( + "context" + "errors" +) + +// 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 +} + +// NewCloudMeta creates new CloudMeta provider. +func NewCloudMeta() (*CloudMeta, error) { + // providers will initialized here and added to CloudMeta.providers. + return &CloudMeta{}, 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 := provider.Metadata(ctx) + if err != nil { + mErr = errors.Join(mErr, err) + continue + } + return meta, nil + } + + return InstanceMetadata{}, mErr +} diff --git a/pkg/cloudmeta/metadata_test.go b/pkg/cloudmeta/metadata_test.go new file mode 100644 index 000000000..ac3040ccc --- /dev/null +++ b/pkg/cloudmeta/metadata_test.go @@ -0,0 +1,141 @@ +// 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 +}