Skip to content

Commit a834313

Browse files
authored
feat: add headers for tenantID and clientRequestID (#1228)
2 parents 37f1bf0 + e9d7c99 commit a834313

File tree

6 files changed

+87
-20
lines changed

6 files changed

+87
-20
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
@@ -157,6 +157,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
157157
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
158158
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
159159
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
160+
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
161+
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
160162
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
161163
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
162164
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=

pkg/clients/azure/compute/vmsizerecommenderclient.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,31 @@ import (
1313
"fmt"
1414
"io"
1515
"net/http"
16+
"os"
17+
"time"
1618

1719
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
20+
"github.com/gofrs/uuid"
1821
"google.golang.org/protobuf/encoding/protojson"
22+
"k8s.io/klog/v2"
1923

2024
computev1 "go.goms.io/fleet/apis/protos/azure/compute/v1"
2125
"go.goms.io/fleet/pkg/clients/httputil"
2226
"go.goms.io/fleet/pkg/utils/controller"
2327
)
2428

2529
const (
30+
tenantIDEnvVarName = "AZURE_TENANT_ID"
2631
// recommendationsPathTemplate is the URL path template for VM size recommendations API.
2732
recommendationsPathTemplate = "/subscriptions/%s/providers/Microsoft.Compute/locations/%s/vmSizeRecommendations/vmAttributeBased/generate"
2833
)
2934

3035
// AttributeBasedVMSizeRecommenderClient accesses Azure Attribute-Based VM Size Recommender API
3136
// to provide VM size recommendations based on specified attributes.
3237
type AttributeBasedVMSizeRecommenderClient struct {
38+
// tenantID is the ID of the Azure fleet's tenant.
39+
// At the moment, Azure fleet is single-tenant, the fleet and all its members must be in the same tenant.
40+
tenantID string
3341
// baseURL is the base URL of the http(s) requests to the attribute-based VM size recommender service endpoint.
3442
baseURL string
3543
// httpClient is the HTTP client used for making requests.
@@ -43,13 +51,18 @@ func NewAttributeBasedVMSizeRecommenderClient(
4351
serverAddress string,
4452
httpClient *http.Client,
4553
) (*AttributeBasedVMSizeRecommenderClient, error) {
54+
tenantID := os.Getenv(tenantIDEnvVarName)
55+
if tenantID == "" {
56+
return nil, fmt.Errorf("failed to get tenantID: environment variable %s is not set", tenantIDEnvVarName)
57+
}
4658
if len(serverAddress) == 0 {
4759
return nil, fmt.Errorf("serverAddress cannot be empty")
4860
}
4961
if httpClient == nil {
5062
return nil, fmt.Errorf("httpClient cannot be nil")
5163
}
5264
return &AttributeBasedVMSizeRecommenderClient{
65+
tenantID: tenantID,
5366
baseURL: serverAddress,
5467
httpClient: httpClient,
5568
}, nil
@@ -59,7 +72,7 @@ func NewAttributeBasedVMSizeRecommenderClient(
5972
func (c *AttributeBasedVMSizeRecommenderClient) GenerateAttributeBasedRecommendations(
6073
ctx context.Context,
6174
req *computev1.GenerateAttributeBasedRecommendationsRequest,
62-
) (*computev1.GenerateAttributeBasedRecommendationsResponse, error) {
75+
) (response *computev1.GenerateAttributeBasedRecommendationsResponse, err error) {
6376
if req == nil {
6477
return nil, controller.NewUnexpectedBehaviorError(errors.New("request cannot be nil"))
6578
}
@@ -94,10 +107,23 @@ func (c *AttributeBasedVMSizeRecommenderClient) GenerateAttributeBasedRecommenda
94107
}
95108

96109
// Set headers
110+
clientRequestID := uuid.Must(uuid.NewV4()).String()
97111
httpReq.Header.Set(httputil.HeaderContentTypeKey, httputil.HeaderContentTypeJSON)
98112
httpReq.Header.Set(httputil.HeaderAcceptKey, httputil.HeaderContentTypeJSON)
113+
httpReq.Header.Set(httputil.HeaderAzureSubscriptionTenantIDKey, c.tenantID)
114+
httpReq.Header.Set(httputil.HeaderAzureClientRequestIDKey, clientRequestID)
99115

100116
// Execute the request
117+
startTime := time.Now()
118+
klog.V(2).InfoS("Generating VM size recommendations", "subscriptionID", req.SubscriptionId, "location", req.Location, "clientRequestID", clientRequestID)
119+
defer func() {
120+
latency := time.Since(startTime).Milliseconds()
121+
if err != nil {
122+
klog.ErrorS(err, "Failed to generate VM size recommendations", "subscriptionID", req.SubscriptionId, "location", req.Location, "clientRequestID", clientRequestID, "latency", latency)
123+
}
124+
klog.V(2).InfoS("Generated VM size recommendations", "subscriptionID", req.SubscriptionId, "location", req.Location, "clientRequestID", clientRequestID, "latency", latency)
125+
}()
126+
101127
resp, err := c.httpClient.Do(httpReq)
102128
if err != nil {
103129
return nil, fmt.Errorf("failed to execute request: %w", err)
@@ -116,13 +142,13 @@ func (c *AttributeBasedVMSizeRecommenderClient) GenerateAttributeBasedRecommenda
116142
}
117143

118144
// Unmarshal response using protojson for proper proto3 support
119-
var response computev1.GenerateAttributeBasedRecommendationsResponse
145+
response = &computev1.GenerateAttributeBasedRecommendationsResponse{}
120146
unmarshaler := protojson.UnmarshalOptions{
121147
DiscardUnknown: true,
122148
}
123-
if err := unmarshaler.Unmarshal(respBody, &response); err != nil {
149+
if err := unmarshaler.Unmarshal(respBody, response); err != nil {
124150
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
125151
}
126152

127-
return &response, nil
153+
return response, nil
128154
}

pkg/clients/azure/compute/vmsizerecommenderclient_test.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,49 @@ import (
2121
computev1 "go.goms.io/fleet/apis/protos/azure/compute/v1"
2222
)
2323

24+
const (
25+
testTenantID = "test-tenant-id"
26+
)
27+
2428
func TestNewAttributeBasedVMSizeRecommenderClient(t *testing.T) {
2529
tests := []struct {
2630
name string
31+
tenantID string
2732
serverAddress string
2833
httpClient *http.Client
2934
wantClient *AttributeBasedVMSizeRecommenderClient
3035
wantErr bool
3136
}{
37+
{
38+
name: "with missing tenant ID environment variable",
39+
serverAddress: "https://example.com",
40+
httpClient: http.DefaultClient,
41+
wantClient: nil,
42+
wantErr: true,
43+
},
3244
{
3345
name: "with empty server address",
46+
tenantID: testTenantID,
3447
serverAddress: "",
3548
httpClient: http.DefaultClient,
3649
wantClient: nil,
3750
wantErr: true,
3851
},
3952
{
4053
name: "with nil HTTP client",
54+
tenantID: testTenantID,
4155
serverAddress: "http://localhost:8080",
4256
httpClient: nil,
4357
wantClient: nil,
4458
wantErr: true,
4559
},
4660
{
47-
name: "with both server address and HTTP client",
61+
name: "with all fields properly set",
62+
tenantID: testTenantID,
4863
serverAddress: "https://example.com",
4964
httpClient: http.DefaultClient,
5065
wantClient: &AttributeBasedVMSizeRecommenderClient{
66+
tenantID: testTenantID,
5167
baseURL: "https://example.com",
5268
httpClient: http.DefaultClient,
5369
},
@@ -57,6 +73,7 @@ func TestNewAttributeBasedVMSizeRecommenderClient(t *testing.T) {
5773

5874
for _, tt := range tests {
5975
t.Run(tt.name, func(t *testing.T) {
76+
t.Setenv(tenantIDEnvVarName, tt.tenantID)
6077
got, gotErr := NewAttributeBasedVMSizeRecommenderClient(tt.serverAddress, tt.httpClient)
6178
if (gotErr != nil) != tt.wantErr {
6279
t.Errorf("NewAttributeBasedVMSizeRecommenderClient() error = %v, wantErr %v", gotErr, tt.wantErr)
@@ -204,29 +221,38 @@ func TestClient_GenerateAttributeBasedRecommendations(t *testing.T) {
204221

205222
for _, tt := range tests {
206223
t.Run(tt.name, func(t *testing.T) {
207-
// Create mock server
224+
// Set tenant ID environment variable to create client.
225+
t.Setenv(tenantIDEnvVarName, testTenantID)
226+
// Create mock server.
208227
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
209-
// Verify request method
228+
// Verify request method.
210229
if r.Method != http.MethodPost {
211230
t.Errorf("got %s, want POST request", r.Method)
212231
}
213232

214-
// Verify headers
233+
// Verify headers.
215234
if r.Header.Get("Content-Type") != "application/json" {
216235
t.Errorf("got %s, want Content-Type: application/json", r.Header.Get("Content-Type"))
217236
}
218237
if r.Header.Get("Accept") != "application/json" {
219238
t.Errorf("got %s, want Accept: application/json", r.Header.Get("Accept"))
220239
}
240+
if r.Header.Get("Grpc-Metadata-subscriptionTenantID") != testTenantID {
241+
t.Errorf("got %s, want Grpc-Metadata-subscriptionTenantID: %s",
242+
r.Header.Get("Grpc-Metadata-subscriptionTenantID"), testTenantID)
243+
}
244+
if r.Header.Get("Grpc-Metadata-clientRequestID") == "" {
245+
t.Error("Grpc-Metadata-clientRequestID header is missing")
246+
}
221247

222-
// Verify URL path if request is not nil
248+
// Verify URL path if request is not nil.
223249
if tt.request != nil && tt.request.SubscriptionId != "" && tt.request.Location != "" {
224250
wantPath := fmt.Sprintf(recommendationsPathTemplate, tt.request.SubscriptionId, tt.request.Location)
225251
if r.URL.Path != wantPath {
226252
t.Errorf("got %s, want path %s", r.URL.Path, wantPath)
227253
}
228254

229-
// Verify request body using protojson for proper proto3 oneof support
255+
// Verify request body using protojson for proper proto3 oneof support.
230256
body, err := io.ReadAll(r.Body)
231257
if err != nil {
232258
t.Fatalf("failed to read request body: %v", err)
@@ -243,24 +269,24 @@ func TestClient_GenerateAttributeBasedRecommendations(t *testing.T) {
243269
}
244270
}
245271

246-
// Write mock response
272+
// Write mock response.
247273
w.WriteHeader(tt.mockStatusCode)
248274
if _, err := w.Write([]byte(tt.mockResponse)); err != nil {
249275
t.Fatalf("failed to write response: %v", err)
250276
}
251277
}))
252278
defer server.Close()
253279

254-
// Create client
280+
// Create client.
255281
client, err := NewAttributeBasedVMSizeRecommenderClient(server.URL, http.DefaultClient)
256282
if err != nil {
257283
t.Errorf("failed to create client: %v", err)
258284
}
259285

260-
// Execute request
286+
// Execute request.
261287
got, err := client.GenerateAttributeBasedRecommendations(context.Background(), tt.request)
262288

263-
// Check error
289+
// Check error.
264290
if (err != nil) != tt.wantErr {
265291
t.Errorf("GenerateAttributeBasedRecommendations() error = %v, wantErr %v", err, tt.wantErr)
266292
return
@@ -271,7 +297,7 @@ func TestClient_GenerateAttributeBasedRecommendations(t *testing.T) {
271297
return
272298
}
273299

274-
// Compare response
300+
// Compare response.
275301
if !proto.Equal(tt.wantResponse, got) {
276302
t.Errorf("GenerateAttributeBasedRecommendations() = %+v, want %+v", got, tt.wantResponse)
277303
}

pkg/clients/httputil/httputil.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@ package httputil
99
import (
1010
"net/http"
1111
"time"
12+
13+
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
1214
)
1315

1416
// Common HTTP constants.
1517
const (
16-
// HTTPTimeoutAzure is the timeout for HTTP requests to Azure services.
17-
// Setting to 60 seconds, following ARM client request timeout conventions:
18-
// https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-details.md#client-request-timeout.
19-
HTTPTimeoutAzure = 60 * time.Second
20-
2118
// HeaderContentTypeKey is the HTTP header key for Content-Type.
2219
HeaderContentTypeKey = "Content-Type"
2320
// HeaderAcceptKey is the HTTP header key for Accept.
@@ -26,6 +23,20 @@ const (
2623
HeaderContentTypeJSON = "application/json"
2724
)
2825

26+
const (
27+
// HTTPTimeoutAzure is the timeout for HTTP requests to Azure services.
28+
// Setting to 60 seconds, following ARM client request timeout conventions:
29+
// https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-details.md#client-request-timeout.
30+
HTTPTimeoutAzure = 60 * time.Second
31+
32+
// HeaderAzureSubscriptionTenantIDKey is the HTTP header key for the tenantID of the requested Azure Subscription.
33+
// grpc-gateway maps headers with Grpc-Metadata- prefix to grpc metadata after removing it.
34+
// See: https://github.com/grpc-ecosystem/grpc-gateway.
35+
HeaderAzureSubscriptionTenantIDKey = runtime.MetadataHeaderPrefix + "subscriptionTenantID"
36+
// HeaderAzureClientRequestIDKey is the HTTP header key for Azure Client Request ID.
37+
HeaderAzureClientRequestIDKey = runtime.MetadataHeaderPrefix + "clientRequestID"
38+
)
39+
2940
var (
3041
// DefaultClientForAzure is the default HTTP client to access Azure services.
3142
DefaultClientForAzure = &http.Client{Timeout: HTTPTimeoutAzure}

pkg/propertychecker/azure/checker_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ func TestCheckIfMeetSKUCapacityRequirement(t *testing.T) {
430430
server := createMockAttributeBasedVMSizeRecommenderServer(t, tt.mockStatusCode)
431431
defer server.Close()
432432

433+
t.Setenv("AZURE_TENANT_ID", "test-tenant-id")
433434
client, err := compute.NewAttributeBasedVMSizeRecommenderClient(server.URL, http.DefaultClient)
434435
if err != nil {
435436
t.Fatalf("failed to create VM size recommender client: %v", err)

0 commit comments

Comments
 (0)