From 8a21738ffc6267aeb2531b1b01411a4a9f77e848 Mon Sep 17 00:00:00 2001 From: Taylor Raack Date: Fri, 5 Jan 2024 08:34:26 -1000 Subject: [PATCH 1/3] Update product cache to use /entitlements endpoint --- pkg/api/product_cache.go | 91 +++++++++++++++++++++++------------ pkg/api/product_cache_test.go | 52 +++++++++----------- 2 files changed, 82 insertions(+), 61 deletions(-) diff --git a/pkg/api/product_cache.go b/pkg/api/product_cache.go index 73ef221..971280a 100644 --- a/pkg/api/product_cache.go +++ b/pkg/api/product_cache.go @@ -27,12 +27,20 @@ const ( // Products is the slice of available products supported by real-time stats. var Products = []string{ProductDefault, ProductOriginInspector, ProductDomainInspector} -// Product models the response from the Fastly Product Entitlement API. -type Product struct { - HasAccess bool `json:"has_access"` - Product struct { - Name string `json:"id"` - } `json:"product"` +type response struct { + Customers *[]customer `json:"customers,omitempty"` +} + +type customer struct { + Contracts *[]contract `json:"contracts,omitempty"` +} + +type contract struct { + Items *[]item `json:"items,omitempty"` +} + +type item struct { + ProductID *string `json:"product_id,omitempty"` } // ProductCache fetches product information from the Fastly Product Entitlement API @@ -59,41 +67,60 @@ func NewProductCache(client HTTPClient, token string, logger log.Logger) *Produc // Refresh requests data from the Fastly API and stores data in the cache. func (p *ProductCache) Refresh(ctx context.Context) error { - for _, product := range Products { - if product == ProductDefault { - continue - } - uri := fmt.Sprintf("https://api.fastly.com/entitled-products/%s", product) - req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) - if err != nil { - return fmt.Errorf("error constructing API product request: %w", err) - } + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.fastly.com/entitlements", nil) + if err != nil { + return fmt.Errorf("error constructing API product request: %w", err) + } - req.Header.Set("Fastly-Key", p.token) - req.Header.Set("Accept", "application/json") - resp, err := p.client.Do(req) - if err != nil { - return fmt.Errorf("error executing API product request: %w", err) - } - defer resp.Body.Close() + req.Header.Set("Fastly-Key", p.token) + req.Header.Set("Accept", "application/json") + resp, err := p.client.Do(req) + if err != nil { + return fmt.Errorf("error executing API product request: %w", err) + } + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return NewError(resp) - } + if resp.StatusCode != http.StatusOK { + return NewError(resp) + } - var response Product + var response response - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - return fmt.Errorf("error decoding API product response: %w", err) - } + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("error decoding API product response: %w", err) + } - level.Debug(p.logger).Log("product", response.Product.Name, "hasAccess", response.HasAccess) + activeProducts := make(map[string]interface{}) + + if response.Customers != nil { + for _, customer := range *response.Customers { + if customer.Contracts == nil { + continue + } + for _, contract := range *customer.Contracts { + if contract.Items == nil { + continue + } + for _, item := range *contract.Items { + if item.ProductID == nil { + continue + } + activeProducts[*item.ProductID] = true + } + } + } + } + for _, product := range Products { + if product == ProductDefault { + continue + } + _, hasAccess := activeProducts[product] + level.Debug(p.logger).Log("product", product, "hasAccess", hasAccess) p.mtx.Lock() - p.products[response.Product.Name] = response.HasAccess + p.products[product] = hasAccess p.mtx.Unlock() - } return nil diff --git a/pkg/api/product_cache_test.go b/pkg/api/product_cache_test.go index 352ae2c..852b144 100644 --- a/pkg/api/product_cache_test.go +++ b/pkg/api/product_cache_test.go @@ -21,7 +21,7 @@ func TestProductCache(t *testing.T) { }{ { name: "success", - client: newSequentialResponseClient(productsResponseOne, productsResponseTwo), + client: newSequentialResponseClient(productsResponse), wantErr: nil, wantProds: map[string]bool{ "origin_inspector": true, @@ -55,34 +55,28 @@ func TestProductCache(t *testing.T) { } } -const productsResponseOne = ` +// only products that the customer is entitled to are returned from the /entitlements endpoint +const productsResponse = ` { - "product": { - "id": "origin_inspector", - "object": "product" - }, - "has_access": true, - "access_level": "Origin_Inspector", - "has_permission_to_enable": false, - "has_permission_to_disable": true, - "_links": { - "self": "" - } -} -` - -const productsResponseTwo = ` -{ - "product": { - "id": "domain_inspector", - "object": "product" - }, - "has_access": false, - "access_level": "Domain_Inspector", - "has_permission_to_enable": false, - "has_permission_to_disable": true, - "_links": { - "self": "" - } + "customers": [ + { + "contracts": [ + { + "items": [ + { + "product_id": "origin_inspector" + }, + { + "other_key": "other" + } + ] + }, + { + "other_key_2": [ + ] + } + ] + } + ] } ` From 925184b514cc62f81711cb4afd3ac659ac5c8c69 Mon Sep 17 00:00:00 2001 From: Taylor Raack Date: Mon, 8 Jan 2024 14:19:24 -1000 Subject: [PATCH 2/3] update for Lukas --- pkg/api/common_test.go | 30 ------------------------ pkg/api/product_cache.go | 44 ++++++++++++++--------------------- pkg/api/product_cache_test.go | 17 +++++++++++++- 3 files changed, 34 insertions(+), 57 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index b0f899d..ff4a07f 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -54,33 +54,3 @@ func (c paginatedResponseClient) Do(req *http.Request) (*http.Response, error) { }).ServeHTTP(rec, req) return rec.Result(), nil } - -// -// -// - -type sequentialResponseClient struct { - responses []string -} - -func newSequentialResponseClient(responses ...string) *sequentialResponseClient { - return &sequentialResponseClient{ - responses: responses, - } -} - -func (c *sequentialResponseClient) Do(req *http.Request) (*http.Response, error) { - var response string - if len(c.responses) <= 1 { - response = c.responses[0] - } else { - response, c.responses = c.responses[0], c.responses[1:] - } - - rec := httptest.NewRecorder() - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, response) - }).ServeHTTP(rec, req) - return rec.Result(), nil -} diff --git a/pkg/api/product_cache.go b/pkg/api/product_cache.go index 971280a..7b4b524 100644 --- a/pkg/api/product_cache.go +++ b/pkg/api/product_cache.go @@ -27,20 +27,14 @@ const ( // Products is the slice of available products supported by real-time stats. var Products = []string{ProductDefault, ProductOriginInspector, ProductDomainInspector} -type response struct { - Customers *[]customer `json:"customers,omitempty"` -} - -type customer struct { - Contracts *[]contract `json:"contracts,omitempty"` -} - -type contract struct { - Items *[]item `json:"items,omitempty"` -} - -type item struct { - ProductID *string `json:"product_id,omitempty"` +type entitlementsResponse struct { + Customers []struct { + Contracts []struct { + Items []struct { + ProductID *string `json:"product_id,omitempty"` + } `json:"items,omitempty"` + } `json:"contracts"` + } `json:"customers"` } // ProductCache fetches product information from the Fastly Product Entitlement API @@ -85,7 +79,7 @@ func (p *ProductCache) Refresh(ctx context.Context) error { return NewError(resp) } - var response response + var response entitlementsResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { return fmt.Errorf("error decoding API product response: %w", err) @@ -93,21 +87,19 @@ func (p *ProductCache) Refresh(ctx context.Context) error { activeProducts := make(map[string]interface{}) - if response.Customers != nil { - for _, customer := range *response.Customers { - if customer.Contracts == nil { + for _, customer := range response.Customers { + if customer.Contracts == nil { + continue + } + for _, contract := range customer.Contracts { + if contract.Items == nil { continue } - for _, contract := range *customer.Contracts { - if contract.Items == nil { + for _, item := range contract.Items { + if item.ProductID == nil { continue } - for _, item := range *contract.Items { - if item.ProductID == nil { - continue - } - activeProducts[*item.ProductID] = true - } + activeProducts[*item.ProductID] = true } } } diff --git a/pkg/api/product_cache_test.go b/pkg/api/product_cache_test.go index 852b144..63cd8f3 100644 --- a/pkg/api/product_cache_test.go +++ b/pkg/api/product_cache_test.go @@ -21,12 +21,18 @@ func TestProductCache(t *testing.T) { }{ { name: "success", - client: newSequentialResponseClient(productsResponse), + client: fixedResponseClient{response: productsResponse, code: http.StatusOK}, wantErr: nil, wantProds: map[string]bool{ "origin_inspector": true, }, }, + { + name: "noCustomerSuccess", + client: fixedResponseClient{response: noCustomerResponse, code: http.StatusOK}, + wantErr: nil, + wantProds: map[string]bool{}, + }, { name: "error", client: fixedResponseClient{code: http.StatusUnauthorized}, @@ -76,7 +82,16 @@ const productsResponse = ` ] } ] + }, + { + "other key": {} } ] } ` + +const noCustomerResponse = ` +{ + "metadata": {} +} +` From eea734b7d949d034f631cd1a01b79349ceb61bdf Mon Sep 17 00:00:00 2001 From: Taylor Raack Date: Thu, 11 Jan 2024 11:54:05 -1000 Subject: [PATCH 3/3] one more update for Lukas --- pkg/api/product_cache.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/api/product_cache.go b/pkg/api/product_cache.go index 7b4b524..0229513 100644 --- a/pkg/api/product_cache.go +++ b/pkg/api/product_cache.go @@ -88,13 +88,7 @@ func (p *ProductCache) Refresh(ctx context.Context) error { activeProducts := make(map[string]interface{}) for _, customer := range response.Customers { - if customer.Contracts == nil { - continue - } for _, contract := range customer.Contracts { - if contract.Items == nil { - continue - } for _, item := range contract.Items { if item.ProductID == nil { continue