Skip to content

Commit d1cac16

Browse files
committed
add headers for tenantID and clientRequestID
1 parent 492ff3c commit d1cac16

File tree

5 files changed

+71
-11
lines changed

5 files changed

+71
-11
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/Azure/karpenter-provider-azure v1.5.1
99
github.com/crossplane/crossplane-runtime v1.17.0
1010
github.com/evanphx/json-patch/v5 v5.9.11
11+
github.com/gofrs/uuid v4.4.0+incompatible
1112
github.com/google/go-cmp v0.7.0
1213
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0
1314
github.com/onsi/ginkgo/v2 v2.23.4

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
155155
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
156156
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
157157
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
158+
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
159+
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
158160
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
159161
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
160162
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=

pkg/clients/azure/compute/vmsizerecommenderclient.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import (
1313
"fmt"
1414
"io"
1515
"net/http"
16+
"os"
1617

1718
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
19+
"github.com/gofrs/uuid"
1820
"google.golang.org/protobuf/encoding/protojson"
1921

2022
computev1 "go.goms.io/fleet/apis/protos/azure/compute/v1"
@@ -23,13 +25,17 @@ import (
2325
)
2426

2527
const (
28+
tenantIDEnvVarName = "AZURE_TENANT_ID"
2629
// recommendationsPathTemplate is the URL path template for VM size recommendations API.
2730
recommendationsPathTemplate = "/subscriptions/%s/providers/Microsoft.Compute/locations/%s/vmSizeRecommendations/vmAttributeBased/generate"
2831
)
2932

3033
// AttributeBasedVMSizeRecommenderClient accesses Azure Attribute-Based VM Size Recommender API
3134
// to provide VM size recommendations based on specified attributes.
3235
type AttributeBasedVMSizeRecommenderClient struct {
36+
// tenantID is the ID of the Azure fleet's tenant.
37+
// At the moment, Azure fleet is single-tenant, the fleet and all its members must be in the same tenant.
38+
tenantID string
3339
// baseURL is the base URL of the http(s) requests to the attribute-based VM size recommender service endpoint.
3440
baseURL string
3541
// httpClient is the HTTP client used for making requests.
@@ -43,13 +49,18 @@ func NewAttributeBasedVMSizeRecommenderClient(
4349
serverAddress string,
4450
httpClient *http.Client,
4551
) (*AttributeBasedVMSizeRecommenderClient, error) {
52+
tenantID := os.Getenv(tenantIDEnvVarName)
53+
if tenantID == "" {
54+
return nil, fmt.Errorf("failed to get tenantID: environment variable %s is not set", tenantIDEnvVarName)
55+
}
4656
if len(serverAddress) == 0 {
4757
return nil, fmt.Errorf("serverAddress cannot be empty")
4858
}
4959
if httpClient == nil {
5060
return nil, fmt.Errorf("httpClient cannot be nil")
5161
}
5262
return &AttributeBasedVMSizeRecommenderClient{
63+
tenantID: tenantID,
5364
baseURL: serverAddress,
5465
httpClient: httpClient,
5566
}, nil
@@ -96,6 +107,8 @@ func (c *AttributeBasedVMSizeRecommenderClient) GenerateAttributeBasedRecommenda
96107
// Set headers
97108
httpReq.Header.Set(httputil.HeaderContentTypeKey, httputil.HeaderContentTypeJSON)
98109
httpReq.Header.Set(httputil.HeaderAcceptKey, httputil.HeaderContentTypeJSON)
110+
httpReq.Header.Set(httputil.HeaderAzureSubscriptionTenantIDKey, c.tenantID)
111+
httpReq.Header.Set(httputil.HeaderAzureClientRequestIDKey, uuid.Must(uuid.NewV4()).String())
99112

100113
// Execute the request
101114
resp, err := c.httpClient.Do(httpReq)

pkg/clients/azure/compute/vmsizerecommenderclient_test.go

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"net/http"
1313
"net/http/httptest"
14+
"os"
1415
"strings"
1516
"testing"
1617

@@ -21,33 +22,49 @@ import (
2122
computev1 "go.goms.io/fleet/apis/protos/azure/compute/v1"
2223
)
2324

25+
const (
26+
testTenantID = "test-tenant-id"
27+
)
28+
2429
func TestNewAttributeBasedVMSizeRecommenderClient(t *testing.T) {
2530
tests := []struct {
2631
name string
32+
tenantID string
2733
serverAddress string
2834
httpClient *http.Client
2935
wantClient *AttributeBasedVMSizeRecommenderClient
3036
wantErr bool
3137
}{
38+
{
39+
name: "with missing tenant ID environment variable",
40+
serverAddress: "https://example.com",
41+
httpClient: http.DefaultClient,
42+
wantClient: nil,
43+
wantErr: true,
44+
},
3245
{
3346
name: "with empty server address",
47+
tenantID: testTenantID,
3448
serverAddress: "",
3549
httpClient: http.DefaultClient,
3650
wantClient: nil,
3751
wantErr: true,
3852
},
3953
{
4054
name: "with nil HTTP client",
55+
tenantID: testTenantID,
4156
serverAddress: "http://localhost:8080",
4257
httpClient: nil,
4358
wantClient: nil,
4459
wantErr: true,
4560
},
4661
{
47-
name: "with both server address and HTTP client",
62+
name: "with all fields properly set",
63+
tenantID: testTenantID,
4864
serverAddress: "https://example.com",
4965
httpClient: http.DefaultClient,
5066
wantClient: &AttributeBasedVMSizeRecommenderClient{
67+
tenantID: testTenantID,
5168
baseURL: "https://example.com",
5269
httpClient: http.DefaultClient,
5370
},
@@ -57,6 +74,11 @@ func TestNewAttributeBasedVMSizeRecommenderClient(t *testing.T) {
5774

5875
for _, tt := range tests {
5976
t.Run(tt.name, func(t *testing.T) {
77+
original := os.Getenv(tenantIDEnvVarName)
78+
_ = os.Setenv(tenantIDEnvVarName, tt.tenantID)
79+
defer func() {
80+
_ = os.Setenv(tenantIDEnvVarName, original)
81+
}()
6082
got, gotErr := NewAttributeBasedVMSizeRecommenderClient(tt.serverAddress, tt.httpClient)
6183
if (gotErr != nil) != tt.wantErr {
6284
t.Errorf("NewAttributeBasedVMSizeRecommenderClient() error = %v, wantErr %v", gotErr, tt.wantErr)
@@ -204,29 +226,42 @@ func TestClient_GenerateAttributeBasedRecommendations(t *testing.T) {
204226

205227
for _, tt := range tests {
206228
t.Run(tt.name, func(t *testing.T) {
207-
// Create mock server
229+
// Set tenant ID environment variable to create client.
230+
original := os.Getenv(tenantIDEnvVarName)
231+
_ = os.Setenv(tenantIDEnvVarName, testTenantID)
232+
defer func() {
233+
_ = os.Setenv(tenantIDEnvVarName, original)
234+
}()
235+
// Create mock server.
208236
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
209-
// Verify request method
237+
// Verify request method.
210238
if r.Method != http.MethodPost {
211239
t.Errorf("got %s, want POST request", r.Method)
212240
}
213241

214-
// Verify headers
242+
// Verify headers.
215243
if r.Header.Get("Content-Type") != "application/json" {
216244
t.Errorf("got %s, want Content-Type: application/json", r.Header.Get("Content-Type"))
217245
}
218246
if r.Header.Get("Accept") != "application/json" {
219247
t.Errorf("got %s, want Accept: application/json", r.Header.Get("Accept"))
220248
}
249+
if r.Header.Get("Grpc-Metadata-subscriptionTenantID") != testTenantID {
250+
t.Errorf("got %s, want Grpc-Metadata-subscriptionTenantID: %s",
251+
r.Header.Get("Grpc-Metadata-subscriptionTenantID"), testTenantID)
252+
}
253+
if r.Header.Get("Grpc-Metadata-clientRequestID") == "" {
254+
t.Error("Grpc-Metadata-clientRequestID header is missing")
255+
}
221256

222-
// Verify URL path if request is not nil
257+
// Verify URL path if request is not nil.
223258
if tt.request != nil && tt.request.SubscriptionId != "" && tt.request.Location != "" {
224259
wantPath := fmt.Sprintf(recommendationsPathTemplate, tt.request.SubscriptionId, tt.request.Location)
225260
if r.URL.Path != wantPath {
226261
t.Errorf("got %s, want path %s", r.URL.Path, wantPath)
227262
}
228263

229-
// Verify request body using protojson for proper proto3 oneof support
264+
// Verify request body using protojson for proper proto3 oneof support.
230265
body, err := io.ReadAll(r.Body)
231266
if err != nil {
232267
t.Fatalf("failed to read request body: %v", err)
@@ -243,24 +278,24 @@ func TestClient_GenerateAttributeBasedRecommendations(t *testing.T) {
243278
}
244279
}
245280

246-
// Write mock response
281+
// Write mock response.
247282
w.WriteHeader(tt.mockStatusCode)
248283
if _, err := w.Write([]byte(tt.mockResponse)); err != nil {
249284
t.Fatalf("failed to write response: %v", err)
250285
}
251286
}))
252287
defer server.Close()
253288

254-
// Create client
289+
// Create client.
255290
client, err := NewAttributeBasedVMSizeRecommenderClient(server.URL, http.DefaultClient)
256291
if err != nil {
257292
t.Errorf("failed to create client: %v", err)
258293
}
259294

260-
// Execute request
295+
// Execute request.
261296
got, err := client.GenerateAttributeBasedRecommendations(context.Background(), tt.request)
262297

263-
// Check error
298+
// Check error.
264299
if (err != nil) != tt.wantErr {
265300
t.Errorf("GenerateAttributeBasedRecommendations() error = %v, wantErr %v", err, tt.wantErr)
266301
return
@@ -271,7 +306,7 @@ func TestClient_GenerateAttributeBasedRecommendations(t *testing.T) {
271306
return
272307
}
273308

274-
// Compare response
309+
// Compare response.
275310
if !proto.Equal(tt.wantResponse, got) {
276311
t.Errorf("GenerateAttributeBasedRecommendations() = %+v, want %+v", got, tt.wantResponse)
277312
}

pkg/clients/httputil/httputil.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ const (
2424
HeaderAcceptKey = "Accept"
2525
// HeaderContentTypeJSON is the Content-Type header value for JSON payloads.
2626
HeaderContentTypeJSON = "application/json"
27+
28+
// HeaderGRPCMetadataPrefix is the prefix for gRPC metadata headers.
29+
// grpc-gateway maps headers with this prefix to grpc metadata after removing the prefix.
30+
// See: https://github.com/grpc-ecosystem/grpc-gateway.
31+
HeaderGRPCMetadataPrefix = "Grpc-Metadata-"
32+
// HeaderAzureSubscriptionTenantIDKey is the HTTP header key for the tenantID of the requested Azure Subscription.
33+
HeaderAzureSubscriptionTenantIDKey = HeaderGRPCMetadataPrefix + "subscriptionTenantID"
34+
// HeaderAzureClientRequestIDKey is the HTTP header key for Azure Client Request ID.
35+
HeaderAzureClientRequestIDKey = HeaderGRPCMetadataPrefix + "clientRequestID"
2736
)
2837

2938
var (

0 commit comments

Comments
 (0)