Skip to content

Commit a372a3d

Browse files
Add Postgres resolve and delete service methods (#444)
- Implements `PostgresService.Resolve` and `PostgresService.Delete` - `render ea pg delete` command will use these to identify and delete a Postgres! - Fixes a bug with `IsValidPostgresID` - Postgres IDs can have suffixes, to indicate what instance they are in a cluster of replicas or HA. PR 2 of 3 for https://linear.app/render-com/issue/GROW-2121/pg-delete GitOrigin-RevId: 895587bc823fa0f6b20266f686124e1b97292b0f
1 parent 44fdc7b commit a372a3d

6 files changed

Lines changed: 374 additions & 2 deletions

File tree

pkg/postgres/repo.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ package postgres
22

33
import (
44
"context"
5+
"errors"
6+
"net/http"
57

68
"github.com/render-oss/cli/pkg/client"
79
"github.com/render-oss/cli/pkg/config"
810
)
911

12+
// ErrPostgresNotFound is returned by Repo.GetPostgres when the API responds
13+
// with a 404, so callers can distinguish "this ID doesn't exist" from other
14+
// failure modes without parsing error messages.
15+
var ErrPostgresNotFound = errors.New("postgres not found")
16+
1017
type Repo struct {
1118
client *client.ClientWithResponses
1219
}
@@ -56,6 +63,10 @@ func (r *Repo) GetPostgres(ctx context.Context, id string) (*client.PostgresDeta
5663
return nil, err
5764
}
5865

66+
if resp.StatusCode() == http.StatusNotFound {
67+
return nil, ErrPostgresNotFound
68+
}
69+
5970
if err := client.ErrorFromResponse(resp); err != nil {
6071
return nil, err
6172
}
@@ -101,3 +112,12 @@ func (r *Repo) CreatePostgres(ctx context.Context, data client.CreatePostgresJSO
101112

102113
return resp.JSON201, nil
103114
}
115+
116+
func (r *Repo) DeletePostgres(ctx context.Context, id string) error {
117+
resp, err := r.client.DeletePostgresWithResponse(ctx, id)
118+
if err != nil {
119+
return err
120+
}
121+
122+
return client.ErrorFromResponse(resp)
123+
}

pkg/postgres/resolve.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/render-oss/cli/pkg/client"
9+
"github.com/render-oss/cli/pkg/config"
10+
"github.com/render-oss/cli/pkg/resolve"
11+
rstrings "github.com/render-oss/cli/pkg/strings"
12+
"github.com/render-oss/cli/pkg/tui"
13+
"github.com/render-oss/cli/pkg/validate"
14+
)
15+
16+
// ResolveInput describes a Postgres lookup by ID or name within optional
17+
// active-workspace project/environment scope.
18+
type ResolveInput struct {
19+
IDOrName string
20+
ProjectIDOrName *string
21+
EnvironmentIDOrName *string
22+
}
23+
24+
type resolveScope struct {
25+
project *client.Project
26+
env *client.Environment
27+
}
28+
29+
func (s *Service) resolve(ctx context.Context, input ResolveInput) (*client.PostgresDetail, error) {
30+
scope, err := s.resolveScope(ctx, input)
31+
if err != nil {
32+
return nil, err
33+
}
34+
return s.resolveInScope(ctx, input.IDOrName, scope)
35+
}
36+
37+
func (s *Service) resolveScope(ctx context.Context, input ResolveInput) (resolveScope, error) {
38+
if input.ProjectIDOrName == nil && input.EnvironmentIDOrName == nil {
39+
return resolveScope{}, nil
40+
}
41+
42+
resolved, err := s.resolver.ResolveScopeInActiveWorkspace(ctx, resolve.ActiveWorkspaceScopeInput{
43+
ProjectIDOrName: input.ProjectIDOrName,
44+
EnvironmentIDOrName: input.EnvironmentIDOrName,
45+
})
46+
if err != nil {
47+
return resolveScope{}, err
48+
}
49+
return resolveScope{project: resolved.Project, env: resolved.Environment}, nil
50+
}
51+
52+
func (s *Service) resolveInScope(ctx context.Context, idOrName string, scope resolveScope) (*client.PostgresDetail, error) {
53+
inputLooksLikeID := validate.IsPostgresID(idOrName)
54+
if inputLooksLikeID {
55+
detail, err := s.repo.GetPostgres(ctx, idOrName)
56+
if err != nil && !errors.Is(err, ErrPostgresNotFound) {
57+
return nil, err
58+
}
59+
if err == nil {
60+
if err := scope.checkMatch(detail); err != nil {
61+
return nil, err
62+
}
63+
return detail, nil
64+
}
65+
}
66+
67+
params := &client.ListPostgresParams{
68+
Name: &client.NameParam{idOrName},
69+
}
70+
environmentIDs, isScoped := scope.environmentIDs()
71+
if isScoped && len(environmentIDs) == 0 {
72+
return nil, scope.notFoundError(idOrName, inputLooksLikeID)
73+
}
74+
if isScoped {
75+
envParam := client.EnvironmentIdParam(environmentIDs)
76+
params.EnvironmentId = &envParam
77+
}
78+
79+
matches, err := s.repo.ListPostgres(ctx, params)
80+
if err != nil {
81+
return nil, err
82+
}
83+
if len(matches) == 0 {
84+
return nil, scope.notFoundError(idOrName, inputLooksLikeID)
85+
}
86+
if len(matches) > 1 {
87+
return nil, tui.UserFacingError{Message: scope.multipleMatchesMessage(idOrName)}
88+
}
89+
return s.repo.GetPostgres(ctx, matches[0].Id)
90+
}
91+
92+
func (s resolveScope) environmentIDs() ([]string, bool) {
93+
if s.env != nil {
94+
return []string{s.env.Id}, true
95+
}
96+
if s.project != nil {
97+
return s.project.EnvironmentIds, true
98+
}
99+
return nil, false
100+
}
101+
102+
func (s resolveScope) checkMatch(pg *client.PostgresDetail) error {
103+
if s.env == nil && s.project == nil {
104+
return nil
105+
}
106+
if s.env != nil && pg.EnvironmentId != nil && *pg.EnvironmentId == s.env.Id {
107+
return nil
108+
}
109+
if s.env != nil {
110+
return tui.UserFacingError{Message: fmt.Sprintf(
111+
"Postgres database %s is not in environment %s. Re-run without --environment, or pass the correct environment.",
112+
rstrings.ResourceLabel(pg.Name, pg.Id), postgresEnvironmentLabel(s.env),
113+
)}
114+
}
115+
if pg.EnvironmentId != nil {
116+
for _, envID := range s.project.EnvironmentIds {
117+
if *pg.EnvironmentId == envID {
118+
return nil
119+
}
120+
}
121+
}
122+
return tui.UserFacingError{Message: fmt.Sprintf(
123+
"Postgres database %s is not in project %s. Re-run without --project, or pass the correct project.",
124+
rstrings.ResourceLabel(pg.Name, pg.Id), postgresProjectLabel(s.project),
125+
)}
126+
}
127+
128+
func (s resolveScope) notFoundError(idOrName string, inputLooksLikeID bool) error {
129+
if inputLooksLikeID {
130+
return tui.UserFacingError{Message: fmt.Sprintf("No Postgres database with ID '%s'.", idOrName)}
131+
}
132+
if s.env != nil {
133+
return tui.UserFacingError{Message: fmt.Sprintf(
134+
"No Postgres database named '%s' in environment %s.",
135+
idOrName, postgresEnvironmentLabel(s.env),
136+
)}
137+
}
138+
if s.project != nil {
139+
return tui.UserFacingError{Message: fmt.Sprintf(
140+
"No Postgres database named '%s' in project %s.",
141+
idOrName, postgresProjectLabel(s.project),
142+
)}
143+
}
144+
workspace := activeWorkspaceLabel()
145+
if workspace == "" {
146+
return tui.UserFacingError{Message: fmt.Sprintf("No Postgres database named '%s'.", idOrName)}
147+
}
148+
return tui.UserFacingError{Message: fmt.Sprintf(
149+
"No Postgres database named '%s' in workspace %s. To search another workspace, run `render workspace set <name|ID>`, or pass the Postgres database ID instead.",
150+
idOrName, workspace,
151+
)}
152+
}
153+
154+
func (s resolveScope) multipleMatchesMessage(idOrName string) string {
155+
if s.env != nil {
156+
return fmt.Sprintf("Multiple Postgres databases found with name '%s' in environment %s. Please specify the Postgres database ID instead.", idOrName, postgresEnvironmentLabel(s.env))
157+
}
158+
if s.project != nil {
159+
return fmt.Sprintf(
160+
"Multiple Postgres databases found with name '%s' in project %s. Pass the Postgres database ID, or use --environment <id|name> to disambiguate.",
161+
idOrName, postgresProjectLabel(s.project),
162+
)
163+
}
164+
return fmt.Sprintf("Multiple Postgres databases found with name '%s'. Pass the Postgres database ID, or use --environment <id|name> to disambiguate.", idOrName)
165+
}
166+
167+
func postgresEnvironmentLabel(env *client.Environment) string {
168+
if env == nil {
169+
return ""
170+
}
171+
return rstrings.ResourceLabel(env.Name, env.Id)
172+
}
173+
174+
func postgresProjectLabel(project *client.Project) string {
175+
if project == nil {
176+
return ""
177+
}
178+
return rstrings.ResourceLabel(project.Name, project.Id)
179+
}
180+
181+
func activeWorkspaceLabel() string {
182+
id, _ := config.WorkspaceID()
183+
name, _ := config.WorkspaceName()
184+
return rstrings.ResourceLabel(name, id)
185+
}

pkg/postgres/service.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ func (s *Service) RestartPostgresDatabase(ctx context.Context, id string) error
7171
return s.repo.RestartPostgresDatabase(ctx, id)
7272
}
7373

74+
// Resolve resolves a Postgres database by ID or name within an optional
75+
// active-workspace project/environment scope.
76+
func (s *Service) Resolve(ctx context.Context, input ResolveInput) (*client.PostgresDetail, error) {
77+
return s.resolve(ctx, input)
78+
}
79+
80+
func (s *Service) Delete(ctx context.Context, id string) error {
81+
return s.repo.DeletePostgres(ctx, id)
82+
}
83+
7484
// Create applies defaults, resolves workspace/project/environment scope,
7585
// and calls the Postgres create endpoint. The non-interactive flag path
7686
// and (eventually) the interactive wizard both go through here.

0 commit comments

Comments
 (0)