Skip to content

Commit 2718b35

Browse files
gagan16kaaronfern
authored andcommitted
Orphan Safety Controller missing VM's due to large resource query (gardener#207)
1 parent 676e5d0 commit 2718b35

File tree

3 files changed

+394
-21
lines changed

3 files changed

+394
-21
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package helpers
6+
7+
import (
8+
"context"
9+
10+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph"
11+
)
12+
13+
// FakeResourceGraphClient is a fake implementation of ResourceGraphClient for testing.
14+
type FakeResourceGraphClient struct {
15+
// Responses is a list of responses to return in sequence for each call to Resources
16+
Responses []armresourcegraph.ClientResourcesResponse
17+
// Errors is a list of errors to return in sequence for each call to Resources
18+
Errors []error
19+
// CallCount tracks how many times Resources has been called
20+
CallCount int
21+
// RecordedRequests stores all query requests made to the client
22+
RecordedRequests []armresourcegraph.QueryRequest
23+
}
24+
25+
// NewFakeResourceGraphClient creates a new FakeResourceGraphClient for testing.
26+
func NewFakeResourceGraphClient() *FakeResourceGraphClient {
27+
return &FakeResourceGraphClient{
28+
Responses: []armresourcegraph.ClientResourcesResponse{},
29+
Errors: []error{},
30+
RecordedRequests: []armresourcegraph.QueryRequest{},
31+
}
32+
}
33+
34+
// Resources implements the ResourceGraphClient interface.
35+
// Returns the next response/error in the sequence based on CallCount.
36+
func (f *FakeResourceGraphClient) Resources(_ context.Context, query armresourcegraph.QueryRequest, _ *armresourcegraph.ClientResourcesOptions) (armresourcegraph.ClientResourcesResponse, error) {
37+
f.RecordedRequests = append(f.RecordedRequests, query)
38+
index := f.CallCount
39+
f.CallCount++
40+
41+
if index < len(f.Errors) && f.Errors[index] != nil {
42+
return armresourcegraph.ClientResourcesResponse{}, f.Errors[index]
43+
}
44+
45+
if index < len(f.Responses) {
46+
return f.Responses[index], nil
47+
}
48+
49+
return armresourcegraph.ClientResourcesResponse{}, nil
50+
}
51+
52+
// AddResponse adds a response to be returned by the fake client.
53+
func (f *FakeResourceGraphClient) AddResponse(response armresourcegraph.ClientResourcesResponse) *FakeResourceGraphClient {
54+
f.Responses = append(f.Responses, response)
55+
return f
56+
}
57+
58+
// AddError adds an error to be returned by the fake client.
59+
func (f *FakeResourceGraphClient) AddError(err error) *FakeResourceGraphClient {
60+
f.Errors = append(f.Errors, err)
61+
return f
62+
}

pkg/azure/access/helpers/resourcegraph.go

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,49 +12,82 @@ import (
1212
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph"
1313
"github.com/gardener/machine-controller-manager-provider-azure/pkg/azure/access/errors"
1414
"github.com/gardener/machine-controller-manager-provider-azure/pkg/azure/instrument"
15-
"k8s.io/utils/pointer"
15+
"k8s.io/klog/v2"
16+
"k8s.io/utils/ptr"
1617
)
1718

1819
const (
1920
resourceGraphQueryServiceLabel = "resource_graph_query"
2021
)
2122

23+
// ResourceGraphClient is an interface for Azure Resource Graph client operations.
24+
// This allows for easier testing by enabling mock implementations.
25+
type ResourceGraphClient interface {
26+
Resources(ctx context.Context, query armresourcegraph.QueryRequest, options *armresourcegraph.ClientResourcesOptions) (armresourcegraph.ClientResourcesResponse, error)
27+
}
28+
29+
// Azure client implements interface
30+
var _ ResourceGraphClient = (*armresourcegraph.Client)(nil)
31+
2232
// MapperFn maps a row of result (represented as map[string]interface{}) to any type T.
2333
type MapperFn[T any] func(map[string]interface{}) *T
2434

2535
// QueryAndMap fires a resource graph KUSTO query constructing it from queryTemplate and templateArgs.
2636
// The result of the query are then mapped using a mapperFn and the result or an error is returned.
2737
// NOTE: All calls to this Azure API are instrumented as prometheus metric.
28-
func QueryAndMap[T any](ctx context.Context, client *armresourcegraph.Client, subscriptionID string, mapperFn MapperFn[T], queryTemplate string, templateArgs ...any) (results []T, err error) {
38+
func QueryAndMap[T any](ctx context.Context, client ResourceGraphClient, subscriptionID string, mapperFn MapperFn[T], queryTemplate string, templateArgs ...any) (results []T, err error) {
2939
defer instrument.AZAPIMetricRecorderFn(resourceGraphQueryServiceLabel, &err)()
3040

3141
query := fmt.Sprintf(queryTemplate, templateArgs...)
32-
resources, err := client.Resources(ctx,
33-
armresourcegraph.QueryRequest{
42+
var skipToken *string
43+
pageCount := 0
44+
45+
// Continue fetching results while there is a skipToken
46+
for {
47+
queryRequest := armresourcegraph.QueryRequest{
3448
Query: to.Ptr(query),
3549
Options: nil,
3650
Subscriptions: []*string{to.Ptr(subscriptionID)},
37-
}, nil)
51+
}
3852

39-
if err != nil {
40-
errors.LogAzAPIError(err, "ResourceGraphQuery failure to execute Query: %s", query)
41-
return nil, err
42-
}
53+
// Set skipToken in options if present for subsequent pages
54+
if skipToken != nil {
55+
queryRequest.Options = &armresourcegraph.QueryRequestOptions{
56+
SkipToken: skipToken,
57+
}
58+
}
4359

44-
if resources.TotalRecords == pointer.Int64(0) {
45-
return results, nil
46-
}
60+
resources, err := client.Resources(ctx, queryRequest, nil)
61+
if err != nil {
62+
errors.LogAzAPIError(err, "ResourceGraphQuery failure to execute Query: %s, with skipToken: %s", query, ptr.Deref(skipToken, "<nil>"))
63+
return nil, err
64+
}
65+
pageCount++
4766

48-
// resourceResponse.Data is a []interface{}
49-
if objSlice, ok := resources.Data.([]interface{}); ok {
50-
for _, obj := range objSlice {
51-
// Each obj in resourceResponse.Data is a map[string]Interface{}
52-
rowElements := obj.(map[string]interface{})
53-
result := mapperFn(rowElements)
54-
if result != nil {
55-
results = append(results, *result)
67+
if ptr.Deref(resources.TotalRecords, 0) == 0 {
68+
klog.Infof("Query completed: fetched %d pages, no results retrieved", pageCount)
69+
return results, nil
70+
}
71+
72+
// resourceResponse.Data is a []interface{}
73+
if objSlice, ok := resources.Data.([]interface{}); ok {
74+
for _, obj := range objSlice {
75+
// Each obj in resourceResponse.Data is a map[string]Interface{}
76+
rowElements := obj.(map[string]interface{})
77+
result := mapperFn(rowElements)
78+
if result != nil {
79+
results = append(results, *result)
80+
}
5681
}
5782
}
83+
// Check if there are more pages to fetch and set skipToken for next iteration
84+
if resources.SkipToken == nil || *resources.SkipToken == "" {
85+
break
86+
}
87+
klog.Infof("Fetching next page (page %d) with skipToken: %s", pageCount+1, *resources.SkipToken)
88+
skipToken = resources.SkipToken
5889
}
59-
return
90+
91+
klog.Infof("Query completed: fetched %d pages, total results: %d", pageCount, len(results))
92+
return results, nil
6093
}

0 commit comments

Comments
 (0)