Skip to content

Commit

Permalink
feat: adds azure metadata provider to cloudmeta pkg. (#4159)
Browse files Browse the repository at this point in the history
feat: adds azure metadata provider to cloudmeta pkg.

This adds azure metadata provider to cloudmeta pkg.
Plus adds retries to azure metadata service using the github.com/hashicorp/go-retryablehttp package.
Also adds some unit tests.

Fixes: #4129
  • Loading branch information
VAveryanov8 authored Dec 17, 2024
1 parent 0372277 commit 2fde888
Show file tree
Hide file tree
Showing 31 changed files with 2,313 additions and 42 deletions.
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/hashicorp/go-version v1.7.0
github.com/json-iterator/go v1.1.12
github.com/mitchellh/mapstructure v1.5.0
Expand Down Expand Up @@ -77,6 +78,7 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hbollon/go-edlib v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
Expand All @@ -85,9 +87,9 @@ require (
github.com/klauspost/compress v1.17.9 // indirect
github.com/lnquy/cron v1.1.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand Down
15 changes: 12 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
Expand Down Expand Up @@ -882,6 +884,12 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
Expand Down Expand Up @@ -958,13 +966,14 @@ github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuz
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
Expand Down
137 changes: 137 additions & 0 deletions pkg/cloudmeta/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (C) 2024 ScyllaDB

package cloudmeta

import (
"context"
"encoding/json"
"net/http"
"time"

"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"github.com/scylladb/go-log"
)

// azureBaseURL is a base url of azure metadata service.
const azureBaseURL = "http://169.254.169.254/metadata"

// azureMetadata is a wrapper around azure metadata service.
type azureMetadata struct {
client *http.Client

baseURL string
}

// newAzureMetadata returns AzureMetadata service.
func newAzureMetadata(logger log.Logger) *azureMetadata {
return &azureMetadata{
client: defaultClient(logger),
baseURL: azureBaseURL,
}
}

func defaultClient(logger log.Logger) *http.Client {
client := retryablehttp.NewClient()

client.RetryMax = 3
client.RetryWaitMin = 500 * time.Millisecond
client.RetryWaitMax = 5 * time.Second
client.Logger = &logWrapper{
logger: logger,
}

transport := http.DefaultTransport.(*http.Transport).Clone()
// we must not use proxy for the metadata requests - see https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux#proxies.
transport.Proxy = nil

client.HTTPClient = &http.Client{
// Quite small timeout per request, because we have retries and also it's a local network call.
Timeout: 1 * time.Second,
Transport: transport,
}
return client.StandardClient()
}

// Metadata return InstanceMetadata from azure if available.
func (azure *azureMetadata) Metadata(ctx context.Context) (InstanceMetadata, error) {
vmSize, err := azure.getVMSize(ctx)
if err != nil {
return InstanceMetadata{}, errors.Wrap(err, "azure.getVMSize")
}
if vmSize == "" {
return InstanceMetadata{}, errors.New("azure vmSize is empty")
}
return InstanceMetadata{
CloudProvider: CloudProviderAzure,
InstanceType: vmSize,
}, nil
}

// azureAPIVersion should be present in every request to metadata service in query parameter.
const azureAPIVersion = "2023-07-01"

func (azure *azureMetadata) getVMSize(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, azure.baseURL+"/instance", http.NoBody)
if err != nil {
return "", errors.Wrap(err, "http new request")
}

// Setting required headers and query parameters - https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux#security-and-authentication.
req.Header.Add("Metadata", "true")
query := req.URL.Query()
query.Add("api-version", azureAPIVersion)
req.URL.RawQuery = query.Encode()

resp, err := azure.client.Do(req)
if err != nil {
return "", errors.Wrap(err, "azure.client.Do")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", errors.Errorf("status code (%d) != 200", resp.StatusCode)
}

var data azureMetadataResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", errors.Wrap(err, "decode json")
}

return data.Compute.VMSize, nil
}

// azureMetadataResponse represents azure metadata service response.
// full response specification can be found here - https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux#response-1.
type azureMetadataResponse struct {
Compute azureCompute `json:"compute"`
}

type azureCompute struct {
VMSize string `json:"vmSize"`
}

// logWrapper implements go-retryablehttp.LeveledLogger interface.
type logWrapper struct {
logger log.Logger
}

// Info wraps logger.Info method.
func (log *logWrapper) Info(msg string, keyVals ...interface{}) {
log.logger.Info(context.Background(), msg, keyVals...)
}

// Error wraps logger.Error method.
func (log *logWrapper) Error(msg string, keyVals ...interface{}) {
log.logger.Error(context.Background(), msg, keyVals...)
}

// Warn wraps logger.Error method.
func (log *logWrapper) Warn(msg string, keyVals ...interface{}) {
log.logger.Error(context.Background(), msg, keyVals...)
}

// Debug wraps logger.Debug method.
func (log *logWrapper) Debug(msg string, keyVals ...interface{}) {
log.logger.Debug(context.Background(), msg, keyVals...)
}
117 changes: 117 additions & 0 deletions pkg/cloudmeta/azure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (C) 2024 ScyllaDB

package cloudmeta

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/scylladb/go-log"
)

func TestAzureMetadata(t *testing.T) {
testCases := []struct {
name string
handler http.Handler

expectedCalls int
expectedErr bool
expectedMeta InstanceMetadata
}{
{
name: "when response is 200",
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testCheckRequireParams(t, r)

w.Write([]byte(`{"compute":{"vmSize":"Standard-A3"}}`))
}),
expectedCalls: 1,
expectedErr: false,
expectedMeta: InstanceMetadata{
CloudProvider: CloudProviderAzure,
InstanceType: "Standard-A3",
},
},
{
name: "when response is 404: not retryable",
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testCheckRequireParams(t, r)

w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`internal server error`))
}),
expectedCalls: 1,
expectedErr: true,
expectedMeta: InstanceMetadata{},
},
{
name: "when response is 500: retryable",
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testCheckRequireParams(t, r)

w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`internal server error`))
}),
expectedCalls: 4,
expectedErr: true,
expectedMeta: InstanceMetadata{},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handler := &testHandler{Handler: tc.handler}
testSrv := httptest.NewServer(handler)
defer testSrv.Close()

azureMeta := newAzureMetadata(log.NewDevelopment())
azureMeta.baseURL = testSrv.URL

meta, err := azureMeta.Metadata(context.Background())
if tc.expectedErr && err == nil {
t.Fatalf("expected err: %v\n", err)
}
if !tc.expectedErr && err != nil {
t.Fatalf("unexpected err: %v\n", err)
}

if tc.expectedCalls != handler.calls {
t.Fatalf("unexected number of calls: %d != %d", handler.calls, tc.expectedCalls)
}

if meta.CloudProvider != tc.expectedMeta.CloudProvider {
t.Fatalf("unexpected cloud provider: %s", meta.CloudProvider)
}

if meta.InstanceType != tc.expectedMeta.InstanceType {
t.Fatalf("unexpected instance type: %s", meta.InstanceType)
}
})
}
}

type testHandler struct {
http.Handler
// Keep track of how many times handler func has been called
// so we can test retries policy.
calls int
}

func (th *testHandler) ServeHTTP(w http.ResponseWriter, t *http.Request) {
th.calls++
th.Handler.ServeHTTP(w, t)
}

func testCheckRequireParams(t *testing.T, r *http.Request) {
t.Helper()
metadataHeader := r.Header.Get("Metadata")
if metadataHeader != "true" {
t.Fatalf("Metadata: true header is required")
}
apiVersion := r.URL.Query().Get("api-version")
if apiVersion != azureAPIVersion {
t.Fatalf("unexpected ?api-version: %s", apiVersion)
}
}
9 changes: 8 additions & 1 deletion pkg/cloudmeta/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"context"
"time"

"github.com/scylladb/go-log"

"github.com/pkg/errors"
"go.uber.org/multierr"
)
Expand All @@ -24,6 +26,8 @@ const (
CloudProviderAWS CloudProvider = "aws"
// CloudProviderGCP represents gcp provider.
CloudProviderGCP CloudProvider = "gcp"
// CloudProviderAzure represents azure provider.
CloudProviderAzure CloudProvider = "azure"
)

// CloudMetadataProvider interface that each metadata provider should implement.
Expand All @@ -39,7 +43,7 @@ type CloudMeta struct {
}

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

awsMeta, err := newAWSMetadata()
Expand All @@ -49,10 +53,13 @@ func NewCloudMeta() (*CloudMeta, error) {

gcpMeta := newGCPMetadata()

azureMeta := newAzureMetadata(logger)

return &CloudMeta{
providers: []CloudMetadataProvider{
awsMeta,
gcpMeta,
azureMeta,
},
providerTimeout: defaultTimeout,
}, nil
Expand Down
Loading

0 comments on commit 2fde888

Please sign in to comment.