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..346eb1ccb1a --- /dev/null +++ b/hyperdrive.go @@ -0,0 +1,195 @@ +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"` +} + +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, params ListHyperdriveConfigParams) ([]HyperdriveConfig, error) { + if rc.Identifier == "" { + return []HyperdriveConfig{}, ErrMissingAccountID + } + + 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..633ce9cbbdc --- /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: BoolPtr(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(""), ListHyperdriveConfigParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + 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]) + } +} + +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: BoolPtr(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: BoolPtr(false), + MaxAge: 30, + StaleWhileRevalidate: 15, + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, testHyperdriveConfig(), result) + } +}