Skip to content

Commit 9882985

Browse files
Add Postgres list command (#448)
https://linear.app/render-com/issue/GROW-2119/pg-listget GitOrigin-RevId: e212fcda5c32ea24b41b82435930503f3f653b6b
1 parent 017288b commit 9882985

9 files changed

Lines changed: 309 additions & 34 deletions

File tree

cmd/pg_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/render-oss/cli/pkg/client"
1212
"github.com/render-oss/cli/pkg/command"
1313
"github.com/render-oss/cli/pkg/dependencies"
14+
"github.com/render-oss/cli/pkg/pointers"
1415
)
1516

1617
var pgActiveWorkspaceID = testids.WorkspaceID("active")
@@ -45,3 +46,20 @@ func executePGCommand(t *testing.T, server *renderapi.Server, args ...string) (C
4546
execErr := root.Execute()
4647
return CommandResult{Stdout: stdout.String(), Stderr: stderr.String()}, execErr
4748
}
49+
50+
// seedPG adds a new Postgres database to the seeded active workspace.
51+
func seedPG(server *renderapi.Server, name string) *client.PostgresDetail {
52+
return server.Postgres.Add(renderapi.NewPostgres(&client.PostgresDetail{
53+
Name: name,
54+
Owner: client.Owner{Id: pgActiveWorkspaceID},
55+
}))
56+
}
57+
58+
// seedPGInEnv adds a new Postgres database to the specified environment inside the active workspace.
59+
func seedPGInEnv(server *renderapi.Server, name string, envID string) *client.PostgresDetail {
60+
return server.Postgres.Add(renderapi.NewPostgres(&client.PostgresDetail{
61+
Name: name,
62+
Owner: client.Owner{Id: pgActiveWorkspaceID},
63+
EnvironmentId: pointers.From(envID),
64+
}))
65+
}

cmd/pgdelete_test.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
renderapi "github.com/render-oss/cli/internal/fakes/renderapi"
1111
"github.com/render-oss/cli/internal/testids"
1212
"github.com/render-oss/cli/pkg/client"
13-
"github.com/render-oss/cli/pkg/pointers"
1413
)
1514

1615
func executePGDelete(t *testing.T, server *renderapi.Server, extraArgs ...string) (CommandResult, error) {
@@ -21,21 +20,6 @@ func executePGDelete(t *testing.T, server *renderapi.Server, extraArgs ...string
2120
return executePGCommand(t, server, append([]string{"ea", "pg", "delete"}, extraArgs...)...)
2221
}
2322

24-
func seedPG(server *renderapi.Server, name string) *client.PostgresDetail {
25-
return server.Postgres.Add(renderapi.NewPostgres(&client.PostgresDetail{
26-
Name: name,
27-
Owner: client.Owner{Id: pgActiveWorkspaceID},
28-
}))
29-
}
30-
31-
func seedPGInEnv(server *renderapi.Server, name string, envID string) *client.PostgresDetail {
32-
return server.Postgres.Add(renderapi.NewPostgres(&client.PostgresDetail{
33-
Name: name,
34-
Owner: client.Owner{Id: pgActiveWorkspaceID},
35-
EnvironmentId: pointers.From(envID),
36-
}))
37-
}
38-
3923
func TestPGDelete_PreviewByID_DoesNotDelete(t *testing.T) {
4024
server := renderapi.NewServer(t)
4125
pg := seedPG(server, "my-db")

cmd/pglist.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/render-oss/cli/pkg/command"
7+
"github.com/render-oss/cli/pkg/dependencies"
8+
"github.com/render-oss/cli/pkg/postgres"
9+
"github.com/render-oss/cli/pkg/text"
10+
pgtypes "github.com/render-oss/cli/pkg/types/postgres"
11+
)
12+
13+
func newPgListCmd(deps *dependencies.Dependencies) *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "list",
16+
Aliases: []string{"ls"},
17+
Short: "List Postgres databases",
18+
Args: cobra.NoArgs,
19+
SilenceUsage: true,
20+
Long: `List Postgres databases in the active workspace.
21+
22+
Use --project to narrow results to a single project, --environment to narrow
23+
to a single environment, or both — when both are supplied, the environment is
24+
resolved within that project.`,
25+
Example: ` # List all Postgres databases in the active workspace
26+
render ea pg list
27+
28+
# List all Postgres databases in a project
29+
render ea pg list --project my-project
30+
31+
# Filter by environment name
32+
render ea pg list --environment production
33+
34+
# Disambiguate an environment name by project
35+
render ea pg list --project my-project --environment production
36+
37+
# JSON output
38+
render ea pg list --output json`,
39+
}
40+
41+
cmd.Flags().String("project", "",
42+
"Project ID or name (optional). Narrows results to environments in this project.")
43+
cmd.Flags().String("environment", "",
44+
"Environment ID or name (optional). Narrows results to this environment.")
45+
46+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
47+
command.DefaultFormatNonInteractive(cmd)
48+
49+
var input pgtypes.ListPostgresInput
50+
if err := command.ParseCommand(cmd, args, &input); err != nil {
51+
return err
52+
}
53+
input = pgtypes.NormalizeListInput(input)
54+
55+
_, err := command.NonInteractive(cmd, func() ([]*postgres.Model, error) {
56+
return deps.PostgresService().List(cmd.Context(), input)
57+
}, text.PostgresTable)
58+
return err
59+
}
60+
61+
return cmd
62+
}

cmd/pglist_test.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
renderapi "github.com/render-oss/cli/internal/fakes/renderapi"
11+
"github.com/render-oss/cli/internal/testids"
12+
"github.com/render-oss/cli/pkg/client"
13+
)
14+
15+
type pgListHarness struct {
16+
t *testing.T
17+
server *renderapi.Server
18+
}
19+
20+
// newPGListHarness sets up a server fake and seeds it with an (active) workspace
21+
func newPGListHarness(t *testing.T) pgListHarness {
22+
t.Helper()
23+
24+
server := renderapi.NewServer(t)
25+
server.Owners.Add(renderapi.NewOwner(client.Owner{Id: pgActiveWorkspaceID, Name: "Test Workspace"}))
26+
t.Setenv("RENDER_WORKSPACE", pgActiveWorkspaceID)
27+
28+
return pgListHarness{t: t, server: server}
29+
}
30+
31+
// execute invokes the `render ea pg list` command, passing through all extraArgs
32+
func (h pgListHarness) execute(extraArgs ...string) (CommandResult, error) {
33+
h.t.Helper()
34+
35+
return executePGCommand(h.t, h.server, append([]string{"ea", "pg", "list"}, extraArgs...)...)
36+
}
37+
38+
func TestPGList_NoDatabases(t *testing.T) {
39+
harness := newPGListHarness(t)
40+
41+
result, err := harness.execute("--output", "text")
42+
require.NoError(t, err)
43+
44+
assert.NotContains(t, result.Stdout, "dpg-")
45+
assert.Contains(t, result.Stdout, "No Postgres databases")
46+
47+
result, err = harness.execute("--output", "json")
48+
require.NoError(t, err)
49+
50+
var body []struct {
51+
Postgres struct {
52+
ID string `json:"id"`
53+
} `json:"postgres"`
54+
}
55+
require.NoError(t, json.Unmarshal([]byte(result.Stdout), &body))
56+
assert.Empty(t, body)
57+
}
58+
59+
func TestPGList_MultipleDatabases(t *testing.T) {
60+
harness := newPGListHarness(t)
61+
pg1 := seedPG(harness.server, "db-one")
62+
pg2 := seedPG(harness.server, "db-two")
63+
64+
result, err := harness.execute("--output", "text")
65+
require.NoError(t, err)
66+
67+
assert.Contains(t, result.Stdout, pg1.Id)
68+
assert.Contains(t, result.Stdout, "db-one")
69+
assert.Contains(t, result.Stdout, pg2.Id)
70+
assert.Contains(t, result.Stdout, "db-two")
71+
}
72+
73+
func TestPGList_FilterByEnvironmentName(t *testing.T) {
74+
harness := newPGListHarness(t)
75+
projectID := testids.ProjectID("project")
76+
envProdID := testids.EnvironmentID("production")
77+
envStagingID := testids.EnvironmentID("staging")
78+
harness.server.Projects.Add(renderapi.NewProject(renderapi.ProjectAttrs{Id: projectID, Name: "My Project", OwnerId: pgActiveWorkspaceID}))
79+
harness.server.Environments.Add(renderapi.NewEnvironment(client.Environment{Id: envProdID, Name: "production", ProjectId: projectID}))
80+
harness.server.Environments.Add(renderapi.NewEnvironment(client.Environment{Id: envStagingID, Name: "staging", ProjectId: projectID}))
81+
82+
prodPG := seedPGInEnv(harness.server, "prod-db", envProdID)
83+
stagingPG := seedPGInEnv(harness.server, "staging-db", envStagingID)
84+
85+
result, err := harness.execute("--environment", "production", "--output", "text")
86+
require.NoError(t, err)
87+
88+
assert.Contains(t, result.Stdout, prodPG.Id)
89+
assert.NotContains(t, result.Stdout, stagingPG.Id)
90+
}
91+
92+
func TestPGList_FilterByProject(t *testing.T) {
93+
harness := newPGListHarness(t)
94+
95+
projectAID := testids.ProjectID("project-a")
96+
projectBID := testids.ProjectID("project-b")
97+
projectAProdID := testids.EnvironmentID("project-a-production")
98+
projectAStagingID := testids.EnvironmentID("project-a-staging")
99+
projectBProdID := testids.EnvironmentID("project-b-production")
100+
101+
projectA := renderapi.NewProject(renderapi.ProjectAttrs{Id: projectAID, Name: "Project A", OwnerId: pgActiveWorkspaceID})
102+
projectA.EnvironmentIds = []string{projectAProdID, projectAStagingID}
103+
projectB := renderapi.NewProject(renderapi.ProjectAttrs{Id: projectBID, Name: "Project B", OwnerId: pgActiveWorkspaceID})
104+
projectB.EnvironmentIds = []string{projectBProdID}
105+
106+
harness.server.Projects.Add(projectA)
107+
harness.server.Projects.Add(projectB)
108+
harness.server.Environments.Add(renderapi.NewEnvironment(client.Environment{Id: projectAProdID, Name: "production", ProjectId: projectAID}))
109+
harness.server.Environments.Add(renderapi.NewEnvironment(client.Environment{Id: projectAStagingID, Name: "staging", ProjectId: projectAID}))
110+
harness.server.Environments.Add(renderapi.NewEnvironment(client.Environment{Id: projectBProdID, Name: "production", ProjectId: projectBID}))
111+
112+
projectAProdPG := seedPGInEnv(harness.server, "a-prod-db", projectAProdID)
113+
projectAStagingPG := seedPGInEnv(harness.server, "a-staging-db", projectAStagingID)
114+
projectBProdPG := seedPGInEnv(harness.server, "b-prod-db", projectBProdID)
115+
116+
result, err := harness.execute("--project", "Project A", "--output", "text")
117+
require.NoError(t, err)
118+
119+
assert.Contains(t, result.Stdout, projectAProdPG.Id)
120+
assert.Contains(t, result.Stdout, projectAStagingPG.Id)
121+
assert.NotContains(t, result.Stdout, projectBProdPG.Id)
122+
}
123+
124+
func TestPGList_FilterByProjectAndEnvironment_NarrowsToEnvWithinProject(t *testing.T) {
125+
harness := newPGListHarness(t)
126+
127+
projectAID := testids.ProjectID("project-a")
128+
projectBID := testids.ProjectID("project-b")
129+
projectAProdID := testids.EnvironmentID("project-a-production")
130+
projectBProdID := testids.EnvironmentID("project-b-production")
131+
132+
projectA := renderapi.NewProject(renderapi.ProjectAttrs{Id: projectAID, Name: "Project A", OwnerId: pgActiveWorkspaceID})
133+
projectA.EnvironmentIds = []string{projectAProdID}
134+
projectB := renderapi.NewProject(renderapi.ProjectAttrs{Id: projectBID, Name: "Project B", OwnerId: pgActiveWorkspaceID})
135+
projectB.EnvironmentIds = []string{projectBProdID}
136+
137+
harness.server.Projects.Add(projectA)
138+
harness.server.Projects.Add(projectB)
139+
harness.server.Environments.Add(renderapi.NewEnvironment(client.Environment{Id: projectAProdID, Name: "production", ProjectId: projectAID}))
140+
harness.server.Environments.Add(renderapi.NewEnvironment(client.Environment{Id: projectBProdID, Name: "production", ProjectId: projectBID}))
141+
142+
projectAProdPG := seedPGInEnv(harness.server, "a-prod-db", projectAProdID)
143+
projectBProdPG := seedPGInEnv(harness.server, "b-prod-db", projectBProdID)
144+
145+
result, err := harness.execute(
146+
"--project", "Project A",
147+
"--environment", "production",
148+
"--output", "text",
149+
)
150+
require.NoError(t, err)
151+
152+
assert.Contains(t, result.Stdout, projectAProdPG.Id)
153+
assert.NotContains(t, result.Stdout, projectBProdPG.Id)
154+
}
155+
156+
func TestPGList_JSONOutput(t *testing.T) {
157+
harness := newPGListHarness(t)
158+
pg1 := seedPG(harness.server, "json-db-one")
159+
pg2 := seedPG(harness.server, "json-db-two")
160+
161+
result, err := harness.execute("--output", "json")
162+
require.NoError(t, err)
163+
164+
var body []struct {
165+
Postgres struct {
166+
ID string `json:"id"`
167+
Name string `json:"name"`
168+
} `json:"postgres"`
169+
}
170+
require.NoError(t, json.Unmarshal([]byte(result.Stdout), &body))
171+
require.Len(t, body, 2)
172+
173+
// Build an _un-ordered_ object to assert against
174+
// Order is just determined by our fake render api, so not meaningful
175+
got := map[string]string{}
176+
for _, item := range body {
177+
got[item.Postgres.ID] = item.Postgres.Name
178+
}
179+
assert.Equal(t, map[string]string{
180+
pg1.Id: "json-db-one",
181+
pg2.Id: "json-db-two",
182+
}, got)
183+
}
184+
185+
func TestPGList_DefaultOutput_TreatedAsText(t *testing.T) {
186+
harness := newPGListHarness(t)
187+
pg := seedPG(harness.server, "default-out")
188+
189+
result, err := harness.execute()
190+
require.NoError(t, err)
191+
192+
assert.Contains(t, result.Stdout, pg.Id)
193+
assert.Contains(t, result.Stdout, "default-out")
194+
}
195+
196+
func TestPGList_UnknownProject_Errors(t *testing.T) {
197+
harness := newPGListHarness(t)
198+
seedPG(harness.server, "any-db")
199+
200+
_, err := harness.execute("--project", "does-not-exist", "--output", "text")
201+
require.Error(t, err)
202+
assert.Contains(t, err.Error(), "does-not-exist")
203+
}

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func setupWorkspaceCommands(deps *dependencies.Dependencies) {
155155
}
156156

157157
func setupPGCommands(earlyAccess *cobra.Command, deps *dependencies.Dependencies) {
158-
earlyAccess.AddCommand(newPgCmd(newPgCreateCmd(deps), newPgDeleteCmd(deps)))
158+
earlyAccess.AddCommand(newPgCmd(newPgCreateCmd(deps), newPgDeleteCmd(deps), newPgListCmd(deps)))
159159
}
160160

161161
func SetupCommands() error {

pkg/postgres/list.go

Lines changed: 0 additions & 12 deletions
This file was deleted.

pkg/postgres/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (s *Service) ListPostgres(ctx context.Context, params *client.ListPostgresP
5858

5959
// List resolves active-workspace project/environment selectors into API params
6060
// before listing Postgres databases.
61-
func (s *Service) List(ctx context.Context, input ListInput) ([]*Model, error) {
61+
func (s *Service) List(ctx context.Context, input pgtypes.ListPostgresInput) ([]*Model, error) {
6262
params := &client.ListPostgresParams{}
6363

6464
if input.HasFilter() {
@@ -154,7 +154,7 @@ func (s *Service) hydratePostgresModel(ctx context.Context, postgres *client.Pos
154154
// environment IDs to filter on. A valid project with no environments returns
155155
// an empty ID list, which callers should treat as an empty resource list rather
156156
// than an invalid selector.
157-
func (s *Service) listEnvIDs(ctx context.Context, input ListInput) ([]string, error) {
157+
func (s *Service) listEnvIDs(ctx context.Context, input pgtypes.ListPostgresInput) ([]string, error) {
158158
scope, err := s.resolver.ResolveScopeInActiveWorkspace(ctx, resolve.ActiveWorkspaceScopeInput{
159159
ProjectIDOrName: input.ProjectIDOrName,
160160
EnvironmentIDOrName: input.EnvironmentIDOrName,

pkg/postgres/service_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func TestServiceList_FilterByProject(t *testing.T) {
189189
projAPostgres := harness.addPostgresInEnvironment("project-a-db", projAProdEnv.Id)
190190
projBPostgres := harness.addPostgresInEnvironment("project-b-db", projBProdEnv.Id)
191191

192-
result, err := harness.service.List(context.Background(), postgres.ListInput{
192+
result, err := harness.service.List(context.Background(), pgtypes.ListPostgresInput{
193193
ProjectIDOrName: pointers.From("Project A"),
194194
})
195195
require.NoError(t, err)
@@ -207,7 +207,7 @@ func TestServiceList_ProjectWithNoEnvironmentsReturnsEmptyList(t *testing.T) {
207207
})
208208
harness.server.Projects.Add(project)
209209

210-
result, err := harness.service.List(context.Background(), postgres.ListInput{
210+
result, err := harness.service.List(context.Background(), pgtypes.ListPostgresInput{
211211
ProjectIDOrName: pointers.From("Empty Project"),
212212
})
213213
require.NoError(t, err)
@@ -227,7 +227,7 @@ func TestServiceList_EnvironmentLookupStaysInActiveWorkspace(t *testing.T) {
227227
activeWorkspacePG := harness.addPostgresInEnvironment("active-db", activeProduction.Id)
228228
otherWorkspacePG := harness.addPostgresInWorkspaceEnvironment("other-db", otherWorkspaceID, otherProduction.Id)
229229

230-
result, err := harness.service.List(context.Background(), postgres.ListInput{
230+
result, err := harness.service.List(context.Background(), pgtypes.ListPostgresInput{
231231
EnvironmentIDOrName: pointers.From("production"),
232232
})
233233
require.NoError(t, err)

0 commit comments

Comments
 (0)