diff --git a/providers/gcp/services/computeengine/client.go b/providers/gcp/services/computeengine/client.go index 7c3f6167..c857d51c 100644 --- a/providers/gcp/services/computeengine/client.go +++ b/providers/gcp/services/computeengine/client.go @@ -29,6 +29,7 @@ import ( "github.com/LeanerCloud/cloud-commitments-go/pkg/common" "github.com/LeanerCloud/cloud-commitments-go/pkg/retry" + "github.com/LeanerCloud/cloud-commitments-go/providers/gcp/internal/skulist" "github.com/LeanerCloud/cloud-commitments-go/providers/gcp/internal/skumatch" ) @@ -335,13 +336,23 @@ func (r *realMachineTypesService) Close() error { // realBillingService wraps the real cloudbilling.APIService. type realBillingService struct { + ctx context.Context service *cloudbilling.APIService } +// ListSKUs returns every SKU of the service: the Compute Engine catalog is +// 33,453 SKUs over 7 pages of 5000, so a single call misses most of it. func (r *realBillingService) ListSKUs(serviceID string) (*cloudbilling.ListSkusResponse, error) { - return r.service.Services.Skus.List(serviceID).Do() + skus, err := skulist.All(r.ctx, r.service.Services.Skus.List(serviceID).PageSize(skuPageSize), skulist.DefaultMaxPages) + if err != nil { + return nil, fmt.Errorf("computeengine: list SKUs for %s: %w", serviceID, err) + } + return &cloudbilling.ListSkusResponse{Skus: skus}, nil } +// skuPageSize is the largest page the catalog serves. +const skuPageSize = 5000 + // realRecommenderIterator wraps the real recommender iterator. type realRecommenderIterator struct { it *recommender.RecommendationIterator @@ -1099,7 +1110,7 @@ func (c *Client) getOrCreateBillingService(ctx context.Context) (BillingService, return nil, fmt.Errorf("failed to create billing service: %w", err) } - return &realBillingService{service: service}, nil + return &realBillingService{ctx: ctx, service: service}, nil } // extractComputePricingFromSKUs extracts on-demand and commitment pricing from SKU list. diff --git a/providers/gcp/services/computeengine/paging_test.go b/providers/gcp/services/computeengine/paging_test.go new file mode 100644 index 00000000..94672849 --- /dev/null +++ b/providers/gcp/services/computeengine/paging_test.go @@ -0,0 +1,66 @@ +package computeengine + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/api/cloudbilling/v1" + "google.golang.org/api/option" +) + +// catalogServer serves total synthetic SKUs in pages of pageSize and records the +// page tokens it was asked for. +func catalogServer(t *testing.T, total, pageSize int, tokens *[]string) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("pageToken") + *tokens = append(*tokens, token) + start := 0 + if token != "" { + start, _ = strconv.Atoi(token) + } + end := min(start+pageSize, total) + var body strings.Builder + body.WriteString(`{"skus":[`) + for i := start; i < end; i++ { + if i > start { + body.WriteString(",") + } + fmt.Fprintf(&body, `{"skuId":"sku-%d"}`, i) + } + body.WriteString("]") + if end < total { + fmt.Fprintf(&body, `,"nextPageToken":"%d"`, end) + } + body.WriteString("}") + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(body.String())) + })) +} + +func billingAgainst(t *testing.T, srv *httptest.Server) *realBillingService { + t.Helper() + api, err := cloudbilling.NewService(context.Background(), option.WithEndpoint(srv.URL), option.WithoutAuthentication()) + require.NoError(t, err) + return &realBillingService{ctx: context.Background(), service: api} +} + +// The live Compute Engine catalog is 33,453 SKUs over 7 pages of 5000; reading +// only the first page left the commitment SKUs unseen. +func TestRealBillingService_ListSKUsFollowsEveryPage(t *testing.T) { + var tokens []string + srv := catalogServer(t, 33453, 5000, &tokens) + defer srv.Close() + + resp, err := billingAgainst(t, srv).ListSKUs("services/6F81-5844-456A") + require.NoError(t, err) + assert.Len(t, resp.Skus, 33453) + assert.Len(t, tokens, 7) +}