From 204023669540f2cc681df15727da5d81bc22fda9 Mon Sep 17 00:00:00 2001 From: Carl Henderson Date: Wed, 31 Jan 2024 11:55:51 +0000 Subject: [PATCH 1/2] Add Hyperdrive CRUD support --- .changelog/1492.txt | 3 + hyperdrive.go | 194 ++++++++++++++++++++++++++++++ hyperdrive_test.go | 285 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 482 insertions(+) create mode 100644 .changelog/1492.txt create mode 100644 hyperdrive.go create mode 100644 hyperdrive_test.go diff --git a/.changelog/1492.txt b/.changelog/1492.txt new file mode 100644 index 00000000000..f46c87416b6 --- /dev/null +++ b/.changelog/1492.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +hyperdrive: Add support for hyperdrive CRUD operations +``` diff --git a/hyperdrive.go b/hyperdrive.go new file mode 100644 index 00000000000..e0a827b1c8e --- /dev/null +++ b/hyperdrive.go @@ -0,0 +1,194 @@ +package cloudflare + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/goccy/go-json" +) + +var ( + ErrMissingHyperdriveConfigID = errors.New("required hyperdrive config id is missing") + ErrMissingHyperdriveConfigName = errors.New("required hyperdrive config name is missing") + ErrMissingHyperdriveConfigPassword = errors.New("required hyperdrive config password is missing") +) + +type HyperdriveConfig struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Origin HyperdriveConfigOrigin `json:"origin,omitempty"` + Caching HyperdriveConfigCaching `json:"caching,omitempty"` +} + +type HyperdriveConfigOrigin struct { + Database string `json:"database,omitempty"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + Scheme string `json:"scheme,omitempty"` + User string `json:"user,omitempty"` +} + +type HyperdriveConfigCaching struct { + Disabled bool `json:"disabled,omitempty"` + MaxAge int `json:"max_age,omitempty"` + StaleWhileRevalidate int `json:"stale_while_revalidate,omitempty"` +} + +type HyperdriveConfigListResponse struct { + Response + Result []HyperdriveConfig `json:"result"` +} + +type CreateHyperdriveConfigParams struct { + Name string `json:"name"` + Password string `json:"password"` + Origin HyperdriveConfigOrigin `json:"origin"` + Caching HyperdriveConfigCaching `json:"caching,omitempty"` +} + +type HyperdriveConfigResponse struct { + Response + Result HyperdriveConfig `json:"result"` +} + +type UpdateHyperdriveConfigParams struct { + HyperdriveID string `json:"-"` + Name string `json:"name"` + Password string `json:"password"` + Origin HyperdriveConfigOrigin `json:"origin"` + Caching HyperdriveConfigCaching `json:"caching,omitempty"` +} + +// ListHyperdriveConfigs returns the Hyperdrive configs owned by an account. +// +// API reference: https://developers.cloudflare.com/api/operations/list-hyperdrive +func (api *API) ListHyperdriveConfigs(ctx context.Context, rc *ResourceContainer) ([]HyperdriveConfig, error) { + if rc.Identifier == "" { + return []HyperdriveConfig{}, ErrMissingAccountID + } + + var hResponse HyperdriveConfigListResponse + hResponse = HyperdriveConfigListResponse{} + uri := fmt.Sprintf("/accounts/%s/hyperdrive/configs", rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []HyperdriveConfig{}, err + } + + err = json.Unmarshal(res, &hResponse) + if err != nil { + return []HyperdriveConfig{}, fmt.Errorf("failed to unmarshal filters JSON data: %w", err) + } + + return hResponse.Result, nil +} + +// CreateHyperdriveConfig creates a new Hyperdrive config. +// +// API reference: https://developers.cloudflare.com/api/operations/create-hyperdrive +func (api *API) CreateHyperdriveConfig(ctx context.Context, rc *ResourceContainer, params CreateHyperdriveConfigParams) (HyperdriveConfig, error) { + if rc.Identifier == "" { + return HyperdriveConfig{}, ErrMissingAccountID + } + + if params.Name == "" { + return HyperdriveConfig{}, ErrMissingHyperdriveConfigName + } + + if params.Password == "" { + return HyperdriveConfig{}, ErrMissingHyperdriveConfigPassword + } + + uri := fmt.Sprintf("/accounts/%s/hyperdrive/configs", rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return HyperdriveConfig{}, err + } + + var r HyperdriveConfigResponse + err = json.Unmarshal(res, &r) + if err != nil { + return HyperdriveConfig{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r.Result, nil +} + +// DeleteHyperdriveConfig deletes a Hyperdrive config. +// +// API reference: https://developers.cloudflare.com/api/operations/delete-hyperdrive +func (api *API) DeleteHyperdriveConfig(ctx context.Context, rc *ResourceContainer, hyperdriveID string) error { + if rc.Identifier == "" { + return ErrMissingAccountID + } + if hyperdriveID == "" { + return ErrMissingHyperdriveConfigID + } + + uri := fmt.Sprintf("/accounts/%s/hyperdrive/configs/%s", rc.Identifier, hyperdriveID) + _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + return nil +} + +// GetHyperdriveConfig returns a single Hyperdrive config based on the ID. +// +// API reference: https://developers.cloudflare.com/api/operations/get-hyperdrive +func (api *API) GetHyperdriveConfig(ctx context.Context, rc *ResourceContainer, hyperdriveID string) (HyperdriveConfig, error) { + if rc.Identifier == "" { + return HyperdriveConfig{}, ErrMissingAccountID + } + + if hyperdriveID == "" { + return HyperdriveConfig{}, ErrMissingHyperdriveConfigID + } + + uri := fmt.Sprintf("/accounts/%s/hyperdrive/configs/%s", rc.Identifier, hyperdriveID) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return HyperdriveConfig{}, err + } + + var r HyperdriveConfigResponse + err = json.Unmarshal(res, &r) + if err != nil { + return HyperdriveConfig{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r.Result, nil +} + +// UpdateHyperdriveConfig updates a Hyperdrive config. +// +// API reference: https://developers.cloudflare.com/api/operations/update-hyperdrive +func (api *API) UpdateHyperdriveConfig(ctx context.Context, rc *ResourceContainer, params UpdateHyperdriveConfigParams) (HyperdriveConfig, error) { + if rc.Identifier == "" { + return HyperdriveConfig{}, ErrMissingAccountID + } + + if params.HyperdriveID == "" { + return HyperdriveConfig{}, ErrMissingHyperdriveConfigID + } + + uri := fmt.Sprintf("/accounts/%s/hyperdrive/configs/%s", rc.Identifier, params.HyperdriveID) + + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) + if err != nil { + return HyperdriveConfig{}, err + } + + var r HyperdriveConfigResponse + err = json.Unmarshal(res, &r) + if err != nil { + return HyperdriveConfig{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r.Result, nil +} diff --git a/hyperdrive_test.go b/hyperdrive_test.go new file mode 100644 index 00000000000..8b4545fdf96 --- /dev/null +++ b/hyperdrive_test.go @@ -0,0 +1,285 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + testHyperdriveConfigId = "6b7efc370ea34ded8327fa20698dfe3a" + testHyperdriveConfigName = "example-hyperdrive" +) + +func testHyperdriveConfig() HyperdriveConfig { + return HyperdriveConfig{ + ID: testHyperdriveConfigId, + Name: testHyperdriveConfigName, + Origin: HyperdriveConfigOrigin{ + Database: "postgres", + Host: "database.example.com", + Port: 5432, + Scheme: "postgres", + User: "postgres", + }, + Caching: HyperdriveConfigCaching{ + Disabled: false, + MaxAge: 30, + StaleWhileRevalidate: 15, + }, + } +} + +func TestHyperdriveConfig_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/accounts/%s/hyperdrive/configs", testAccountID), func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [{ + "id": "6b7efc370ea34ded8327fa20698dfe3a", + "caching": { + "disabled": false, + "max_age": 30, + "stale_while_revalidate": 15 + }, + "name": "example-hyperdrive", + "origin": { + "database": "postgres", + "host": "database.example.com", + "port": 5432, + "scheme": "postgres", + "user": "postgres" + } + }] + }`) + }) + + _, err := client.ListHyperdriveConfigs(context.Background(), AccountIdentifier("")) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + result, err := client.ListHyperdriveConfigs(context.Background(), AccountIdentifier(testAccountID)) + if assert.NoError(t, err) { + assert.Equal(t, 1, len(result)) + assert.Equal(t, testHyperdriveConfig(), result[0]) + } +} + +func TestHyperdriveConfig_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/accounts/%s/hyperdrive/configs/%s", testAccountID, testHyperdriveConfigId), func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "6b7efc370ea34ded8327fa20698dfe3a", + "caching": { + "disabled": false, + "max_age": 30, + "stale_while_revalidate": 15 + }, + "name": "example-hyperdrive", + "origin": { + "database": "postgres", + "host": "database.example.com", + "port": 5432, + "scheme": "postgres", + "user": "postgres" + } + } + }`) + }) + + _, err := client.GetHyperdriveConfig(context.Background(), AccountIdentifier(""), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.GetHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingHyperdriveConfigID, err) + } + + result, err := client.GetHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), testHyperdriveConfigId) + if assert.NoError(t, err) { + assert.Equal(t, testHyperdriveConfig(), result) + } +} + +func TestHyperdriveConfig_Create(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/accounts/%s/hyperdrive/configs", testAccountID), func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "6b7efc370ea34ded8327fa20698dfe3a", + "caching": { + "disabled": false, + "max_age": 30, + "stale_while_revalidate": 15 + }, + "name": "example-hyperdrive", + "origin": { + "database": "postgres", + "host": "database.example.com", + "port": 5432, + "scheme": "postgres", + "user": "postgres" + } + } + }`) + }) + + _, err := client.CreateHyperdriveConfig(context.Background(), AccountIdentifier(""), CreateHyperdriveConfigParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.CreateHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), CreateHyperdriveConfigParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingHyperdriveConfigName, err) + } + + _, err = client.CreateHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), CreateHyperdriveConfigParams{Name: "example-hyperdrive"}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingHyperdriveConfigPassword, err) + } + + result, err := client.CreateHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), CreateHyperdriveConfigParams{ + Name: "example-hyperdrive", + Password: "password", + Origin: HyperdriveConfigOrigin{ + Database: "postgres", + Host: "database.example.com", + Port: 5432, + Scheme: "postgres", + User: "postgres", + }, + Caching: HyperdriveConfigCaching{ + Disabled: false, + MaxAge: 30, + StaleWhileRevalidate: 15, + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, testHyperdriveConfig(), result) + } +} + +func TestHyperdriveConfig_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/accounts/%s/hyperdrive/configs/%s", testAccountID, testHyperdriveConfigId), func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": null + }`) + }) + err := client.DeleteHyperdriveConfig(context.Background(), AccountIdentifier(""), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + err = client.DeleteHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingHyperdriveConfigID, err) + } + + err = client.DeleteHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), testHyperdriveConfigId) + assert.NoError(t, err) +} + +func TestHyperdriveConfig_Update(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/accounts/%s/hyperdrive/configs/%s", testAccountID, testHyperdriveConfigId), func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "6b7efc370ea34ded8327fa20698dfe3a", + "caching": { + "disabled": false, + "max_age": 30, + "stale_while_revalidate": 15 + }, + "name": "example-hyperdrive", + "origin": { + "database": "postgres", + "host": "database.example.com", + "port": 5432, + "scheme": "postgres", + "user": "postgres" + } + } + }`) + }) + + _, err := client.UpdateHyperdriveConfig(context.Background(), AccountIdentifier(""), UpdateHyperdriveConfigParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.UpdateHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), UpdateHyperdriveConfigParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingHyperdriveConfigID, err) + } + + result, err := client.UpdateHyperdriveConfig(context.Background(), AccountIdentifier(testAccountID), UpdateHyperdriveConfigParams{ + HyperdriveID: "6b7efc370ea34ded8327fa20698dfe3a", + Name: "example-hyperdrive", + Password: "password", + Origin: HyperdriveConfigOrigin{ + Database: "postgres", + Host: "database.example.com", + Port: 5432, + Scheme: "postgres", + User: "postgres", + }, + Caching: HyperdriveConfigCaching{ + Disabled: false, + MaxAge: 30, + StaleWhileRevalidate: 15, + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, testHyperdriveConfig(), result) + } +} From 2a4f15503dc337c421df286dd1f0c8c44b83cb09 Mon Sep 17 00:00:00 2001 From: Carl Henderson Date: Thu, 1 Feb 2024 07:07:48 +0000 Subject: [PATCH 2/2] Address PR feedback --- hyperdrive.go | 13 +++++++------ hyperdrive_test.go | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/hyperdrive.go b/hyperdrive.go index e0a827b1c8e..346eb1ccb1a 100644 --- a/hyperdrive.go +++ b/hyperdrive.go @@ -31,9 +31,9 @@ type HyperdriveConfigOrigin struct { } type HyperdriveConfigCaching struct { - Disabled bool `json:"disabled,omitempty"` - MaxAge int `json:"max_age,omitempty"` - StaleWhileRevalidate int `json:"stale_while_revalidate,omitempty"` + Disabled *bool `json:"disabled,omitempty"` + MaxAge int `json:"max_age,omitempty"` + StaleWhileRevalidate int `json:"stale_while_revalidate,omitempty"` } type HyperdriveConfigListResponse struct { @@ -61,16 +61,17 @@ type UpdateHyperdriveConfigParams struct { Caching HyperdriveConfigCaching `json:"caching,omitempty"` } +type ListHyperdriveConfigParams struct{} + // ListHyperdriveConfigs returns the Hyperdrive configs owned by an account. // // API reference: https://developers.cloudflare.com/api/operations/list-hyperdrive -func (api *API) ListHyperdriveConfigs(ctx context.Context, rc *ResourceContainer) ([]HyperdriveConfig, error) { +func (api *API) ListHyperdriveConfigs(ctx context.Context, rc *ResourceContainer, params ListHyperdriveConfigParams) ([]HyperdriveConfig, error) { if rc.Identifier == "" { return []HyperdriveConfig{}, ErrMissingAccountID } - var hResponse HyperdriveConfigListResponse - hResponse = HyperdriveConfigListResponse{} + hResponse := HyperdriveConfigListResponse{} uri := fmt.Sprintf("/accounts/%s/hyperdrive/configs", rc.Identifier) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) diff --git a/hyperdrive_test.go b/hyperdrive_test.go index 8b4545fdf96..633ce9cbbdc 100644 --- a/hyperdrive_test.go +++ b/hyperdrive_test.go @@ -26,7 +26,7 @@ func testHyperdriveConfig() HyperdriveConfig { User: "postgres", }, Caching: HyperdriveConfigCaching{ - Disabled: false, + Disabled: BoolPtr(false), MaxAge: 30, StaleWhileRevalidate: 15, }, @@ -64,12 +64,12 @@ func TestHyperdriveConfig_List(t *testing.T) { }`) }) - _, err := client.ListHyperdriveConfigs(context.Background(), AccountIdentifier("")) + _, err := client.ListHyperdriveConfigs(context.Background(), AccountIdentifier(""), ListHyperdriveConfigParams{}) if assert.Error(t, err) { assert.Equal(t, ErrMissingAccountID, err) } - result, err := client.ListHyperdriveConfigs(context.Background(), AccountIdentifier(testAccountID)) + result, err := client.ListHyperdriveConfigs(context.Background(), AccountIdentifier(testAccountID), ListHyperdriveConfigParams{}) if assert.NoError(t, err) { assert.Equal(t, 1, len(result)) assert.Equal(t, testHyperdriveConfig(), result[0]) @@ -180,7 +180,7 @@ func TestHyperdriveConfig_Create(t *testing.T) { User: "postgres", }, Caching: HyperdriveConfigCaching{ - Disabled: false, + Disabled: BoolPtr(false), MaxAge: 30, StaleWhileRevalidate: 15, }, @@ -273,7 +273,7 @@ func TestHyperdriveConfig_Update(t *testing.T) { User: "postgres", }, Caching: HyperdriveConfigCaching{ - Disabled: false, + Disabled: BoolPtr(false), MaxAge: 30, StaleWhileRevalidate: 15, },