Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions providers/azure/services/managedredis/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ import (
"github.com/LeanerCloud/CUDly/providers/azure/services/internal/reservations"
)

// maxRecsPages caps Consumption API recommendation pagination (matches the
// sibling Azure service clients, e.g. cache/client.go).
const maxRecsPages = 10

// recommendationsListArgs builds the (scope, options) for the Consumption
// ReservationRecommendations pager. NewListPager's first argument is the
// billing scope (the subscription), NOT the ODATA filter -- passing the filter
// as the scope produces a malformed URL where every request errors (the exact
// failure mode documented in compute/client.go). The filter goes in
// options.Filter. Extracted so a unit test can assert the scope shape without a
// real Azure client (the injected mock pager bypasses NewListPager entirely).
func (c *ManagedRedisClient) recommendationsListArgs() (string, *armconsumption.ReservationRecommendationsClientListOptions) {
scope := fmt.Sprintf("/subscriptions/%s", c.subscriptionID)
filter := "properties/scope eq 'Shared' and properties/resourceType eq 'RedisCache'"
return scope, &armconsumption.ReservationRecommendationsClientListOptions{Filter: &filter}
}

// HTTPClient interface for HTTP operations (enables mocking)
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
Expand Down Expand Up @@ -126,11 +143,17 @@ func (c *ManagedRedisClient) GetRecommendations(ctx context.Context, params comm
if err != nil {
return nil, fmt.Errorf("failed to create consumption client: %w", err)
}
filter := "properties/scope eq 'Shared' and properties/resourceType eq 'RedisCache'"
pager = client.NewListPager(filter, &armconsumption.ReservationRecommendationsClientListOptions{})
scope, opts := c.recommendationsListArgs()
pager = client.NewListPager(scope, opts)
}

for pager.More() {
for pageIdx := 0; pager.More(); pageIdx++ {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("context cancelled during pagination: %w", err)
}
if pageIdx >= maxRecsPages {
return nil, fmt.Errorf("managedredis: GetRecommendations pagination cap (%d pages) reached", maxRecsPages)
}
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get Redis Cache recommendations: %w", err)
Expand Down
15 changes: 15 additions & 0 deletions providers/azure/services/managedredis/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,21 @@ func TestValidateOffering_InvalidSKU(t *testing.T) {

// -- GetRecommendations --

// TestRecommendationsListArgs_UsesSubscriptionScope is the regression guard for
// the pager-construction bug: NewListPager's first argument must be the
// subscription billing scope, not the ODATA filter (the wrong shape produced a
// malformed URL that errored on every request, breaking Managed Redis recs).
// The injected mock pager bypasses NewListPager, so this asserts the args helper.
func TestRecommendationsListArgs_UsesSubscriptionScope(t *testing.T) {
c := NewClient(nil, "sub-123", "eastus")
scope, opts := c.recommendationsListArgs()
assert.Equal(t, "/subscriptions/sub-123", scope,
"first NewListPager arg must be the subscription scope, not the filter")
require.NotNil(t, opts)
require.NotNil(t, opts.Filter, "the ODATA filter must be passed via options.Filter")
assert.Contains(t, *opts.Filter, "resourceType eq 'RedisCache'")
}

func TestGetRecommendations_EmptyPager(t *testing.T) {
c := NewClient(nil, "sub", "eastus")
c.SetRecommendationsPager(&mockRecommendationsPager{
Expand Down
Loading