Skip to content

Commit df89d54

Browse files
authored
Merge pull request #1222 from planetscale/tc-budget-show
Support showing Traffic Control budgets
2 parents b22891a + 4dbf574 commit df89d54

7 files changed

Lines changed: 289 additions & 1 deletion

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/mattn/go-shellwords v1.0.12
2626
github.com/mitchellh/go-homedir v1.1.0
2727
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
28-
github.com/planetscale/planetscale-go v0.155.0
28+
github.com/planetscale/planetscale-go v0.156.0
2929
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4
3030
github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7
3131
github.com/spf13/cobra v1.10.2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e h1:MZ8D+Z3m2v
184184
github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e/go.mod h1:hwAsSPQdvPa3WcfKfzTXxtEq/HlqwLjQasfO6QbGo4Q=
185185
github.com/planetscale/planetscale-go v0.155.0 h1:KYFRWFn9d5BeZc++4DF0wS+mlRQ4efrAy+6Zw/1kzXs=
186186
github.com/planetscale/planetscale-go v0.155.0/go.mod h1:paQCI5SgquuoewvMQM7R+r1XJO868bdP6/ihGidYRM0=
187+
github.com/planetscale/planetscale-go v0.156.0 h1:VThRagwFmthnPZ/A0W8N+bHHQ8+PdqdcLBHunr7p/jY=
188+
github.com/planetscale/planetscale-go v0.156.0/go.mod h1:paQCI5SgquuoewvMQM7R+r1XJO868bdP6/ihGidYRM0=
187189
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 h1:Xv5pj20Rhfty1Tv0OVcidg4ez4PvGrpKvb6rvUwQgDs=
188190
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4/go.mod h1:M52h5IWxAcbdQ1hSZrLAGQC4ZXslxEsK/Wh9nu3wdWs=
189191
github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 h1:aRd6vdE1fyuSI4RVj7oCr8lFmgqXvpnPUmN85VbZCp8=

internal/cmd/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/planetscale/cli/internal/cmd/mcp"
3030
"github.com/planetscale/cli/internal/cmd/role"
3131
"github.com/planetscale/cli/internal/cmd/size"
32+
"github.com/planetscale/cli/internal/cmd/trafficcontrol"
3233
"github.com/planetscale/cli/internal/cmd/workflow"
3334

3435
"github.com/fatih/color"
@@ -320,6 +321,10 @@ func runCmd(ctx context.Context, ver, commit, buildDate string, format *printer.
320321
roleCmd.GroupID = "postgres"
321322
rootCmd.AddCommand(roleCmd)
322323

324+
trafficCmd := trafficcontrol.TrafficCmd(ch)
325+
trafficCmd.GroupID = "postgres"
326+
rootCmd.AddCommand(trafficCmd)
327+
323328
annotateRequiredFlags(rootCmd)
324329

325330
return rootCmd.ExecuteContext(ctx)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package trafficcontrol
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/planetscale/cli/internal/cmdutil"
7+
"github.com/planetscale/cli/internal/printer"
8+
ps "github.com/planetscale/planetscale-go/planetscale"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func BudgetShowCmd(ch *cmdutil.Helper) *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "show <database> <branch> <budget-id>",
15+
Short: "Show a traffic budget",
16+
Args: cmdutil.RequiredArgs("database", "branch", "budget-id"),
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
ctx := cmd.Context()
19+
database := args[0]
20+
branch := args[1]
21+
budgetID := args[2]
22+
23+
client, err := ch.Client()
24+
if err != nil {
25+
return err
26+
}
27+
28+
end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching traffic budget %s", printer.BoldBlue(budgetID)))
29+
defer end()
30+
31+
budget, err := client.TrafficBudgets.Get(ctx, &ps.GetTrafficBudgetRequest{
32+
Organization: ch.Config.Organization,
33+
Database: database,
34+
Branch: branch,
35+
BudgetID: budgetID,
36+
})
37+
if err != nil {
38+
switch cmdutil.ErrCode(err) {
39+
case ps.ErrNotFound:
40+
return fmt.Errorf("traffic budget %s does not exist in %s/%s (organization: %s)",
41+
printer.BoldBlue(budgetID), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization))
42+
default:
43+
return cmdutil.HandleError(err)
44+
}
45+
}
46+
47+
end()
48+
return ch.Printer.PrintResource(toTrafficBudget(budget))
49+
},
50+
}
51+
52+
return cmd
53+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package trafficcontrol
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
"time"
8+
9+
qt "github.com/frankban/quicktest"
10+
"github.com/planetscale/cli/internal/cmdutil"
11+
"github.com/planetscale/cli/internal/config"
12+
"github.com/planetscale/cli/internal/mock"
13+
"github.com/planetscale/cli/internal/printer"
14+
ps "github.com/planetscale/planetscale-go/planetscale"
15+
)
16+
17+
func TestBudgetShowCmd(t *testing.T) {
18+
c := qt.New(t)
19+
20+
var buf bytes.Buffer
21+
format := printer.JSON
22+
p := printer.NewPrinter(&format)
23+
p.SetResourceOutput(&buf)
24+
25+
org := "planetscale"
26+
db := "database"
27+
branch := "main"
28+
budgetID := "qok87ki4xlau"
29+
cap := 80
30+
31+
budget := &ps.TrafficBudget{
32+
ID: budgetID,
33+
Name: "CPU Limiter",
34+
Mode: "enforce",
35+
Capacity: &cap,
36+
Rules: []ps.TrafficRule{{ID: "rule-1", Kind: "fingerprint"}},
37+
CreatedAt: time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC),
38+
UpdatedAt: time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC),
39+
}
40+
41+
svc := &mock.TrafficBudgetsService{
42+
GetFn: func(ctx context.Context, req *ps.GetTrafficBudgetRequest) (*ps.TrafficBudget, error) {
43+
c.Assert(req.Organization, qt.Equals, org)
44+
c.Assert(req.Database, qt.Equals, db)
45+
c.Assert(req.Branch, qt.Equals, branch)
46+
c.Assert(req.BudgetID, qt.Equals, budgetID)
47+
return budget, nil
48+
},
49+
}
50+
51+
ch := &cmdutil.Helper{
52+
Printer: p,
53+
Config: &config.Config{Organization: org},
54+
Client: func() (*ps.Client, error) {
55+
return &ps.Client{TrafficBudgets: svc}, nil
56+
},
57+
}
58+
59+
cmd := BudgetShowCmd(ch)
60+
cmd.SetArgs([]string{db, branch, budgetID})
61+
err := cmd.Execute()
62+
63+
c.Assert(err, qt.IsNil)
64+
c.Assert(svc.GetFnInvoked, qt.IsTrue)
65+
66+
res := &TrafficBudget{orig: budget}
67+
c.Assert(buf.String(), qt.JSONEquals, res)
68+
}
69+
70+
func TestBudgetShowCmd_NotFound(t *testing.T) {
71+
c := qt.New(t)
72+
73+
var buf bytes.Buffer
74+
format := printer.JSON
75+
p := printer.NewPrinter(&format)
76+
p.SetResourceOutput(&buf)
77+
78+
org := "planetscale"
79+
db := "database"
80+
branch := "main"
81+
budgetID := "qok87ki4xlau"
82+
83+
svc := &mock.TrafficBudgetsService{
84+
GetFn: func(ctx context.Context, req *ps.GetTrafficBudgetRequest) (*ps.TrafficBudget, error) {
85+
return nil, &ps.Error{Code: ps.ErrNotFound}
86+
},
87+
}
88+
89+
ch := &cmdutil.Helper{
90+
Printer: p,
91+
Config: &config.Config{Organization: org},
92+
Client: func() (*ps.Client, error) {
93+
return &ps.Client{TrafficBudgets: svc}, nil
94+
},
95+
}
96+
97+
cmd := BudgetShowCmd(ch)
98+
cmd.SetArgs([]string{db, branch, budgetID})
99+
err := cmd.Execute()
100+
101+
c.Assert(err, qt.IsNotNil)
102+
c.Assert(err.Error(), qt.Contains, "does not exist")
103+
c.Assert(svc.GetFnInvoked, qt.IsTrue)
104+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package trafficcontrol
2+
3+
import (
4+
"encoding/json"
5+
"strconv"
6+
7+
"github.com/planetscale/cli/internal/cmdutil"
8+
"github.com/planetscale/cli/internal/printer"
9+
ps "github.com/planetscale/planetscale-go/planetscale"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func TrafficCmd(ch *cmdutil.Helper) *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "traffic-control <command>",
16+
Short: "Manage Database Traffic Control™ for a Postgres database branch",
17+
Long: "Manage Database Traffic Control™ budgets and rules for a Postgres database branch.\n\n" +
18+
"This command is only supported for Postgres databases.",
19+
PersistentPreRunE: cmdutil.CheckAuthentication(ch.Config),
20+
}
21+
22+
cmd.PersistentFlags().StringVar(&ch.Config.Organization, "org", ch.Config.Organization, "The organization for the current user")
23+
cmd.MarkPersistentFlagRequired("org") // nolint:errcheck
24+
25+
budgetCmd := &cobra.Command{
26+
Use: "budget <command>",
27+
Short: "Manage traffic budgets",
28+
}
29+
budgetCmd.AddCommand(
30+
BudgetShowCmd(ch),
31+
)
32+
33+
cmd.AddCommand(budgetCmd)
34+
return cmd
35+
}
36+
37+
type TrafficBudget struct {
38+
ID string `header:"id" json:"id"`
39+
Name string `header:"name" json:"name"`
40+
Mode string `header:"mode" json:"mode"`
41+
Capacity string `header:"capacity" json:"capacity"`
42+
Rate string `header:"rate" json:"rate"`
43+
Burst string `header:"burst" json:"burst"`
44+
Concurrency string `header:"concurrency" json:"concurrency"`
45+
CreatedAt int64 `header:"created_at,timestamp(ms|utc|human)" json:"created_at"`
46+
UpdatedAt int64 `header:"updated_at,timestamp(ms|utc|human)" json:"updated_at"`
47+
48+
orig *ps.TrafficBudget
49+
}
50+
51+
func (b *TrafficBudget) MarshalJSON() ([]byte, error) {
52+
return json.MarshalIndent(b.orig, "", " ")
53+
}
54+
55+
func (b *TrafficBudget) MarshalCSVValue() any {
56+
return []*TrafficBudget{b}
57+
}
58+
59+
func formatOptionalInt(v *int) string {
60+
if v == nil {
61+
return "-"
62+
}
63+
return strconv.Itoa(*v)
64+
}
65+
66+
func toTrafficBudget(b *ps.TrafficBudget) *TrafficBudget {
67+
return &TrafficBudget{
68+
ID: b.ID,
69+
Name: b.Name,
70+
Mode: b.Mode,
71+
Capacity: formatOptionalInt(b.Capacity),
72+
Rate: formatOptionalInt(b.Rate),
73+
Burst: formatOptionalInt(b.Burst),
74+
Concurrency: formatOptionalInt(b.Concurrency),
75+
CreatedAt: printer.GetMilliseconds(b.CreatedAt),
76+
UpdatedAt: printer.GetMilliseconds(b.UpdatedAt),
77+
orig: b,
78+
}
79+
}

internal/mock/traffic_budget.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package mock
2+
3+
import (
4+
"context"
5+
6+
ps "github.com/planetscale/planetscale-go/planetscale"
7+
)
8+
9+
type TrafficBudgetsService struct {
10+
ListFn func(context.Context, *ps.ListTrafficBudgetsRequest) ([]*ps.TrafficBudget, error)
11+
ListFnInvoked bool
12+
GetFn func(context.Context, *ps.GetTrafficBudgetRequest) (*ps.TrafficBudget, error)
13+
GetFnInvoked bool
14+
CreateFn func(context.Context, *ps.CreateTrafficBudgetRequest) (*ps.TrafficBudget, error)
15+
CreateFnInvoked bool
16+
UpdateFn func(context.Context, *ps.UpdateTrafficBudgetRequest) (*ps.TrafficBudget, error)
17+
UpdateFnInvoked bool
18+
DeleteFn func(context.Context, *ps.DeleteTrafficBudgetRequest) error
19+
DeleteFnInvoked bool
20+
}
21+
22+
func (s *TrafficBudgetsService) List(ctx context.Context, req *ps.ListTrafficBudgetsRequest) ([]*ps.TrafficBudget, error) {
23+
s.ListFnInvoked = true
24+
return s.ListFn(ctx, req)
25+
}
26+
27+
func (s *TrafficBudgetsService) Get(ctx context.Context, req *ps.GetTrafficBudgetRequest) (*ps.TrafficBudget, error) {
28+
s.GetFnInvoked = true
29+
return s.GetFn(ctx, req)
30+
}
31+
32+
func (s *TrafficBudgetsService) Create(ctx context.Context, req *ps.CreateTrafficBudgetRequest) (*ps.TrafficBudget, error) {
33+
s.CreateFnInvoked = true
34+
return s.CreateFn(ctx, req)
35+
}
36+
37+
func (s *TrafficBudgetsService) Update(ctx context.Context, req *ps.UpdateTrafficBudgetRequest) (*ps.TrafficBudget, error) {
38+
s.UpdateFnInvoked = true
39+
return s.UpdateFn(ctx, req)
40+
}
41+
42+
func (s *TrafficBudgetsService) Delete(ctx context.Context, req *ps.DeleteTrafficBudgetRequest) error {
43+
s.DeleteFnInvoked = true
44+
return s.DeleteFn(ctx, req)
45+
}

0 commit comments

Comments
 (0)