Skip to content

Commit 5f18e09

Browse files
Show workspace, project, and env in KeyValue and Postgres text detail output (#502)
Updates the output for kv and pg commands to always show the workspace. If the datastore is in an environment, also displays the Project and Environment. GROW-2601 GitOrigin-RevId: 3c9c94c6f14e6061d7587b298dbb317291d125bc
1 parent 689a9d4 commit 5f18e09

9 files changed

Lines changed: 245 additions & 38 deletions

File tree

cmd/kvget_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ func TestKVGet_ByID_TextOutput(t *testing.T) {
3535
assert.False(t, server.HasRequest("GET", "/connection-info"), "no connection info request without flag")
3636
}
3737

38+
func TestKVGet_TextOutput_IncludesWorkspaceProjectAndEnvironment(t *testing.T) {
39+
server := renderapi.NewServer(t)
40+
project := server.CreateProject(
41+
renderapi.ProjectAttrs{Name: "My Project", OwnerId: kvTestWorkspaceID},
42+
renderapi.EnvAttrs{Name: "production"},
43+
)
44+
env := project.Env("production")
45+
kv := seedKVInEnv(server, "project-cache", env.Id)
46+
47+
result, err := executeKVGet(t, server, kv.Id, "--output", "text")
48+
require.NoError(t, err)
49+
50+
assert.Contains(t, result.Stdout, "Workspace: Test Workspace ("+kvTestWorkspaceID+")")
51+
assert.Contains(t, result.Stdout, "Project: My Project ("+project.Project.Id+")")
52+
assert.Contains(t, result.Stdout, "Environment: production ("+env.Id+")")
53+
}
54+
3855
func TestKVGet_ByName_TextOutput(t *testing.T) {
3956
server := renderapi.NewServer(t)
4057
kv := seedKV(server, "by-name-cache")

cmd/pgget_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ func TestPGGet_ByID(t *testing.T) {
4848
assert.False(t, harness.server.HasRequest("GET", "/connection-info"), "no connection info request without flag")
4949
}
5050

51+
func TestPGGet_TextOutput_IncludesWorkspaceProjectAndEnvironment(t *testing.T) {
52+
harness := newPGGetHarness(t)
53+
project := harness.server.CreateProject(
54+
renderapi.ProjectAttrs{Name: "My Project", OwnerId: pgActiveWorkspaceID},
55+
renderapi.EnvAttrs{Name: "production"},
56+
)
57+
env := project.Env("production")
58+
pg := seedPGInEnv(harness.server, "project-db", env.Id)
59+
60+
result, err := harness.execute(pg.Id, "--output", "text")
61+
require.NoError(t, err)
62+
63+
assert.Contains(t, result.Stdout, "Workspace: Test Workspace ("+pgActiveWorkspaceID+")")
64+
assert.Contains(t, result.Stdout, "Project: My Project ("+project.Project.Id+")")
65+
assert.Contains(t, result.Stdout, "Environment: production ("+env.Id+")")
66+
}
67+
5168
func TestPGGet_ByName(t *testing.T) {
5269
harness := newPGGetHarness(t)
5370
pg := seedPG(harness.server, "by-name-db")

internal/fakes/renderapi/server.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,37 @@ func (s *Server) ownerByID(id string) (client.Owner, bool) {
301301
return client.Owner{}, false
302302
}
303303

304+
// hydrateOwner returns the registered Owner for resources that store only an
305+
// owner ID. Real API detail responses include the expanded owner object; the
306+
// fake keeps fixtures lightweight by letting tests seed resource.Owner.Id and
307+
// server.Owners separately. Callers should assign the result to a response
308+
// copy, not the stored fixture, so GET handlers do not mutate seeded state.
309+
func (s *Server) hydrateOwner(owner client.Owner) client.Owner {
310+
if owner.Id == "" {
311+
return owner
312+
}
313+
hydrated, ok := s.ownerByID(owner.Id)
314+
if !ok {
315+
return owner
316+
}
317+
if hydrated.Type == "" {
318+
hydrated.Type = owner.Type
319+
}
320+
return hydrated
321+
}
322+
323+
func (s *Server) keyValueDetailResponse(kv *client.KeyValueDetail) client.KeyValueDetail {
324+
detail := *kv
325+
detail.Owner = s.hydrateOwner(detail.Owner)
326+
return detail
327+
}
328+
329+
func (s *Server) postgresDetailResponse(pg *client.PostgresDetail) client.PostgresDetail {
330+
detail := *pg
331+
detail.Owner = s.hydrateOwner(detail.Owner)
332+
return detail
333+
}
334+
304335
// URL returns the base URL of the fake server.
305336
func (s *Server) URL() string {
306337
return s.server.URL
@@ -614,7 +645,7 @@ func NewServer(t *testing.T) *Server {
614645
}
615646
s.KV.Instances = append(s.KV.Instances, kv)
616647

617-
writeJSON(w, http.StatusCreated, kv)
648+
writeJSON(w, http.StatusCreated, s.keyValueDetailResponse(kv))
618649
})
619650

620651
// GET /key-value/{id} — retrieve a KV instance
@@ -627,7 +658,7 @@ func NewServer(t *testing.T) *Server {
627658
}
628659
for _, kv := range s.KV.Instances {
629660
if kv.Id == id {
630-
writeJSON(w, http.StatusOK, kv)
661+
writeJSON(w, http.StatusOK, s.keyValueDetailResponse(kv))
631662
return
632663
}
633664
}
@@ -683,7 +714,7 @@ func NewServer(t *testing.T) *Server {
683714
kv.IpAllowList = *body.IpAllowList
684715
}
685716
kv.UpdatedAt = time.Now()
686-
writeJSON(w, http.StatusOK, kv)
717+
writeJSON(w, http.StatusOK, s.keyValueDetailResponse(kv))
687718
})
688719

689720
// POST /key-value/{id}/suspend — suspend a KV instance
@@ -833,7 +864,7 @@ func NewServer(t *testing.T) *Server {
833864
}
834865

835866
s.Postgres.Instances = append(s.Postgres.Instances, pg)
836-
writeJSON(w, http.StatusCreated, pg)
867+
writeJSON(w, http.StatusCreated, s.postgresDetailResponse(pg))
837868
})
838869

839870
// GET /postgres/{id} — retrieve a Postgres instance
@@ -846,7 +877,7 @@ func NewServer(t *testing.T) *Server {
846877
}
847878
for _, pg := range s.Postgres.Instances {
848879
if pg.Id == id {
849-
writeJSON(w, http.StatusOK, pg)
880+
writeJSON(w, http.StatusOK, s.postgresDetailResponse(pg))
850881
return
851882
}
852883
}
@@ -970,7 +1001,7 @@ func NewServer(t *testing.T) *Server {
9701001
pg.IpAllowList = *body.IpAllowList
9711002
}
9721003
pg.UpdatedAt = time.Now()
973-
writeJSON(w, http.StatusOK, pg)
1004+
writeJSON(w, http.StatusOK, s.postgresDetailResponse(pg))
9741005
})
9751006

9761007
registerServiceRoutes(mux, s, record)

pkg/keyvalue/output.go

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type KeyValueOut struct {
1818
Version string `json:"version,omitempty"`
1919
OwnerID string `json:"ownerId"`
2020
OwnerType client.OwnerType `json:"ownerType,omitempty"`
21+
WorkspaceName string `json:"-"`
2122
ProjectID *string `json:"projectId"`
2223
ProjectName string `json:"-"`
2324
EnvironmentID *string `json:"environmentId"`
@@ -83,17 +84,18 @@ func NewKeyValueOut(resolved *ResolvedKeyValue) KeyValueOut {
8384

8485
kv := resolved.KeyValue
8586
out := KeyValueOut{
86-
ID: kv.Id,
87-
Name: kv.Name,
88-
Plan: kv.Plan,
89-
Region: kv.Region,
90-
Status: kv.Status,
91-
CreatedAt: kv.CreatedAt,
92-
UpdatedAt: kv.UpdatedAt,
93-
Version: kv.Version,
94-
OwnerID: kv.Owner.Id,
95-
OwnerType: kv.Owner.Type,
96-
IPAllowList: kv.IpAllowList,
87+
ID: kv.Id,
88+
Name: kv.Name,
89+
Plan: kv.Plan,
90+
Region: kv.Region,
91+
Status: kv.Status,
92+
CreatedAt: kv.CreatedAt,
93+
UpdatedAt: kv.UpdatedAt,
94+
Version: kv.Version,
95+
OwnerID: kv.Owner.Id,
96+
OwnerType: kv.Owner.Type,
97+
WorkspaceName: kv.Owner.Name,
98+
IPAllowList: kv.IpAllowList,
9799
}
98100
if out.IPAllowList == nil {
99101
out.IPAllowList = []client.CidrBlockAndDescription{}
@@ -130,17 +132,18 @@ func NewKeyValueOutFromModel(model *Model) KeyValueOut {
130132

131133
kv := model.KeyValue
132134
out := KeyValueOut{
133-
ID: kv.Id,
134-
Name: kv.Name,
135-
Plan: kv.Plan,
136-
Region: kv.Region,
137-
Status: kv.Status,
138-
CreatedAt: kv.CreatedAt,
139-
UpdatedAt: kv.UpdatedAt,
140-
Version: kv.Version,
141-
OwnerID: kv.Owner.Id,
142-
OwnerType: kv.Owner.Type,
143-
IPAllowList: kv.IpAllowList,
135+
ID: kv.Id,
136+
Name: kv.Name,
137+
Plan: kv.Plan,
138+
Region: kv.Region,
139+
Status: kv.Status,
140+
CreatedAt: kv.CreatedAt,
141+
UpdatedAt: kv.UpdatedAt,
142+
Version: kv.Version,
143+
OwnerID: kv.Owner.Id,
144+
OwnerType: kv.Owner.Type,
145+
WorkspaceName: kv.Owner.Name,
146+
IPAllowList: kv.IpAllowList,
144147
}
145148
if out.IPAllowList == nil {
146149
out.IPAllowList = []client.CidrBlockAndDescription{}

pkg/text/keyvalue.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"github.com/jedib0t/go-pretty/table"
88

99
"github.com/render-oss/cli/pkg/keyvalue"
10+
"github.com/render-oss/cli/pkg/pointers"
11+
rstrings "github.com/render-oss/cli/pkg/strings"
1012
)
1113

1214
func KeyValueTable(v []keyvalue.KeyValueOut) string {
@@ -30,10 +32,21 @@ func KeyValueDetail(kv *keyvalue.KeyValueOut) string {
3032
lines := []string{
3133
fmt.Sprintf("Name: %s", kv.Name),
3234
fmt.Sprintf("ID: %s", kv.ID),
35+
}
36+
if line := workspaceLine(kv.WorkspaceName, kv.OwnerID); line != "" {
37+
lines = append(lines, line)
38+
}
39+
if label := rstrings.ResourceLabel(kv.ProjectName, pointers.StringValue(kv.ProjectID)); label != "" {
40+
lines = append(lines, fmt.Sprintf("Project: %s", label))
41+
}
42+
if label := rstrings.ResourceLabel(kv.EnvironmentName, pointers.StringValue(kv.EnvironmentID)); label != "" {
43+
lines = append(lines, fmt.Sprintf("Environment: %s", label))
44+
}
45+
lines = append(lines,
3346
fmt.Sprintf("Plan: %s", string(kv.Plan)),
3447
fmt.Sprintf("Region: %s", string(kv.Region)),
3548
fmt.Sprintf("Status: %s", string(kv.Status)),
36-
}
49+
)
3750
if kv.MaxmemoryPolicy != nil {
3851
lines = append(lines, fmt.Sprintf("Memory policy: %s", *kv.MaxmemoryPolicy))
3952
}

pkg/text/keyvalue_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package text_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/render-oss/cli/internal/testassert"
7+
"github.com/render-oss/cli/pkg/client"
8+
"github.com/render-oss/cli/pkg/keyvalue"
9+
"github.com/render-oss/cli/pkg/pointers"
10+
"github.com/render-oss/cli/pkg/text"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestKeyValueDetail_OmitsProjectAndEnvironmentWhenUnset(t *testing.T) {
15+
kv := keyvalue.KeyValueOut{
16+
ID: "red-abc123",
17+
Name: "my-cache",
18+
OwnerID: "tea-workspace",
19+
WorkspaceName: "My Workspace",
20+
Region: client.Oregon,
21+
Status: client.DatabaseStatusAvailable,
22+
}
23+
24+
out := text.KeyValueDetail(&kv)
25+
26+
assert.NotContains(t, out, "Project:")
27+
assert.NotContains(t, out, "Environment:")
28+
}
29+
30+
func TestKeyValueDetail_HappyPath(t *testing.T) {
31+
projectID := "prj-project"
32+
envID := "evm-production"
33+
memoryPolicy := "allkeys-lru"
34+
kv := keyvalue.KeyValueOut{
35+
ID: "red-abc123",
36+
Name: "my-cache",
37+
OwnerID: "tea-workspace",
38+
WorkspaceName: "My Workspace",
39+
ProjectID: pointers.From(projectID),
40+
ProjectName: "My Project",
41+
EnvironmentID: pointers.From(envID),
42+
EnvironmentName: "production",
43+
Plan: client.KeyValuePlanStarter,
44+
Region: client.Oregon,
45+
Status: client.DatabaseStatusAvailable,
46+
MaxmemoryPolicy: pointers.From(memoryPolicy),
47+
}
48+
49+
out := text.KeyValueDetail(&kv)
50+
51+
testassert.ContainsInOrder(t, out,
52+
"Name: my-cache",
53+
"ID: red-abc123",
54+
"Workspace: My Workspace (tea-workspace)",
55+
"Project: My Project (prj-project)",
56+
"Environment: production (evm-production)",
57+
"Plan: starter",
58+
"Region: oregon",
59+
"Status: available",
60+
"Memory policy: allkeys-lru",
61+
)
62+
}

pkg/text/owner.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package text
2+
3+
import (
4+
"fmt"
5+
6+
rstrings "github.com/render-oss/cli/pkg/strings"
7+
)
8+
9+
func workspaceLine(name, id string) string {
10+
label := rstrings.ResourceLabel(name, id)
11+
if label == "" {
12+
return ""
13+
}
14+
return fmt.Sprintf("Workspace: %s", label)
15+
}

pkg/text/postgres.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"github.com/jedib0t/go-pretty/table"
88

99
"github.com/render-oss/cli/pkg/client"
10+
"github.com/render-oss/cli/pkg/pointers"
1011
"github.com/render-oss/cli/pkg/postgres"
12+
rstrings "github.com/render-oss/cli/pkg/strings"
1113
)
1214

1315
func PostgresTable(v []postgres.PostgresListItemOut) string {
@@ -37,21 +39,29 @@ func PostgresDetail(pg *postgres.PostgresOut) string {
3739
lines := []string{
3840
fmt.Sprintf("Name: %s", pg.Name),
3941
fmt.Sprintf("ID: %s", pg.Id),
42+
}
43+
if line := workspaceLine(pg.Owner.Name, pg.Owner.Id); line != "" {
44+
lines = append(lines, line)
45+
}
46+
if label := rstrings.ResourceLabel(pg.ProjectName, pointers.StringValue(pg.ProjectID)); label != "" {
47+
lines = append(lines, fmt.Sprintf("Project: %s", label))
48+
}
49+
if label := rstrings.ResourceLabel(pg.EnvironmentName, pointers.StringValue(pg.EnvironmentId)); label != "" {
50+
lines = append(lines, fmt.Sprintf("Environment: %s", label))
51+
}
52+
lines = append(lines,
4053
fmt.Sprintf("Plan: %s", string(pg.Plan)),
4154
fmt.Sprintf("Version: %s", string(pg.Version)),
4255
fmt.Sprintf("Region: %s", string(pg.Region)),
4356
fmt.Sprintf("Status: %s", string(pg.Status)),
4457
fmt.Sprintf("Database: %s", pg.DatabaseName),
4558
fmt.Sprintf("User: %s", pg.DatabaseUser),
46-
}
59+
)
4760
if pg.DiskSizeGB != nil {
4861
lines = append(lines, fmt.Sprintf("Disk size: %d GB", *pg.DiskSizeGB))
4962
}
5063
lines = append(lines, fmt.Sprintf("Disk autoscaling: %s", boolLabel(pg.DiskAutoscalingEnabled)))
5164
lines = append(lines, fmt.Sprintf("High availability: %s", boolLabel(pg.HighAvailabilityEnabled)))
52-
if pg.EnvironmentId != nil {
53-
lines = append(lines, fmt.Sprintf("Environment ID: %s", *pg.EnvironmentId))
54-
}
5565
lines = append(lines, fmt.Sprintf("Dashboard: %s", pg.DashboardUrl))
5666
if block := readReplicasBlock(pg.ReadReplicas); block != "" {
5767
lines = append(lines, block)

0 commit comments

Comments
 (0)