diff --git a/tiered_cache.go b/tiered_cache.go new file mode 100644 index 00000000000..ea6028a17d6 --- /dev/null +++ b/tiered_cache.go @@ -0,0 +1,300 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +type TieredCacheType int + +const ( + TieredCacheOff TieredCacheType = 0 + TieredCacheGeneric TieredCacheType = 1 + TieredCacheSmart TieredCacheType = 2 +) + +type TieredCache struct { + Type TieredCacheType + LastModified time.Time +} + +const notFoundError = "Unable to retrieve tiered_cache_smart_topology_enable setting value. The zone setting does not exist. (1142)" + +// GetTieredCache allows you to retrive the current Tiered Cache Settings for a Zone +// This function does not support custom topologies, only Generic and Smart Tiered Caching +// +// API Reference: TODO +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: TODO +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: TODO +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 { + if err.Error() == notFoundError { + 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.MethodDelete, 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 { + if err.Error() == notFoundError { + 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 00000000000..1bf7f5ade89 --- /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) + } + }) +}