From 27c24f026d5e51138a5c4d9f37674cf45fa1208b Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 2 Oct 2024 18:48:43 -0400 Subject: [PATCH] Add keyspace resize methods (#216) * Add Resize method * Reorder tests * Implement CancelResize * Add ResizeStatus method to keyspace * Add comment * Change param from replicas to extra replicas * Remove omit empty from extra replicas * Make replicas and cluster size pointers on resize, so they can truly be optional --- planetscale/keyspaces.go | 99 ++++++++++++++++++++++++++ planetscale/keyspaces_test.go | 128 ++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) diff --git a/planetscale/keyspaces.go b/planetscale/keyspaces.go index 6ac035a..12816b2 100644 --- a/planetscale/keyspaces.go +++ b/planetscale/keyspaces.go @@ -72,6 +72,47 @@ type branchKeyspacesResponse struct { Keyspaces []*Keyspace `json:"data"` } +type ResizeKeyspaceRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + Keyspace string `json:"-"` + ExtraReplicas *uint `json:"extra_replicas,omitempty"` + ClusterSize *ClusterSize `json:"cluster_size,omitempty"` +} + +type KeyspaceResizeRequest struct { + ID string `json:"id"` + State string `json:"state"` + Actor *Actor `json:"actor"` + + ClusterSize ClusterSize `json:"cluster_rate_name"` + PreviousClusterSize ClusterSize `json:"previous_cluster_rate_name"` + + Replicas uint `json:"replicas"` + ExtraReplicas uint `json:"extra_replicas"` + PreviousReplicas uint `json:"previous_replicas"` + + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + CompletedAt *time.Time `json:"completed_at"` +} + +type CancelKeyspaceResizeRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + Keyspace string `json:"-"` +} + +type KeyspaceResizeStatusRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + Keyspace string `json:"-"` +} + // BranchKeyspaceService is an interface for interacting with the keyspace endpoints of the PlanetScale API type BranchKeyspacesService interface { Create(context.Context, *CreateBranchKeyspaceRequest) (*Keyspace, error) @@ -79,6 +120,9 @@ type BranchKeyspacesService interface { Get(context.Context, *GetBranchKeyspaceRequest) (*Keyspace, error) VSchema(context.Context, *GetKeyspaceVSchemaRequest) (*VSchema, error) UpdateVSchema(context.Context, *UpdateKeyspaceVSchemaRequest) (*VSchema, error) + Resize(context.Context, *ResizeKeyspaceRequest) (*KeyspaceResizeRequest, error) + CancelResize(context.Context, *CancelKeyspaceResizeRequest) error + ResizeStatus(context.Context, *KeyspaceResizeStatusRequest) (*KeyspaceResizeRequest, error) } type branchKeyspacesService struct { @@ -167,6 +211,31 @@ func (s *branchKeyspacesService) UpdateVSchema(ctx context.Context, updateReq *U return vschema, nil } +// Resize starts or queues a resize of a branch's keyspace. +func (s *branchKeyspacesService) Resize(ctx context.Context, resizeReq *ResizeKeyspaceRequest) (*KeyspaceResizeRequest, error) { + req, err := s.client.newRequest(http.MethodPut, databaseBranchKeyspaceResizesAPIPath(resizeReq.Organization, resizeReq.Database, resizeReq.Branch, resizeReq.Keyspace), resizeReq) + if err != nil { + return nil, errors.Wrap(err, "error creating http request") + } + + keyspaceResize := &KeyspaceResizeRequest{} + if err := s.client.do(ctx, req, keyspaceResize); err != nil { + return nil, err + } + + return keyspaceResize, nil +} + +// CancelResize cancels a queued resize of a branch's keyspace. +func (s *branchKeyspacesService) CancelResize(ctx context.Context, cancelReq *CancelKeyspaceResizeRequest) error { + req, err := s.client.newRequest(http.MethodDelete, databaseBranchKeyspaceResizesAPIPath(cancelReq.Organization, cancelReq.Database, cancelReq.Branch, cancelReq.Keyspace), nil) + if err != nil { + return errors.Wrap(err, "error creating http request") + } + + return s.client.do(ctx, req, nil) +} + func databaseBranchKeyspacesAPIPath(org, db, branch string) string { return fmt.Sprintf("%s/keyspaces", databaseBranchAPIPath(org, db, branch)) } @@ -174,3 +243,33 @@ func databaseBranchKeyspacesAPIPath(org, db, branch string) string { func databaseBranchKeyspaceAPIPath(org, db, branch, keyspace string) string { return fmt.Sprintf("%s/%s", databaseBranchKeyspacesAPIPath(org, db, branch), keyspace) } + +func databaseBranchKeyspaceResizesAPIPath(org, db, branch, keyspace string) string { + return fmt.Sprintf("%s/resizes", databaseBranchKeyspaceAPIPath(org, db, branch, keyspace)) +} + +type keyspaceResizesResponse struct { + Resizes []*KeyspaceResizeRequest `json:"data"` +} + +func (s *branchKeyspacesService) ResizeStatus(ctx context.Context, resizeReq *KeyspaceResizeStatusRequest) (*KeyspaceResizeRequest, error) { + req, err := s.client.newRequest(http.MethodGet, databaseBranchKeyspaceResizesAPIPath(resizeReq.Organization, resizeReq.Database, resizeReq.Branch, resizeReq.Keyspace), nil) + if err != nil { + return nil, errors.Wrap(err, "error creating http request") + } + + resizesResponse := &keyspaceResizesResponse{} + if err := s.client.do(ctx, req, resizesResponse); err != nil { + return nil, err + } + + // If there are no resizes, treat the same as a not found error + if len(resizesResponse.Resizes) == 0 { + return nil, &Error{ + msg: "Not Found", + Code: ErrNotFound, + } + } + + return resizesResponse.Resizes[0], nil +} diff --git a/planetscale/keyspaces_test.go b/planetscale/keyspaces_test.go index 7dd5bc0..4bf789a 100644 --- a/planetscale/keyspaces_test.go +++ b/planetscale/keyspaces_test.go @@ -164,3 +164,131 @@ func TestKeyspaces_UpdateVSchema(t *testing.T) { c.Assert(vSchema.Raw, qt.Equals, wantRaw) c.Assert(vSchema.HTML, qt.Equals, wantHTML) } + +func TestKeyspaces_Resize(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + out := `{"id":"thisisanid","type":"KeyspaceResizeRequest","state":"pending","started_at":"2024-06-25T18:03:09.459Z","completed_at":"2024-06-25T18:04:06.228Z","created_at":"2024-06-25T18:03:09.439Z","updated_at":"2024-06-25T18:04:06.238Z","actor":{"id":"actorid","type":"User","display_name":"Test User"},"cluster_rate_name":"PS_10","extra_replicas":1,"previous_cluster_rate_name":"PS_10","replicas":3,"previous_replicas":5}` + _, err := w.Write([]byte(out)) + c.Assert(err, qt.IsNil) + c.Assert(r.Method, qt.Equals, http.MethodPut) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + ctx := context.Background() + + size := ClusterSize("PS_10") + replicas := uint(3) + + krr, err := client.Keyspaces.Resize(ctx, &ResizeKeyspaceRequest{ + Organization: "foo", + Database: "bar", + Branch: "baz", + Keyspace: "qux", + ClusterSize: &size, + ExtraReplicas: &replicas, + }) + + wantID := "thisisanid" + + c.Assert(err, qt.IsNil) + c.Assert(krr.ID, qt.Equals, wantID) + c.Assert(krr.ExtraReplicas, qt.Equals, uint(1)) + c.Assert(krr.Replicas, qt.Equals, uint(3)) + c.Assert(krr.PreviousReplicas, qt.Equals, uint(5)) + c.Assert(krr.ClusterSize, qt.Equals, ClusterSize("PS_10")) + c.Assert(krr.PreviousClusterSize, qt.Equals, ClusterSize("PS_10")) +} + +func TestKeyspaces_CancelResize(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(204) + c.Assert(r.Method, qt.Equals, http.MethodDelete) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + ctx := context.Background() + + err = client.Keyspaces.CancelResize(ctx, &CancelKeyspaceResizeRequest{ + Organization: "foo", + Database: "bar", + Branch: "baz", + Keyspace: "qux", + }) + + c.Assert(err, qt.IsNil) +} + +func TestKeyspaces_ResizeStatus(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + out := `{"type":"list","current_page":1,"next_page":null,"next_page_url":null,"prev_page":null,"prev_page_url":null,"data":[{"id":"thisisanid","type":"KeyspaceResizeRequest","state":"completed","started_at":"2024-06-25T18:03:09.459Z","completed_at":"2024-06-25T18:04:06.228Z","created_at":"2024-06-25T18:03:09.439Z","updated_at":"2024-06-25T18:04:06.238Z","actor":{"id":"thisisanid","type":"User","display_name":"Test User"},"cluster_rate_name":"PS_10","extra_replicas":0,"previous_cluster_rate_name":"PS_10","replicas":2,"previous_replicas":5}]}` + _, err := w.Write([]byte(out)) + c.Assert(err, qt.IsNil) + c.Assert(r.Method, qt.Equals, http.MethodGet) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + ctx := context.Background() + + krr, err := client.Keyspaces.ResizeStatus(ctx, &KeyspaceResizeStatusRequest{ + Organization: "foo", + Database: "bar", + Branch: "baz", + Keyspace: "qux", + }) + + wantID := "thisisanid" + + c.Assert(err, qt.IsNil) + c.Assert(krr.ID, qt.Equals, wantID) + c.Assert(krr.ExtraReplicas, qt.Equals, uint(0)) + c.Assert(krr.Replicas, qt.Equals, uint(2)) + c.Assert(krr.PreviousReplicas, qt.Equals, uint(5)) + c.Assert(krr.ClusterSize, qt.Equals, ClusterSize("PS_10")) + c.Assert(krr.PreviousClusterSize, qt.Equals, ClusterSize("PS_10")) +} + +func TestKeyspaces_ResizeStatusEmpty(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + out := `{"type":"list","current_page":1,"next_page":null,"next_page_url":null,"prev_page":null,"prev_page_url":null,"data":[]}` + _, err := w.Write([]byte(out)) + c.Assert(err, qt.IsNil) + c.Assert(r.Method, qt.Equals, http.MethodGet) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + ctx := context.Background() + + krr, err := client.Keyspaces.ResizeStatus(ctx, &KeyspaceResizeStatusRequest{ + Organization: "foo", + Database: "bar", + Branch: "baz", + Keyspace: "qux", + }) + + wantError := &Error{ + msg: "Not Found", + Code: ErrNotFound, + } + + c.Assert(krr, qt.IsNil) + c.Assert(err.Error(), qt.Equals, wantError.Error()) +}