Skip to content

Commit 9bb50b3

Browse files
authored
Merge pull request #241 from planetscale/workflows-endpoints
Add initial workflows endpoint
2 parents 487c942 + 41be388 commit 9bb50b3

File tree

3 files changed

+261
-0
lines changed

3 files changed

+261
-0
lines changed

planetscale/client.go

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type Client struct {
5959
DeployRequests DeployRequestsService
6060
ServiceTokens ServiceTokenService
6161
Keyspaces KeyspacesService
62+
Workflows WorkflowsService
6263
}
6364

6465
// ListOptions are options for listing responses.
@@ -275,6 +276,7 @@ func NewClient(opts ...ClientOption) (*Client, error) {
275276
c.DeployRequests = &deployRequestsService{client: c}
276277
c.ServiceTokens = &serviceTokenService{client: c}
277278
c.Keyspaces = &keyspacesService{client: c}
279+
c.Workflows = &workflowsService{client: c}
278280

279281
return c, nil
280282
}

planetscale/workflows.go

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package planetscale
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
"github.com/pkg/errors"
10+
)
11+
12+
type Workflow struct {
13+
ID string `json:"id"`
14+
Name string `json:"name"`
15+
Number int `json:"number"`
16+
State string `json:"state"`
17+
CreatedAt time.Time `json:"created_at"`
18+
UpdatedAt time.Time `json:"updated_at"`
19+
StartedAt *time.Time `json:"started_at"`
20+
CompletedAt *time.Time `json:"completed_at"`
21+
CancelledAt *time.Time `json:"cancelled_at"`
22+
ReversedAt *time.Time `json:"reversed_at"`
23+
RetriedAt *time.Time `json:"retried_at"`
24+
DataCopycCompletedAt *time.Time `json:"data_copy_completed_at"`
25+
CutoverAt *time.Time `json:"cutover_at"`
26+
ReplicasSwitched bool `json:"replicas_switched"`
27+
PrimariesSwitched bool `json:"primaries_switched"`
28+
SwitchReplicasAt *time.Time `json:"switch_replicas_at"`
29+
SwitchPrimariesAt *time.Time `json:"switch_primaries_at"`
30+
VerifyDataAt *time.Time `json:"verify_data_at"`
31+
32+
Branch DatabaseBranch `json:"branch"`
33+
SourceKeyspace Keyspace `json:"source_keyspace"`
34+
TargetKeyspace Keyspace `json:"target_keyspace"`
35+
36+
Actor Actor `json:"actor"`
37+
VerifyDataBy *Actor `json:"verify_data_by"`
38+
ReversedBy *Actor `json:"reversed_by"`
39+
SwitchReplicasBy *Actor `json:"switch_replicas_by"`
40+
SwitchPrimariesBy *Actor `json:"switch_primaries_by"`
41+
CancelledBy *Actor `json:"cancelled_by"`
42+
CompletedBy *Actor `json:"completed_by"`
43+
RetriedBy *Actor `json:"retried_by"`
44+
CutoverBy *Actor `json:"cutover_by"`
45+
ReversedCutoverBy *Actor `json:"reversed_cutover_by"`
46+
47+
Streams *[]WorkflowStream `json:"streams"`
48+
Tables *[]WorkflowTable `json:"tables"`
49+
VDiff *WorkflowVDiff `json:"vdiff"`
50+
}
51+
52+
type WorkflowStream struct {
53+
PublicID string `json:"id"`
54+
State string `json:"state"`
55+
CreatedAt time.Time `json:"created_at"`
56+
UpdatedAt time.Time `json:"updated_at"`
57+
TargetShard string `json:"target_shard"`
58+
SourceShard string `json:"source_shard"`
59+
Position string `json:"position"`
60+
StopPosition string `json:"stop_position"`
61+
RowsCopied int64 `json:"rows_copied"`
62+
ComponentThrottled *string `json:"component_throttled"`
63+
ComponentThrottledAt *time.Time `json:"component_throttled_at"`
64+
PrimaryServing bool `json:"primary_serving"`
65+
Info string `json:"info"`
66+
Logs []WorkflowStreamLog `json:"logs"`
67+
}
68+
69+
type WorkflowStreamLog struct {
70+
PublicID string `json:"id"`
71+
State string `json:"state"`
72+
CreatedAt time.Time `json:"created_at"`
73+
UpdatedAt time.Time `json:"updated_at"`
74+
Message string `json:"message"`
75+
LogType string `json:"log_type"`
76+
}
77+
78+
type WorkflowTable struct {
79+
PublicID string `json:"id"`
80+
Name string `json:"name"`
81+
CreatedAt time.Time `json:"created_at"`
82+
UpdatedAt time.Time `json:"updated_at"`
83+
RowsCopied int64 `json:"rows_copied"`
84+
RowsTotal int64 `json:"rows_total"`
85+
RowsPercentage int `json:"rows_percentage"`
86+
}
87+
88+
type WorkflowVDiff struct {
89+
PublicID string `json:"id"`
90+
State string `json:"state"`
91+
CreatedAt time.Time `json:"created_at"`
92+
UpdatedAt time.Time `json:"updated_at"`
93+
StartedAt *time.Time `json:"started_at"`
94+
CompletedAt *time.Time `json:"completed_at"`
95+
HasMismatch bool `json:"has_mismatch"`
96+
ProgressPercentage int `json:"progress_percentage"`
97+
EtaSeconds int64 `json:"eta_seconds"`
98+
TableReports []WorkflowVDiffTableReport `json:"table_reports"`
99+
}
100+
101+
type WorkflowVDiffTableReport struct {
102+
PublicID string `json:"id"`
103+
TableName string `json:"table_name"`
104+
Shard string `json:"shard"`
105+
MismatchedRowsCount int64 `json:"mismatched_rows_count"`
106+
ExtraSourceRowsCount int64 `json:"extra_source_rows_count"`
107+
ExtraTargetRowsCount int64 `json:"extra_target_rows_count"`
108+
ExtraSourceRows []interface{} `json:"extra_source_rows"`
109+
ExtraTargetRows []interface{} `json:"extra_target_rows"`
110+
MismatchedRows []interface{} `json:"mismatched_rows"`
111+
SampleExtraSourceRowsQuery string `json:"sample_extra_source_rows_query"`
112+
SampleExtraTargetRowsQuery string `json:"sample_extra_target_rows_query"`
113+
SampleMismatchedRowsQuery string `json:"sample_mismatched_rows_query"`
114+
CreatedAt time.Time `json:"created_at"`
115+
UpdatedAt time.Time `json:"updated_at"`
116+
}
117+
118+
type ListWorkflowsRequest struct {
119+
Organization string `json:"-"`
120+
Database string `json:"-"`
121+
}
122+
123+
type GetWorkflowRequest struct {
124+
Organization string `json:"-"`
125+
Database string `json:"-"`
126+
WorkflowNumber int `json:"-"`
127+
}
128+
129+
// WorkflowsService is an interface for interacting with the workflow endpoints of the PlanetScale API
130+
type WorkflowsService interface {
131+
List(context.Context, *ListWorkflowsRequest) ([]*Workflow, error)
132+
Get(context.Context, *GetWorkflowRequest) (*Workflow, error)
133+
}
134+
135+
type workflowsService struct {
136+
client *Client
137+
}
138+
139+
var _ WorkflowsService = &workflowsService{}
140+
141+
func NewWorkflowsService(client *Client) *workflowsService {
142+
return &workflowsService{client}
143+
}
144+
145+
type workflowsResponse struct {
146+
Workflows []*Workflow `json:"data"`
147+
}
148+
149+
func (ws *workflowsService) List(ctx context.Context, listReq *ListWorkflowsRequest) ([]*Workflow, error) {
150+
req, err := ws.client.newRequest(http.MethodGet, workflowsAPIPath(listReq.Organization, listReq.Database), nil)
151+
if err != nil {
152+
return nil, errors.Wrap(err, "error creating http request")
153+
}
154+
155+
workflows := &workflowsResponse{}
156+
157+
if err := ws.client.do(ctx, req, workflows); err != nil {
158+
return nil, err
159+
}
160+
161+
return workflows.Workflows, nil
162+
}
163+
164+
func (ws *workflowsService) Get(ctx context.Context, getReq *GetWorkflowRequest) (*Workflow, error) {
165+
req, err := ws.client.newRequest(http.MethodGet, workflowAPIPath(getReq.Organization, getReq.Database, getReq.WorkflowNumber), nil)
166+
if err != nil {
167+
return nil, errors.Wrap(err, "error creating http request")
168+
}
169+
170+
workflow := &Workflow{}
171+
172+
if err := ws.client.do(ctx, req, workflow); err != nil {
173+
return nil, err
174+
}
175+
176+
return workflow, nil
177+
}
178+
179+
func workflowsAPIPath(org, db string) string {
180+
return fmt.Sprintf("%s/%s/workflows", databasesAPIPath(org), db)
181+
}
182+
183+
func workflowAPIPath(org, db string, number int) string {
184+
return fmt.Sprintf("%s/%d", workflowsAPIPath(org, db), number)
185+
}

planetscale/workflows_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package planetscale
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
qt "github.com/frankban/quicktest"
10+
)
11+
12+
func TestWorkflows_List(t *testing.T) {
13+
c := qt.New(t)
14+
15+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16+
w.WriteHeader(200)
17+
out := `{"type":"list","current_page":1,"next_page":null,"next_page_url":null,"prev_page":null,"prev_page_url":null,"data":[{"id":"thisisanid","type":"Workflow","name":"shard-table","number":1,"state":"completed","created_at":"2025-03-18T16:22:15.293Z","updated_at":"2025-03-18T16:22:15.293Z","started_at":null,"completed_at":null,"cancelled_at":null,"reversed_at":null,"retried_at":null,"data_copy_completed_at":null,"cutover_at":null,"replicas_switched":false,"primaries_switched":false,"switch_replicas_at":null,"switch_primaries_at":null,"verify_data_at":null,"workflow_type":"move_tables","workflow_subtype":null,"may_retry":false,"verified_data_stale":false,"branch":{"id":"ddi0rgmj636p","type":"Branch","name":"main","created_at":"2025-03-18T16:22:14.872Z","deleted_at":null,"updated_at":"2025-03-18T16:22:15.169Z"},"source_keyspace":{"id":"ki6zinvzi973","type":"BranchKeyspace","name":"source-keyspace","created_at":"2025-03-18T16:22:15.016Z","deleted_at":null,"updated_at":"2025-03-18T16:22:15.128Z"},"target_keyspace":{"id":"n4bqtq0akviv","type":"BranchKeyspace","name":"target-keyspace","created_at":"2025-03-18T16:22:15.240Z","deleted_at":null,"updated_at":"2025-03-18T16:22:15.240Z"},"actor":{"id":"lcuyaidzbteb","type":"User","display_name":"[email protected]","avatar_url":"https://app.planetscale.com/gravatar-fallback.png"},"verify_data_by":null,"reversed_by":null,"switch_replicas_by":null,"switch_primaries_by":null,"cancelled_by":null,"completed_by":null,"retried_by":null,"cutover_by":null,"reversed_cutover_by":null}]}`
18+
_, err := w.Write([]byte(out))
19+
c.Assert(err, qt.IsNil)
20+
}))
21+
22+
client, err := NewClient(WithBaseURL(ts.URL))
23+
c.Assert(err, qt.IsNil)
24+
25+
ctx := context.Background()
26+
27+
workflows, err := client.Workflows.List(ctx, &ListWorkflowsRequest{
28+
Organization: "foo",
29+
Database: "bar",
30+
})
31+
32+
wantID := "thisisanid"
33+
34+
c.Assert(err, qt.IsNil)
35+
c.Assert(len(workflows), qt.Equals, 1)
36+
c.Assert(workflows[0].ID, qt.Equals, wantID)
37+
c.Assert(workflows[0].Name, qt.Equals, "shard-table")
38+
c.Assert(workflows[0].Number, qt.Equals, 1)
39+
}
40+
41+
func TestWorkflows_Get(t *testing.T) {
42+
c := qt.New(t)
43+
44+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45+
w.WriteHeader(200)
46+
out := `{"id":"thisisanid","type":"Workflow","name":"shard-table","number":1,"state":"pending","created_at":"2025-03-18T17:21:55.546Z","updated_at":"2025-03-18T17:21:55.618Z","started_at":null,"completed_at":null,"cancelled_at":null,"reversed_at":null,"retried_at":null,"data_copy_completed_at":null,"cutover_at":null,"replicas_switched":false,"primaries_switched":false,"switch_replicas_at":null,"switch_primaries_at":null,"verify_data_at":null,"workflow_type":"move_tables","workflow_subtype":null,"may_retry":false,"verified_data_stale":false,"branch":{"id":"hjcq437nimp2","type":"Branch","name":"branch","created_at":"2025-03-18T17:21:55.194Z","updated_at":"2025-03-18T17:21:55.434Z","restore_checklist_completed_at":null,"schema_last_updated_at":"2025-03-18T17:21:55.360Z","mysql_address":"us-east.connect.psdb.cloud","mysql_provider_address":"aws.connect.psdb.cloud","schema_ready":true,"state":"ready","vtgate_size":"vg.c1.nano","vtgate_count":1,"cluster_rate_name":"PS_10","mysql_edge_address":"aws.connect.psdb.cloud","ready":true,"metal":false,"production":true,"safe_migrations":true,"sharded":true,"shard_count":4,"stale_schema":false,"index_usage_enabled":true,"actor":{"id":"55cloymikacf","type":"User","display_name":"[email protected]","avatar_url":"https://app.planetscale.com/gravatar-fallback.png"},"restored_from_branch":null,"private_connectivity":false,"private_edge_connectivity":false,"html_url":"http://app.pscaledev.com:3001/organization1/weathered-bush-4453/main","has_replicas":true,"has_read_only_replicas":false,"url":"http://api.pscaledev.com:3000/v1/organizations/organization1/databases/weathered-bush-4453/branches/main","region":{"id":"ps-region-id","type":"Region","provider":"AWS","enabled":true,"public_ip_addresses":[],"display_name":"AWS us-east-1","location":"Ashburn, Virginia","slug":"us-east","current_default":true},"parent_branch":null},"source_keyspace":{"id":"w7l4fekda4xg","type":"BranchKeyspace","name":"source-keyspace","shards":1,"sharded":false,"replicas":2,"extra_replicas":0,"created_at":"2025-03-18T17:21:55.284Z","updated_at":"2025-03-18T17:21:55.390Z","cluster_rate_name":"PS_10","cluster_rate_display_name":"PS-10","resizing":false,"ready":true,"metal":false,"vector_pool_allocation":null,"resize_pending":false},"target_keyspace":{"id":"65qnxzwehl6f","type":"BranchKeyspace","name":"target-keyspace","shards":4,"sharded":true,"replicas":2,"extra_replicas":0,"created_at":"2025-03-18T17:21:55.499Z","updated_at":"2025-03-18T17:21:55.499Z","cluster_rate_name":"PS_10","cluster_rate_display_name":"PS-10","resizing":false,"ready":true,"metal":false,"vector_pool_allocation":null,"resize_pending":false},"actor":{"id":"55cloymikacf","type":"User","display_name":"[email protected]","avatar_url":"https://app.planetscale.com/gravatar-fallback.png"},"verify_data_by":null,"reversed_by":null,"switch_replicas_by":null,"switch_primaries_by":null,"cancelled_by":null,"completed_by":null,"retried_by":null,"cutover_by":null,"reversed_cutover_by":null,"streams":[{"id":"z7orf7caq72o","type":"WorkflowStream","state":"copying","created_at":"2025-03-18T17:21:55.598Z","updated_at":"2025-03-18T17:21:55.598Z","vitess_id":1,"target_shard":"-80","source_shard":"-80","target_tablet_uid":"target-uid","target_tablet_cell":"target-cell","position":"position","stop_position":"stop-position","rows_copied":10,"component_throttled":null,"component_throttled_at":null,"primary_serving":false,"info":"important info"}],"tables":[{"id":"5lje0cf2dvdi","type":"WorkflowTable","name":"cool-snowflake-2076","created_at":"2025-03-18T17:21:55.584Z","updated_at":"2025-03-18T17:21:55.584Z","rows_copied":10,"rows_total":100,"rows_percentage":10}],"vdiff":{"id":"n86yn5nr26zc","type":"WorkflowVDiff","state":"pending","created_at":"2025-03-18T17:21:55.614Z","updated_at":"2025-03-18T17:21:55.614Z","started_at":null,"completed_at":null,"has_mismatch":false,"progress_percentage":null,"eta_seconds":null,"table_reports":[{"id":"rw5saem6ltq0","type":"WorkflowVDiffTableReport","table_name":"users","shard":"-","mismatched_rows_count":0,"extra_source_rows_count":0,"extra_target_rows_count":0,"extra_source_rows":[],"extra_target_rows":[],"mismatched_rows":[],"sample_extra_source_rows_query":null,"sample_extra_target_rows_query":null,"sample_mismatched_rows_query":null,"created_at":"2025-03-18T17:21:55.624Z","updated_at":"2025-03-18T17:21:55.624Z"}]}}`
47+
_, err := w.Write([]byte(out))
48+
c.Assert(err, qt.IsNil)
49+
}))
50+
51+
client, err := NewClient(WithBaseURL(ts.URL))
52+
c.Assert(err, qt.IsNil)
53+
54+
ctx := context.Background()
55+
56+
workflow, err := client.Workflows.Get(ctx, &GetWorkflowRequest{
57+
Organization: "foo",
58+
Database: "bar",
59+
WorkflowNumber: 1,
60+
})
61+
62+
wantID := "thisisanid"
63+
64+
c.Assert(err, qt.IsNil)
65+
c.Assert(workflow.ID, qt.Equals, wantID)
66+
c.Assert(workflow.Name, qt.Equals, "shard-table")
67+
c.Assert(workflow.Number, qt.Equals, 1)
68+
c.Assert(workflow.SourceKeyspace.Name, qt.Equals, "source-keyspace")
69+
c.Assert(workflow.TargetKeyspace.Name, qt.Equals, "target-keyspace")
70+
c.Assert(workflow.Branch.Name, qt.Equals, "branch")
71+
c.Assert(workflow.VDiff.State, qt.Equals, "pending")
72+
c.Assert(len(*workflow.Streams), qt.Equals, 1)
73+
c.Assert(len(*workflow.Tables), qt.Equals, 1)
74+
}

0 commit comments

Comments
 (0)