diff --git a/planetscale/deploy_requests.go b/planetscale/deploy_requests.go index 024cd9a..8a626ef 100644 --- a/planetscale/deploy_requests.go +++ b/planetscale/deploy_requests.go @@ -26,6 +26,7 @@ type DeployRequestsService interface { CreateReview(context.Context, *ReviewDeployRequestRequest) (*DeployRequestReview, error) Deploy(context.Context, *PerformDeployRequest) (*DeployRequest, error) Diff(ctx context.Context, diffReq *DiffRequest) ([]*Diff, error) + ForceCutover(context.Context, *ForceCutoverDeployRequestRequest) (*DeployRequest, error) Get(context.Context, *GetDeployRequestRequest) (*DeployRequest, error) List(context.Context, *ListDeployRequestsRequest) ([]*DeployRequest, error) GetDeployOperations(context.Context, *GetDeployOperationsRequest) ([]*DeployOperation, error) @@ -138,11 +139,12 @@ type Deployment struct { CutoverActor *Actor `json:"cutover_actor"` CancelledActor *Actor `json:"cancelled_actor"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - StartedAt *time.Time `json:"started_at"` - QueuedAt *time.Time `json:"queued_at"` - FinishedAt *time.Time `json:"finished_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + StartedAt *time.Time `json:"started_at"` + QueuedAt *time.Time `json:"queued_at"` + FinishedAt *time.Time `json:"finished_at"` + ForceCutoverRequestedAt *time.Time `json:"force_cutover_requested_at"` } // DeployRequest encapsulates the request to deploy a database branch's schema @@ -182,6 +184,12 @@ type ApplyDeployRequestRequest struct { Number uint64 `json:"-"` } +type ForceCutoverDeployRequestRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Number uint64 `json:"-"` +} + type AutoApplyDeployRequestRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -366,6 +374,22 @@ func (d *deployRequestsService) ApplyDeploy(ctx context.Context, applyReq *Apply return drr, nil } +// ForceCutover requests a force cutover for a deploy request stuck in the cutover phase. +func (d *deployRequestsService) ForceCutover(ctx context.Context, forceReq *ForceCutoverDeployRequestRequest) (*DeployRequest, error) { + path := deployRequestActionAPIPath(forceReq.Organization, forceReq.Database, forceReq.Number, "force-cutover") + req, err := d.client.newRequest(http.MethodPost, path, forceReq) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + + drr := &DeployRequest{} + if err := d.client.do(ctx, req, &drr); err != nil { + return nil, err + } + + return drr, nil +} + func (d *deployRequestsService) AutoApplyDeploy(ctx context.Context, autoApplyReq *AutoApplyDeployRequestRequest) (*DeployRequest, error) { reqBody := struct { Enable bool `json:"enable"` diff --git a/planetscale/deploy_requests_test.go b/planetscale/deploy_requests_test.go index 9630d17..16f6980 100644 --- a/planetscale/deploy_requests_test.go +++ b/planetscale/deploy_requests_test.go @@ -240,6 +240,50 @@ func TestDeployRequests_CancelDeploy(t *testing.T) { c.Assert(dr, qt.DeepEquals, want) } +func TestDeployRequests_ForceCutover(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, qt.Equals, http.MethodPost) + c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/test-organization/databases/test-database/deploy-requests/1337/force-cutover") + w.WriteHeader(200) + out := `{"id": "test-deploy-request-id", "branch": "development", "into_branch": "some-branch", "notes": "", "created_at": "2021-01-14T10:19:23.000Z", "updated_at": "2021-01-14T10:19:23.000Z", "closed_at": null, "deployment": { "state": "in_progress_cutover", "force_cutover_requested_at": "2021-01-14T10:19:23.000Z" }, "number": 1337}` + _, err := w.Write([]byte(out)) + c.Assert(err, qt.IsNil) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + ctx := context.Background() + + dr, err := client.DeployRequests.ForceCutover(ctx, &ForceCutoverDeployRequestRequest{ + Organization: "test-organization", + Database: "test-database", + Number: 1337, + }) + + testTime := time.Date(2021, time.January, 14, 10, 19, 23, 0, time.UTC) + + want := &DeployRequest{ + ID: "test-deploy-request-id", + Branch: "development", + Deployment: &Deployment{ + State: "in_progress_cutover", + ForceCutoverRequestedAt: &testTime, + }, + IntoBranch: "some-branch", + Number: 1337, + Notes: "", + CreatedAt: testTime, + UpdatedAt: testTime, + ClosedAt: nil, + } + + c.Assert(err, qt.IsNil) + c.Assert(dr, qt.DeepEquals, want) +} + func TestDeployRequests_Close(t *testing.T) { c := qt.New(t) diff --git a/planetscale/keyspaces.go b/planetscale/keyspaces.go index d5eaecc..1b78de6 100644 --- a/planetscale/keyspaces.go +++ b/planetscale/keyspaces.go @@ -70,6 +70,13 @@ type GetKeyspaceRequest struct { Full bool `json:"-"` } +type DeleteKeyspaceRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + Keyspace string `json:"-"` +} + type UpdateReadOnlyRegionsRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -184,6 +191,7 @@ type KeyspacesService interface { Create(context.Context, *CreateKeyspaceRequest) (*Keyspace, error) List(context.Context, *ListKeyspacesRequest) ([]*Keyspace, error) Get(context.Context, *GetKeyspaceRequest) (*Keyspace, error) + Delete(context.Context, *DeleteKeyspaceRequest) error UpdateReadOnlyRegions(context.Context, *UpdateReadOnlyRegionsRequest) ([]*ReadOnlyRegionKeyspace, error) VSchema(context.Context, *GetKeyspaceVSchemaRequest) (*VSchema, error) UpdateVSchema(context.Context, *UpdateKeyspaceVSchemaRequest) (*VSchema, error) @@ -270,6 +278,16 @@ func (s *keyspacesService) Create(ctx context.Context, createReq *CreateKeyspace return keyspace, nil } +// Delete deletes a keyspace from a branch. +func (s *keyspacesService) Delete(ctx context.Context, deleteReq *DeleteKeyspaceRequest) error { + req, err := s.client.newRequest(http.MethodDelete, keyspaceAPIPath(deleteReq.Organization, deleteReq.Database, deleteReq.Branch, deleteReq.Keyspace), nil) + if err != nil { + return fmt.Errorf("error creating http request: %w", err) + } + + return s.client.do(ctx, req, nil) +} + // VSchema returns the VSchema for a keyspace in a branch func (s *keyspacesService) VSchema(ctx context.Context, getReq *GetKeyspaceVSchemaRequest) (*VSchema, error) { pathStr := path.Join(keyspaceAPIPath(getReq.Organization, getReq.Database, getReq.Branch, getReq.Keyspace), "vschema") diff --git a/planetscale/keyspaces_test.go b/planetscale/keyspaces_test.go index 3325c43..792ec39 100644 --- a/planetscale/keyspaces_test.go +++ b/planetscale/keyspaces_test.go @@ -195,6 +195,27 @@ func TestKeyspaces_Create(t *testing.T) { c.Assert(keyspace.Shards, qt.Equals, 2) } +func TestKeyspaces_Delete(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, qt.Equals, http.MethodDelete) + c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/foo/databases/bar/branches/baz/keyspaces/qux") + w.WriteHeader(http.StatusNoContent) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + err = client.Keyspaces.Delete(context.Background(), &DeleteKeyspaceRequest{ + Organization: "foo", + Database: "bar", + Branch: "baz", + Keyspace: "qux", + }) + c.Assert(err, qt.IsNil) +} + func TestKeyspaces_VSchema(t *testing.T) { c := qt.New(t) diff --git a/planetscale/vtctld_general.go b/planetscale/vtctld_general.go index 28026dc..afd6f0d 100644 --- a/planetscale/vtctld_general.go +++ b/planetscale/vtctld_general.go @@ -16,6 +16,8 @@ type VtctldService interface { ListWorkflows(context.Context, *VtctldListWorkflowsRequest) (json.RawMessage, error) ListKeyspaces(context.Context, *VtctldListKeyspacesRequest) (json.RawMessage, error) GetRoutingRules(context.Context, *VtctldGetRoutingRulesRequest) (json.RawMessage, error) + GetKeyspaceRoutingRules(context.Context, *VtctldGetKeyspaceRoutingRulesRequest) (json.RawMessage, error) + ApplyKeyspaceRoutingRules(context.Context, *VtctldApplyKeyspaceRoutingRulesRequest) (json.RawMessage, error) GetShard(context.Context, *VtctldGetShardRequest) (json.RawMessage, error) SetShardTabletControl(context.Context, *VtctldSetShardTabletControlRequest) (json.RawMessage, error) RefreshStateByShard(context.Context, *VtctldRefreshStateByShardRequest) (json.RawMessage, error) @@ -52,6 +54,31 @@ type VtctldGetRoutingRulesRequest struct { Branch string `json:"-"` } +type VtctldKeyspaceRoutingRule struct { + FromKeyspace string `json:"from_keyspace"` + ToKeyspace string `json:"to_keyspace"` +} + +type VtctldKeyspaceRoutingRules struct { + Rules []VtctldKeyspaceRoutingRule `json:"rules"` +} + +type VtctldGetKeyspaceRoutingRulesRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` +} + +type VtctldApplyKeyspaceRoutingRulesRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + + Rules []VtctldKeyspaceRoutingRule `json:"rules"` + SkipRebuild bool `json:"skip_rebuild"` + RebuildCells []string `json:"rebuild_cells,omitempty"` +} + // VtctldGetShardRequest is a request for reading a shard record from the // cluster via vtctld. type VtctldGetShardRequest struct { @@ -197,6 +224,10 @@ func vtctldRoutingRulesAPIPath(org, db, branch string) string { return path.Join(databaseBranchAPIPath(org, db, branch), "vtctld", "routing-rules") } +func vtctldKeyspaceRoutingRulesAPIPath(org, db, branch string) string { + return path.Join(databaseBranchAPIPath(org, db, branch), "vtctld", "keyspace-routing-rules") +} + func vtctldShardAPIPath(org, db, branch string) string { return path.Join(databaseBranchAPIPath(org, db, branch), "vtctld", "shard") } @@ -260,6 +291,32 @@ func (s *vtctldService) GetRoutingRules(ctx context.Context, req *VtctldGetRouti return resp.Data, nil } +func (s *vtctldService) GetKeyspaceRoutingRules(ctx context.Context, req *VtctldGetKeyspaceRoutingRulesRequest) (json.RawMessage, error) { + p := vtctldKeyspaceRoutingRulesAPIPath(req.Organization, req.Database, req.Branch) + httpReq, err := s.client.newRequest(http.MethodGet, p, nil) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + resp := &vtctldDataResponse{} + if err := s.client.do(ctx, httpReq, resp); err != nil { + return nil, err + } + return resp.Data, nil +} + +func (s *vtctldService) ApplyKeyspaceRoutingRules(ctx context.Context, req *VtctldApplyKeyspaceRoutingRulesRequest) (json.RawMessage, error) { + p := vtctldKeyspaceRoutingRulesAPIPath(req.Organization, req.Database, req.Branch) + httpReq, err := s.client.newRequest(http.MethodPut, p, req) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + resp := &vtctldDataResponse{} + if err := s.client.do(ctx, httpReq, resp); err != nil { + return nil, err + } + return resp.Data, nil +} + func (s *vtctldService) GetShard(ctx context.Context, req *VtctldGetShardRequest) (json.RawMessage, error) { p := vtctldShardAPIPath(req.Organization, req.Database, req.Branch) v := url.Values{} diff --git a/planetscale/vtctld_general_test.go b/planetscale/vtctld_general_test.go index 1a720e3..22540ec 100644 --- a/planetscale/vtctld_general_test.go +++ b/planetscale/vtctld_general_test.go @@ -146,6 +146,72 @@ func TestVtctld_GetRoutingRules(t *testing.T) { c.Assert(string(data), qt.Equals, `{"rules":[]}`) } +func TestVtctld_GetKeyspaceRoutingRules(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, qt.Equals, http.MethodGet) + c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/branches/my-branch/vtctld/keyspace-routing-rules") + + w.WriteHeader(200) + _, err := w.Write([]byte(`{"data":{"rules":[]}}`)) + c.Assert(err, qt.IsNil) + })) + defer ts.Close() + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + data, err := client.Vtctld.GetKeyspaceRoutingRules(context.Background(), &VtctldGetKeyspaceRoutingRulesRequest{ + Organization: "my-org", + Database: "my-db", + Branch: "my-branch", + }) + c.Assert(err, qt.IsNil) + c.Assert(string(data), qt.Equals, `{"rules":[]}`) +} + +func TestVtctld_ApplyKeyspaceRoutingRules(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, qt.Equals, http.MethodPut) + c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/branches/my-branch/vtctld/keyspace-routing-rules") + + var body VtctldApplyKeyspaceRoutingRulesRequest + err := json.NewDecoder(r.Body).Decode(&body) + c.Assert(err, qt.IsNil) + c.Assert(body.Rules, qt.DeepEquals, []VtctldKeyspaceRoutingRule{{ + FromKeyspace: "source", + ToKeyspace: "target", + }}) + c.Assert(body.SkipRebuild, qt.IsTrue) + c.Assert(body.RebuildCells, qt.DeepEquals, []string{"zone1"}) + + w.WriteHeader(200) + _, err = w.Write([]byte(`{"data":{"rules":[{"from_keyspace":"source","to_keyspace":"target"}]}}`)) + c.Assert(err, qt.IsNil) + })) + defer ts.Close() + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + data, err := client.Vtctld.ApplyKeyspaceRoutingRules(context.Background(), &VtctldApplyKeyspaceRoutingRulesRequest{ + Organization: "my-org", + Database: "my-db", + Branch: "my-branch", + Rules: []VtctldKeyspaceRoutingRule{{ + FromKeyspace: "source", + ToKeyspace: "target", + }}, + SkipRebuild: true, + RebuildCells: []string{"zone1"}, + }) + c.Assert(err, qt.IsNil) + c.Assert(string(data), qt.Equals, `{"rules":[{"from_keyspace":"source","to_keyspace":"target"}]}`) +} + func TestVtctld_GetShard(t *testing.T) { c := qt.New(t)