diff --git a/pkg/cloudmeta/gcp.go b/pkg/cloudmeta/gcp.go new file mode 100644 index 000000000..5317a4fe5 --- /dev/null +++ b/pkg/cloudmeta/gcp.go @@ -0,0 +1,66 @@ +// Copyright (C) 2024 ScyllaDB + +package cloudmeta + +import ( + "context" + "strings" + + "cloud.google.com/go/compute/metadata" + "github.com/pkg/errors" +) + +// gcpMetadata is a wrapper around gcp metadata client. +type gcpMetadata struct { + meta *metadata.Client +} + +// newGCPMetadata returns gcp metadata provider. +func newGCPMetadata() *gcpMetadata { + return &gcpMetadata{ + meta: metadata.NewClient(nil), + } +} + +// Metadata returns InstanceMetadata from gcp if available. +func (gcp *gcpMetadata) Metadata(ctx context.Context) (InstanceMetadata, error) { + machineType, err := gcp.getMachineType(ctx) + if err != nil { + return InstanceMetadata{}, errors.Wrap(err, "gcp.meta.GetWithContext") + } + return InstanceMetadata{ + CloudProvider: CloudProviderGCP, + InstanceType: machineType, + }, nil +} + +func (gcp *gcpMetadata) getMachineType(ctx context.Context) (string, error) { + machineTypeResp, err := gcp.meta.GetWithContext(ctx, "instance/machine-type") + if err != nil { + return "", errors.Wrap(err, "gcp.meta.GetWithContext") + } + + machineType, err := parseMachineTypeResponse(machineTypeResp) + if err != nil { + return "", err + } + + return machineType, nil +} + +// The machine type for this VM. This value has the following format: projects/PROJECT_NUM/machineTypes/MACHINE_TYPE. +// See https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys#instance-metadata. +func parseMachineTypeResponse(resp string) (string, error) { + errUnexpectedFormat := errors.Errorf("unexpected machineType response format: %s", resp) + + parts := strings.Split(resp, "/") + if len(parts) != 4 { + return "", errUnexpectedFormat + } + + if parts[2] != "machineTypes" { + return "", errUnexpectedFormat + } + + return parts[3], nil +} diff --git a/pkg/cloudmeta/gcp_test.go b/pkg/cloudmeta/gcp_test.go new file mode 100644 index 000000000..69e4d0c9f --- /dev/null +++ b/pkg/cloudmeta/gcp_test.go @@ -0,0 +1,67 @@ +// Copyright (C) 2024 ScyllaDB + +package cloudmeta + +import "testing" + +func TestParseMachineTypeResponse(t *testing.T) { + testCases := []struct { + name string + machineTypeResponse string + + expectedErr bool + expected string + }{ + { + name: "everything is fine", + machineTypeResponse: "projects/project1/machineTypes/machineType1", + + expectedErr: false, + expected: "machineType1", + }, + { + name: "new response part is added", + machineTypeResponse: "projects/project1/zone/zone1/machineTypes/machineType1", + + expectedErr: true, + expected: "", + }, + { + name: "new response part is added after machineTypes part", + machineTypeResponse: "projects/project1/machineTypes/machineType1/zones/zone1", + + expectedErr: true, + expected: "", + }, + { + name: "parts are mixed up", + machineTypeResponse: "machineTypes/machineType1/projects/project1", + + expectedErr: true, + expected: "", + }, + { + name: "empty response", + machineTypeResponse: "", + + expectedErr: true, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + machineType, err := parseMachineTypeResponse(tc.machineTypeResponse) + if tc.expectedErr && err == nil { + t.Fatalf("expected err, but got %v", err) + } + if !tc.expectedErr && err != nil { + t.Fatalf("unexpected err: %v", err) + } + + if tc.expected != machineType { + t.Fatalf("machineType(%s) != expected(%s)", machineType, tc.expected) + } + }) + } +} diff --git a/pkg/cloudmeta/metadata.go b/pkg/cloudmeta/metadata.go index a0507248c..707485af9 100644 --- a/pkg/cloudmeta/metadata.go +++ b/pkg/cloudmeta/metadata.go @@ -19,8 +19,12 @@ type InstanceMetadata struct { // CloudProvider is enum of supported cloud providers. type CloudProvider string -// CloudProviderAWS represents aws provider. -var CloudProviderAWS CloudProvider = "aws" +const ( + // CloudProviderAWS represents aws provider. + CloudProviderAWS CloudProvider = "aws" + // CloudProviderGCP represents gcp provider. + CloudProviderGCP CloudProvider = "gcp" +) // CloudMetadataProvider interface that each metadata provider should implement. type CloudMetadataProvider interface { @@ -43,9 +47,12 @@ func NewCloudMeta() (*CloudMeta, error) { return nil, err } + gcpMeta := newGCPMetadata() + return &CloudMeta{ providers: []CloudMetadataProvider{ awsMeta, + gcpMeta, }, providerTimeout: defaultTimeout, }, nil