Skip to content

Commit aa7be73

Browse files
Define Postgres command JSON output (#480)
Defines JSON / YAML output contracts for postgres commands. Does not wire them up yet. That is to come! GROW-2588 GitOrigin-RevId: 38a2829061648f3a9dbadd72d459842930d0a897
1 parent 2a7fb9b commit aa7be73

7 files changed

Lines changed: 674 additions & 10 deletions

File tree

cmd/testhelpers_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,18 @@ func newTestConfigPath(t *testing.T) string {
2525

2626
// requireSubMap returns the nested map stored at key, failing the test if the
2727
// key is absent or the value is not a map[string]any.
28+
//
29+
// Deprecated: use testrequire.SubMap in new tests.
2830
func requireSubMap(t *testing.T, body map[string]any, key string) map[string]any {
2931
t.Helper()
3032
return testrequire.SubMap(t, body, key)
3133
}
3234

3335
// requireSubSlice returns the nested slice stored at key, failing the test if
3436
// the key is absent or the value is not a []any.
37+
//
38+
// Deprecated: use testrequire.SubSlice in new tests.
3539
func requireSubSlice(t *testing.T, body map[string]any, key string) []any {
3640
t.Helper()
37-
require.Contains(t, body, key, "expected %q", key)
38-
require.IsType(t, []any{}, body[key], "expected %q to contain a slice", key)
39-
return body[key].([]any)
41+
return testrequire.SubSlice(t, body, key)
4042
}

internal/testrequire/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package testrequire provides additional require-style test helpers that
2+
// stretchr/testify does not cover.
3+
package testrequire

internal/testrequire/maps.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package testrequire
22

3-
import (
4-
"testing"
5-
6-
"github.com/stretchr/testify/require"
7-
)
3+
import "github.com/stretchr/testify/require"
84

95
// SubMap returns the nested map stored at key, failing the test if the key is
106
// absent or the value is not a map[string]any.
11-
func SubMap(t *testing.T, body map[string]any, key string) map[string]any {
12-
t.Helper()
7+
func SubMap(t require.TestingT, body map[string]any, key string) map[string]any {
8+
if h, ok := t.(interface{ Helper() }); ok {
9+
h.Helper()
10+
}
1311
require.Contains(t, body, key, "expected %q", key)
1412
require.IsType(t, map[string]any{}, body[key], "expected %q to contain a map", key)
1513
return body[key].(map[string]any)

internal/testrequire/slices.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package testrequire
2+
3+
import "github.com/stretchr/testify/require"
4+
5+
// SubSlice returns the nested slice stored at key, failing the test if the key
6+
// is absent or the value is not a []any.
7+
func SubSlice(t require.TestingT, body map[string]any, key string) []any {
8+
if h, ok := t.(interface{ Helper() }); ok {
9+
h.Helper()
10+
}
11+
require.Contains(t, body, key, "expected %q", key)
12+
require.IsType(t, []any{}, body[key], "expected %q to contain a slice", key)
13+
return body[key].([]any)
14+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package testrequire_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/render-oss/cli/internal/testrequire"
9+
)
10+
11+
func TestSubMap(t *testing.T) {
12+
body := map[string]any{
13+
"data": map[string]any{"id": "dpg-123"},
14+
"string": "this is a string",
15+
}
16+
17+
t.Run("nested map passes", func(t *testing.T) {
18+
assert.Equal(t, map[string]any{"id": "dpg-123"}, testrequire.SubMap(t, body, "data"))
19+
})
20+
21+
t.Run("missing key fails now", func(t *testing.T) {
22+
fake := &fakeT{}
23+
callAndRecover(func() {
24+
_ = testrequire.SubMap(fake, body, "missing")
25+
})
26+
assert.True(t, fake.failed)
27+
assert.True(t, fake.failNow)
28+
})
29+
30+
t.Run("wrong type fails now", func(t *testing.T) {
31+
fake := &fakeT{}
32+
callAndRecover(func() {
33+
_ = testrequire.SubMap(fake, body, "string")
34+
})
35+
assert.True(t, fake.failed)
36+
assert.True(t, fake.failNow)
37+
})
38+
}
39+
40+
func TestSubSlice(t *testing.T) {
41+
body := map[string]any{
42+
"data": []any{float64(1), float64(2), float64(3)},
43+
"string": "this is a string",
44+
}
45+
46+
t.Run("nested slice passes", func(t *testing.T) {
47+
assert.Equal(t, []any{float64(1), float64(2), float64(3)}, testrequire.SubSlice(t, body, "data"))
48+
})
49+
50+
t.Run("missing key fails now", func(t *testing.T) {
51+
fake := &fakeT{}
52+
callAndRecover(func() {
53+
_ = testrequire.SubSlice(fake, body, "missing")
54+
})
55+
assert.True(t, fake.failed)
56+
assert.True(t, fake.failNow)
57+
})
58+
59+
t.Run("wrong type fails now", func(t *testing.T) {
60+
fake := &fakeT{}
61+
callAndRecover(func() {
62+
_ = testrequire.SubSlice(fake, body, "string")
63+
})
64+
assert.True(t, fake.failed)
65+
assert.True(t, fake.failNow)
66+
})
67+
}
68+
69+
func callAndRecover(fn func()) {
70+
defer func() {
71+
_ = recover()
72+
}()
73+
fn()
74+
}
75+
76+
type fakeT struct {
77+
failed bool
78+
failNow bool
79+
}
80+
81+
func (f *fakeT) Helper() {}
82+
83+
func (f *fakeT) Errorf(string, ...any) {
84+
f.failed = true
85+
}
86+
87+
func (f *fakeT) FailNow() {
88+
f.failNow = true
89+
}

pkg/postgres/output.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package postgres
2+
3+
import (
4+
"reflect"
5+
6+
"github.com/render-oss/cli/internal/ipallowlist"
7+
"github.com/render-oss/cli/pkg/client"
8+
pgclient "github.com/render-oss/cli/pkg/client/postgres"
9+
"github.com/render-oss/cli/pkg/pointers"
10+
)
11+
12+
// PostgresListItemOut is the JSON/YAML contract for a Postgres list item.
13+
type PostgresListItemOut struct {
14+
client.Postgres
15+
ProjectID *string `json:"projectId"`
16+
ProjectName string `json:"-"`
17+
EnvironmentName string `json:"-"`
18+
}
19+
20+
// PostgresOut is the JSON/YAML contract for a Postgres detail.
21+
type PostgresOut struct {
22+
client.PostgresDetail
23+
ProjectID *string `json:"projectId"`
24+
ProjectName string `json:"-"`
25+
EnvironmentName string `json:"-"`
26+
ConnectionInfo *client.PostgresConnectionInfo `json:"connectionInfo,omitempty"`
27+
}
28+
29+
type PostgresListOut struct {
30+
Data []PostgresListItemOut `json:"data"`
31+
}
32+
33+
type GetOut struct {
34+
Data PostgresOut `json:"data"`
35+
}
36+
37+
type CreateOut = GetOut
38+
39+
type DeleteOut struct {
40+
Data PostgresOut `json:"data"`
41+
Meta DeleteOutMeta `json:"meta"`
42+
}
43+
44+
type DeleteOutMeta struct {
45+
Deleted bool `json:"deleted"`
46+
Message string `json:"message,omitempty"`
47+
}
48+
49+
type ResumeOut = GetOut
50+
51+
type SuspendOut struct {
52+
Data PostgresOut `json:"data"`
53+
Meta SuspendOutMeta `json:"meta"`
54+
}
55+
56+
type SuspendOutMeta struct {
57+
Suspended bool `json:"suspended"`
58+
Message string `json:"message,omitempty"`
59+
}
60+
61+
type PostgresUpdateOut struct {
62+
Data PostgresOut `json:"data"`
63+
Diff PostgresUpdateDiff `json:"diff"`
64+
}
65+
66+
type PostgresUpdateDiff struct {
67+
Name *PostgresFieldDiff[string] `json:"name,omitempty"`
68+
Plan *PostgresFieldDiff[pgclient.PostgresPlans] `json:"plan,omitempty"`
69+
DiskSizeGB *PostgresFieldDiff[*int] `json:"diskSizeGB,omitempty"`
70+
DiskAutoscalingEnabled *PostgresFieldDiff[bool] `json:"diskAutoscalingEnabled,omitempty"`
71+
HighAvailabilityEnabled *PostgresFieldDiff[bool] `json:"highAvailabilityEnabled,omitempty"`
72+
IPAllowList *PostgresFieldDiff[[]client.CidrBlockAndDescription] `json:"ipAllowList,omitempty"`
73+
ParameterOverrides *PostgresFieldDiff[*client.PostgresParameterOverrides] `json:"parameterOverrides,omitempty"`
74+
}
75+
76+
type PostgresFieldDiff[T any] struct {
77+
Before T `json:"before"`
78+
After T `json:"after"`
79+
}
80+
81+
func NewPostgresGetOut(resolved *ResolvedPostgres) GetOut {
82+
return GetOut{Data: newPostgresOut(resolved)}
83+
}
84+
85+
func NewPostgresCreateOut(resolved *ResolvedPostgres) CreateOut {
86+
return CreateOut{Data: newPostgresOut(resolved)}
87+
}
88+
89+
func NewPostgresResumeOut(resolved *ResolvedPostgres) ResumeOut {
90+
return ResumeOut{Data: newPostgresOut(resolved)}
91+
}
92+
93+
func NewPostgresDeleteOut(resolved *ResolvedPostgres) DeleteOut {
94+
return DeleteOut{Data: newPostgresOut(resolved)}
95+
}
96+
97+
func NewPostgresSuspendOut(resolved *ResolvedPostgres) SuspendOut {
98+
return SuspendOut{Data: newPostgresOut(resolved)}
99+
}
100+
101+
func newPostgresOut(resolved *ResolvedPostgres) PostgresOut {
102+
if resolved == nil || resolved.Postgres == nil {
103+
return PostgresOut{}
104+
}
105+
106+
out := PostgresOut{
107+
PostgresDetail: *resolved.Postgres,
108+
}
109+
finalizePostgresOut(&out, resolved.Project, resolved.Environment)
110+
return out
111+
}
112+
113+
func NewPostgresListOut(models []*Model) PostgresListOut {
114+
data := make([]PostgresListItemOut, 0, len(models))
115+
for _, model := range models {
116+
data = append(data, newPostgresListItemOutFromModel(model))
117+
}
118+
return PostgresListOut{Data: data}
119+
}
120+
121+
func newPostgresListItemOutFromModel(model *Model) PostgresListItemOut {
122+
if model == nil || model.Postgres == nil {
123+
return PostgresListItemOut{}
124+
}
125+
return newPostgresListItemOutFromPostgres(model.Postgres, model.Project, model.Environment)
126+
}
127+
128+
func NewPostgresUpdateOut(before *client.PostgresDetail, after *ResolvedPostgres) PostgresUpdateOut {
129+
out := PostgresUpdateOut{
130+
Data: newPostgresOut(after),
131+
}
132+
if before == nil {
133+
return out
134+
}
135+
out.Diff = NewPostgresUpdateDiff(before, &out.Data)
136+
return out
137+
}
138+
139+
func NewPostgresUpdateDiff(before *client.PostgresDetail, after *PostgresOut) PostgresUpdateDiff {
140+
var diff PostgresUpdateDiff
141+
if before == nil || after == nil {
142+
return diff
143+
}
144+
145+
if before.Name != after.Name {
146+
diff.Name = newPostgresFieldDiff(before.Name, after.Name)
147+
}
148+
if before.Plan != after.Plan {
149+
diff.Plan = newPostgresFieldDiff(before.Plan, after.Plan)
150+
}
151+
if !pointers.Equal(before.DiskSizeGB, after.DiskSizeGB) {
152+
diff.DiskSizeGB = newPostgresFieldDiff(before.DiskSizeGB, after.DiskSizeGB)
153+
}
154+
if before.DiskAutoscalingEnabled != after.DiskAutoscalingEnabled {
155+
diff.DiskAutoscalingEnabled = newPostgresFieldDiff(before.DiskAutoscalingEnabled, after.DiskAutoscalingEnabled)
156+
}
157+
if before.HighAvailabilityEnabled != after.HighAvailabilityEnabled {
158+
diff.HighAvailabilityEnabled = newPostgresFieldDiff(before.HighAvailabilityEnabled, after.HighAvailabilityEnabled)
159+
}
160+
if !ipallowlist.Equal(before.IpAllowList, after.IpAllowList) {
161+
diff.IPAllowList = newPostgresFieldDiff(before.IpAllowList, after.IpAllowList)
162+
}
163+
beforeOverrides := normalizePostgresParameterOverrides(before.ParameterOverrides)
164+
afterOverrides := normalizePostgresParameterOverrides(after.ParameterOverrides)
165+
if !reflect.DeepEqual(beforeOverrides, afterOverrides) {
166+
diff.ParameterOverrides = newPostgresFieldDiff(
167+
beforeOverrides,
168+
afterOverrides,
169+
)
170+
}
171+
return diff
172+
}
173+
174+
func newPostgresListItemOutFromPostgres(
175+
pg *client.Postgres,
176+
project *client.Project,
177+
env *client.Environment,
178+
) PostgresListItemOut {
179+
if pg == nil {
180+
return PostgresListItemOut{}
181+
}
182+
183+
out := PostgresListItemOut{
184+
Postgres: *pg,
185+
}
186+
finalizePostgresListItemOut(&out, project, env)
187+
return out
188+
}
189+
190+
func finalizePostgresOut(out *PostgresOut, project *client.Project, env *client.Environment) {
191+
if out.IpAllowList == nil {
192+
out.IpAllowList = []client.CidrBlockAndDescription{}
193+
}
194+
if out.ReadReplicas == nil {
195+
out.ReadReplicas = client.ReadReplicas{}
196+
}
197+
if out.ParameterOverrides != nil && len(*out.ParameterOverrides) == 0 {
198+
out.ParameterOverrides = nil
199+
}
200+
if env != nil {
201+
out.EnvironmentId = &env.Id
202+
out.EnvironmentName = env.Name
203+
}
204+
if project != nil {
205+
out.ProjectID = &project.Id
206+
out.ProjectName = project.Name
207+
}
208+
}
209+
210+
func finalizePostgresListItemOut(out *PostgresListItemOut, project *client.Project, env *client.Environment) {
211+
if out.IpAllowList == nil {
212+
out.IpAllowList = []client.CidrBlockAndDescription{}
213+
}
214+
if out.ReadReplicas == nil {
215+
out.ReadReplicas = client.ReadReplicas{}
216+
}
217+
if env != nil {
218+
out.EnvironmentId = &env.Id
219+
out.EnvironmentName = env.Name
220+
}
221+
if project != nil {
222+
out.ProjectID = &project.Id
223+
out.ProjectName = project.Name
224+
}
225+
}
226+
227+
func newPostgresFieldDiff[T any](before, after T) *PostgresFieldDiff[T] {
228+
return &PostgresFieldDiff[T]{
229+
Before: before,
230+
After: after,
231+
}
232+
}
233+
234+
func normalizePostgresParameterOverrides(overrides *client.PostgresParameterOverrides) *client.PostgresParameterOverrides {
235+
if overrides == nil || len(*overrides) == 0 {
236+
return nil
237+
}
238+
return overrides
239+
}

0 commit comments

Comments
 (0)