-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
c12903d
commit c2dcb17
Showing
2 changed files
with
212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |