From 1be29fa28f882e7bacbb1803e551a48b2837cf02 Mon Sep 17 00:00:00 2001 From: Mikey Sleevi Date: Tue, 20 Dec 2022 20:53:21 -0700 Subject: [PATCH] Adds support for Tiered Caching Adds support for utilizing previously undocumented APIs for Tiered Caching endpoints. It allows for setting zones to utilize either generic, smart or off. Implementation: * Adds three new api methods for manipulating tiered cache settings * GetTieredCache which returns the current setting * SetTieredCache which allows for manipulation of existing settings * DeleteTieredCache which allows for unsetting tiered cache --- .changelog/1149.txt | 3 + tiered_cache.go | 316 ++++++++++++++++++++++++++++++++++++++++++ tiered_cache_test.go | 321 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 640 insertions(+) create mode 100644 .changelog/1149.txt create mode 100644 tiered_cache.go create mode 100644 tiered_cache_test.go diff --git a/.changelog/1149.txt b/.changelog/1149.txt new file mode 100644 index 000000000..c6c40af07 --- /dev/null +++ b/.changelog/1149.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +tiered_cache: Add support for Tiered Caching interactions for setting Smart and Generic topologies +``` \ No newline at end of file diff --git a/tiered_cache.go b/tiered_cache.go new file mode 100644 index 000000000..77b0ce5bb --- /dev/null +++ b/tiered_cache.go @@ -0,0 +1,316 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +type TieredCacheType int + +const ( + TieredCacheOff TieredCacheType = 0 + TieredCacheGeneric TieredCacheType = 1 + TieredCacheSmart TieredCacheType = 2 +) + +func (e TieredCacheType) String() string { + switch e { + case TieredCacheGeneric: + return "generic" + case TieredCacheSmart: + return "smart" + case TieredCacheOff: + return "off" + default: + return fmt.Sprintf("%d", int(e)) + } +} + +type TieredCache struct { + Type TieredCacheType + LastModified time.Time +} + +// GetTieredCache allows you to retrieve the current Tiered Cache Settings for a Zone. +// This function does not support custom topologies, only Generic and Smart Tiered Caching. +// +// API Reference: https://api.cloudflare.com/#smart-tiered-cache-get-smart-tiered-cache-setting +// API Reference: https://api.cloudflare.com/#tiered-cache-get-tiered-cache-setting +func (api *API) GetTieredCache(ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + var lastModified time.Time + + generic, err := getGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = generic.LastModified + + smart, err := getSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if smart.LastModified.After(lastModified) { + lastModified = smart.LastModified + } + + if generic.Type == TieredCacheOff { + return TieredCache{Type: TieredCacheOff, LastModified: lastModified}, nil + } + + if smart.Type == TieredCacheOff { + return TieredCache{Type: TieredCacheGeneric, LastModified: lastModified}, nil + } + + return TieredCache{Type: TieredCacheSmart, LastModified: lastModified}, nil +} + +// SetTieredCache allows you to set a zone's tiered cache topology between the available types. +// Using the value of TieredCacheOff will disable Tiered Cache entirely. +// +// API Reference: https://api.cloudflare.com/#smart-tiered-cache-patch-smart-tiered-cache-setting +// API Reference: https://api.cloudflare.com/#tiered-cache-patch-tiered-cache-setting +func (api *API) SetTieredCache(ctx context.Context, rc *ResourceContainer, value TieredCacheType) (TieredCache, error) { + if value == TieredCacheOff { + return api.DeleteTieredCache(ctx, rc) + } + + var lastModified time.Time + + if value == TieredCacheGeneric { + result, err := deleteSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = result.LastModified + + result, err = enableGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if result.LastModified.After(lastModified) { + lastModified = result.LastModified + } + return TieredCache{Type: TieredCacheGeneric, LastModified: lastModified}, nil + } + + result, err := enableGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = result.LastModified + + result, err = enableSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if result.LastModified.After(lastModified) { + lastModified = result.LastModified + } + return TieredCache{Type: TieredCacheSmart, LastModified: lastModified}, nil +} + +// DeleteTieredCache allows you to delete the tiered cache settings for a zone. +// This is equivalent to using SetTieredCache with the value of TieredCacheOff. +// +// API Reference: https://api.cloudflare.com/#smart-tiered-cache-delete-smart-tiered-cache-setting +// API Reference: https://api.cloudflare.com/#tiered-cache-patch-tiered-cache-setting +func (api *API) DeleteTieredCache(ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + var lastModified time.Time + + result, err := deleteSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = result.LastModified + + result, err = disableGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if result.LastModified.After(lastModified) { + lastModified = result.LastModified + } + return TieredCache{Type: TieredCacheOff, LastModified: lastModified}, nil +} + +type tieredCacheResult struct { + ID string `json:"id"` + Value string `json:"value,omitempty"` + LastModified time.Time `json:"modified_on"` +} + +type tieredCacheResponse struct { + Result tieredCacheResult `json:"result"` + Response +} + +type tieredCacheSetting struct { + Value string `json:"value"` +} + +func getGenericTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/argo/tiered_caching", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to retrieve generic tiered cache failed") + } + + if response.Result.Value == "off" { + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil + } + + return TieredCache{Type: TieredCacheGeneric, LastModified: response.Result.LastModified}, nil +} + +func getSmartTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/cache/tiered_cache_smart_topology_enable", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + var notFoundError *NotFoundError + if errors.As(err, ¬FoundError) { + return TieredCache{Type: TieredCacheOff}, nil + } + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to retrieve smart tiered cache failed") + } + + if response.Result.Value == "off" { + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil + } + return TieredCache{Type: TieredCacheSmart, LastModified: response.Result.LastModified}, nil +} + +func enableGenericTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/argo/tiered_caching", rc.Identifier) + setting := tieredCacheSetting{ + Value: "on", + } + body, err := json.Marshal(setting) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, body) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to enable generic tiered cache failed") + } + + return TieredCache{Type: TieredCacheGeneric, LastModified: response.Result.LastModified}, nil +} + +func enableSmartTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/cache/tiered_cache_smart_topology_enable", rc.Identifier) + setting := tieredCacheSetting{ + Value: "on", + } + body, err := json.Marshal(setting) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, body) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to enable smart tiered cache failed") + } + + return TieredCache{Type: TieredCacheSmart, LastModified: response.Result.LastModified}, nil +} + +func disableGenericTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/argo/tiered_caching", rc.Identifier) + setting := tieredCacheSetting{ + Value: "off", + } + body, err := json.Marshal(setting) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, body) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to disable generic tiered cache failed") + } + + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil +} + +func deleteSmartTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/cache/tiered_cache_smart_topology_enable", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + var notFoundError *NotFoundError + if errors.As(err, ¬FoundError) { + return TieredCache{Type: TieredCacheOff}, nil + } + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to disable smart tiered cache failed") + } + + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil +} diff --git a/tiered_cache_test.go b/tiered_cache_test.go new file mode 100644 index 000000000..7277b5315 --- /dev/null +++ b/tiered_cache_test.go @@ -0,0 +1,321 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func createSmartTieredCacheHandler(val string, lastModified string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "editable": true, + "id": "tiered_cache_smart_topology_enable", + "modified_on": "%s", + "value": "%s" + } + }`, lastModified, val) + } +} + +func nonexistentSmartTieredCacheHandler() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + w.WriteHeader(404) + fmt.Fprintf(w, `{ + "result": null, + "success": false, + "errors": [ + { + "code": 1142, + "message": "Unable to retrieve tiered_cache_smart_topology_enable setting value. The zone setting does not exist." + } + ], + "messages": [] + }`) + } +} + +func createGenericTieredCacheHandler(val string, lastModified string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "tiered_caching", + "value": "%s", + "modified_on": "%s", + "editable": false + } + }`, val, lastModified) + } +} + +func TestGetTieredCache(t *testing.T) { + t.Run("can identify when Smart Tiered Cache", func(t *testing.T) { + t.Run("is disabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("off", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("is enabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("zone setting does not exist", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) + + t.Run("can identify when generic tiered cache", func(t *testing.T) { + t.Run("is disabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("off", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheOff, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("is enabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) + + t.Run("determines the latest last modified when", func(t *testing.T) { + t.Run("smart tiered cache zone setting does not exist", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("Generic Tiered Cache was modified more recently", func(t *testing.T) { + setup() + defer teardown() + + earlier := time.Now().Add(time.Minute * -5).Format(time.RFC3339) + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", earlier)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("smart tiered cache was modified more recently", func(t *testing.T) { + setup() + defer teardown() + + earlier := time.Now().Add(time.Minute * -5).Format(time.RFC3339) + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", earlier)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) +} + +func TestSetTieredCache(t *testing.T) { + t.Run("can enable tiered caching", func(t *testing.T) { + t.Run("using smart caching", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.SetTieredCache(context.Background(), ZoneIdentifier(testZoneID), TieredCacheSmart) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("use generic caching", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.SetTieredCache(context.Background(), ZoneIdentifier(testZoneID), TieredCacheGeneric) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) +} + +func TestDeleteTieredCache(t *testing.T) { + t.Run("can disable tiered caching", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("off", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheOff, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) +}