-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add product entitlement check for Origin Inspector before enabling th…
…e subscriber for origins. (#118) * check if origins are available as a product before subscribing * s/productCache.Fetch/productCache.Refresh/g * refactor ProductCache * consolidate Product structs * add periodic producRefresh. Refactor manager * manager will track subscribers by type rather than by service * prodcut cache with refresh every 10m by default * fix typos * refactor manager key to be a struct * use a bool for product access. This way if access is removed the subscribers will be stopped * fail open * remove managedKeysWithLock func and move sorting to the tests * fix the tests. fix the linting. fix the formatting * remove unnecessary lookups to the managed map.
- Loading branch information
Showing
8 changed files
with
423 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package api | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"sync" | ||
|
||
"github.com/go-kit/log" | ||
"github.com/go-kit/log/level" | ||
) | ||
|
||
const ( | ||
// Default is the standard real-time stats available to all services | ||
Default = "default" | ||
// OriginInspector is the product name used to determine access to Origin Inspector via the entitlement API | ||
OriginInspector = "origin_inspector" | ||
) | ||
|
||
// Products is the slice of available products supported by real-time stats. | ||
var Products = []string{Default, OriginInspector} | ||
|
||
// Product models the response from the Fastly Product Entitlement API. | ||
type Product struct { | ||
HasAccess bool `json:"has_access"` | ||
Meta struct { | ||
Name string `json:"id"` | ||
} `json:"product"` | ||
} | ||
|
||
// ProductCache fetches product information from the Fastly Product Entitlement API | ||
// and stores results in a local cache. | ||
type ProductCache struct { | ||
client HTTPClient | ||
token string | ||
logger log.Logger | ||
|
||
mtx sync.Mutex | ||
products map[string]bool | ||
} | ||
|
||
// NewProductCache returns an empty cache of Product information. Use the Refresh method | ||
// to populate with data. | ||
func NewProductCache(client HTTPClient, token string, logger log.Logger) *ProductCache { | ||
return &ProductCache{ | ||
client: client, | ||
token: token, | ||
logger: logger, | ||
products: make(map[string]bool), | ||
} | ||
} | ||
|
||
// 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 == Default { | ||
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.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) | ||
} | ||
|
||
var response Product | ||
|
||
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.Meta.Name, "hasAccess", response.HasAccess) | ||
|
||
p.mtx.Lock() | ||
p.products[response.Meta.Name] = response.HasAccess | ||
p.mtx.Unlock() | ||
|
||
} | ||
|
||
return nil | ||
} | ||
|
||
// HasAccess takes a product as a string and returns a boolean | ||
// based on the response from the Product API. | ||
func (p *ProductCache) HasAccess(product string) bool { | ||
if product == Default { | ||
return true | ||
} | ||
p.mtx.Lock() | ||
defer p.mtx.Unlock() | ||
if v, ok := p.products[product]; ok { | ||
return v | ||
} | ||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package api_test | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/fastly/fastly-exporter/pkg/api" | ||
"github.com/go-kit/log" | ||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
func TestProductCache(t *testing.T) { | ||
t.Parallel() | ||
|
||
for _, testcase := range []struct { | ||
name string | ||
client api.HTTPClient | ||
wantProds map[string]bool | ||
wantErr error | ||
}{ | ||
{ | ||
name: "success", | ||
client: newSequentialResponseClient(productsResponseOne, productsResponseTwo), | ||
wantErr: nil, | ||
wantProds: map[string]bool{ | ||
"origin_inspector": true, | ||
}, | ||
}, | ||
{ | ||
name: "error", | ||
client: fixedResponseClient{code: http.StatusUnauthorized}, | ||
wantErr: &api.Error{Code: http.StatusUnauthorized}, | ||
wantProds: map[string]bool{}, | ||
}, | ||
} { | ||
t.Run(testcase.name, func(t *testing.T) { | ||
var ( | ||
ctx = context.Background() | ||
client = testcase.client | ||
cache = api.NewProductCache(client, "irrelevant token", log.NewNopLogger()) | ||
) | ||
|
||
// err | ||
if want, have := testcase.wantErr, cache.Refresh(ctx); !cmp.Equal(want, have) { | ||
t.Fatal(cmp.Diff(want, have)) | ||
} | ||
|
||
for k, v := range testcase.wantProds { | ||
if v != cache.HasAccess(k) { | ||
t.Fatalf("expected %v, got %v for %v", v, cache.HasAccess(k), k) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
const productsResponseOne = ` | ||
{ | ||
"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": "" | ||
} | ||
} | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.