Skip to content
15 changes: 13 additions & 2 deletions providers/gcp/services/computeengine/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
66 changes: 66 additions & 0 deletions providers/gcp/services/computeengine/paging_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading