Skip to content

Commit 33a6b8d

Browse files
Refactor service update command construction (#489)
Refactor `service update` cobra command instance from a singleton (initialized in `init`) into a dependency-injected command function, which is registered alongside the other service subcommands in `root.go`. Currently, `service update` is a no-op command that does nothing! This does not change that. GROW-2375 GitOrigin-RevId: f7dbe2f7b0175eeb665f7269ba76601fb6066f90
1 parent a857312 commit 33a6b8d

3 files changed

Lines changed: 74 additions & 54 deletions

File tree

cmd/root.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ func setupSandboxCommands(earlyAccess *cobra.Command, deps *dependencies.Depende
166166
earlyAccess.AddCommand(newSandboxCmd(newSandboxCreateCmd(deps), newSandboxExecCmd(deps), newSandboxListCmd(deps), newSandboxStopCmd(deps)))
167167
}
168168

169+
func setupServiceCommands(deps *dependencies.Dependencies) {
170+
servicesCmd.AddCommand(newServiceDeleteCmd(deps), newServiceUpdateCmd(deps))
171+
}
172+
169173
func SetupCommands() error {
170174
c, err := client.NewDefaultClient()
171175
if err != nil {
@@ -184,7 +188,7 @@ func SetupCommands() error {
184188
setupWorkflowCommands(deps)
185189
setupLogCommands(deps)
186190
setupWorkspaceCommands(deps)
187-
servicesCmd.AddCommand(newServiceDeleteCmd(deps))
191+
setupServiceCommands(deps)
188192
setupKVCommands(EarlyAccessCmd, deps)
189193
setupPGCommands(EarlyAccessCmd, deps)
190194
setupSandboxCommands(EarlyAccessCmd, deps)

cmd/serviceupdate.go

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,22 @@ import (
1313
servicetypes "github.com/render-oss/cli/pkg/types/service"
1414
)
1515

16-
var ServiceUpdateCmd = &cobra.Command{
17-
Use: "update <service>",
18-
Args: cobra.ExactArgs(1),
19-
Short: "Update configuration for an existing service",
20-
Long: `Update a service on Render. This command only runs in non-interactive modes.
16+
func newServiceUpdateCmd(deps *dependencies.Dependencies) *cobra.Command {
17+
cmd := &cobra.Command{
18+
Use: "update <service>",
19+
Args: cobra.ExactArgs(1),
20+
Short: "Update configuration for an existing service",
21+
Long: `Update a service on Render. This command only runs in non-interactive modes.
2122
2223
Provide configuration updates with flags.`,
23-
Example: ` # Rename a service
24+
Example: ` # Rename a service
2425
render services update my-service --name my-new-name --output json
2526
2627
# Change a service plan
2728
render services update srv-abc123 --plan pro --output json`,
28-
}
29-
30-
func init() {
31-
servicesCmd.AddCommand(ServiceUpdateCmd)
29+
}
3230

33-
ServiceUpdateCmd.RunE = func(cmd *cobra.Command, args []string) error {
31+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
3432
var cliInput servicetypes.ServiceUpdateInput
3533
if err := command.ParseCommand(cmd, args, &cliInput); err != nil {
3634
return fmt.Errorf("failed to parse command: %w", err)
@@ -47,7 +45,7 @@ func init() {
4745
ctx := cmd.Context()
4846

4947
nonInteractive, err := command.NonInteractiveWithConfirm(cmd, func() (*client.Service, error) {
50-
return updateServiceNonInteractive(ctx, cliInput)
48+
return updateServiceNonInteractive(ctx, deps, cliInput)
5149
}, func(svc *client.Service) string {
5250
return text.FormatStringF("Updated service %s (%s)", svc.Name, svc.Id)
5351
}, nil)
@@ -63,56 +61,57 @@ func init() {
6361
}
6462

6563
// Identity and source flags
66-
ServiceUpdateCmd.Flags().String("name", "", "Service name")
67-
ServiceUpdateCmd.Flags().String("repo", "", "Git repository URL")
68-
ServiceUpdateCmd.Flags().String("branch", "", "Git branch")
69-
ServiceUpdateCmd.Flags().String("image", "", "Docker image URL")
64+
cmd.Flags().String("name", "", "Service name")
65+
cmd.Flags().String("repo", "", "Git repository URL")
66+
cmd.Flags().String("branch", "", "Git branch")
67+
cmd.Flags().String("image", "", "Docker image URL")
7068

7169
// Deployment configuration flags
72-
ServiceUpdateCmd.Flags().String("plan", "", "Service plan")
70+
cmd.Flags().String("plan", "", "Service plan")
7371
runtimeFlag := command.NewEnumInput(servicetypes.ServiceRuntimeValues(), false)
74-
ServiceUpdateCmd.Flags().Var(runtimeFlag, "runtime", "Runtime environment")
75-
ServiceUpdateCmd.Flags().String("root-directory", "", "Root directory")
72+
cmd.Flags().Var(runtimeFlag, "runtime", "Runtime environment")
73+
cmd.Flags().String("root-directory", "", "Root directory")
7674

7775
// Build and start commands
78-
ServiceUpdateCmd.Flags().String("build-command", "", "Build command")
79-
ServiceUpdateCmd.Flags().String("start-command", "", "Start command")
80-
ServiceUpdateCmd.Flags().String("pre-deploy-command", "", "Pre-deploy command")
76+
cmd.Flags().String("build-command", "", "Build command")
77+
cmd.Flags().String("start-command", "", "Start command")
78+
cmd.Flags().String("pre-deploy-command", "", "Pre-deploy command")
8179

8280
// Type-specific flags
83-
ServiceUpdateCmd.Flags().String("health-check-path", "", "Health check path")
84-
ServiceUpdateCmd.Flags().String("publish-directory", "", "Publish directory")
85-
ServiceUpdateCmd.Flags().String("cron-command", "", "Cron command")
86-
ServiceUpdateCmd.Flags().String("cron-schedule", "", "Cron schedule")
81+
cmd.Flags().String("health-check-path", "", "Health check path")
82+
cmd.Flags().String("publish-directory", "", "Publish directory")
83+
cmd.Flags().String("cron-command", "", "Cron command")
84+
cmd.Flags().String("cron-schedule", "", "Cron schedule")
8785

8886
// Registry flag
89-
ServiceUpdateCmd.Flags().String("registry-credential", "", "Registry credential")
87+
cmd.Flags().String("registry-credential", "", "Registry credential")
9088

9189
// Behavior flags
92-
ServiceUpdateCmd.Flags().Bool("auto-deploy", false, "Enable auto-deploy")
90+
cmd.Flags().Bool("auto-deploy", false, "Enable auto-deploy")
9391

9492
// Build filter flags
95-
ServiceUpdateCmd.Flags().StringArray("build-filter-path", nil, "Build filter path (can be specified multiple times)")
96-
ServiceUpdateCmd.Flags().StringArray("build-filter-ignored-path", nil, "Build filter ignored path (can be specified multiple times)")
93+
cmd.Flags().StringArray("build-filter-path", nil, "Build filter path (can be specified multiple times)")
94+
cmd.Flags().StringArray("build-filter-ignored-path", nil, "Build filter ignored path (can be specified multiple times)")
9795

9896
// Instance and scaling flags
99-
ServiceUpdateCmd.Flags().Int("num-instances", 0, "Number of instances")
100-
ServiceUpdateCmd.Flags().Int("max-shutdown-delay", 0, "Max shutdown delay in seconds")
97+
cmd.Flags().Int("num-instances", 0, "Number of instances")
98+
cmd.Flags().Int("max-shutdown-delay", 0, "Max shutdown delay in seconds")
10199

102100
// Preview and preview generation flags
103101
previewsFlag := command.NewEnumInput(servicetypes.PreviewsGenerationValues(), false)
104-
ServiceUpdateCmd.Flags().Var(previewsFlag, "previews", "Preview generation mode")
102+
cmd.Flags().Var(previewsFlag, "previews", "Preview generation mode")
105103

106104
// Maintenance mode flags
107-
ServiceUpdateCmd.Flags().Bool("maintenance-mode", false, "Enable maintenance mode")
108-
ServiceUpdateCmd.Flags().String("maintenance-mode-uri", "", "Maintenance mode URI")
105+
cmd.Flags().Bool("maintenance-mode", false, "Enable maintenance mode")
106+
cmd.Flags().String("maintenance-mode-uri", "", "Maintenance mode URI")
109107

110108
// IP allow list flag
111-
ServiceUpdateCmd.Flags().StringArray("ip-allow-list", nil, "IP allow list entry in cidr=..., description=... format (can be specified multiple times)")
109+
cmd.Flags().StringArray("ip-allow-list", nil, "IP allow list entry in cidr=..., description=... format (can be specified multiple times)")
110+
111+
return cmd
112112
}
113113

114-
func updateServiceNonInteractive(ctx context.Context, cliInput servicetypes.ServiceUpdateInput) (*client.Service, error) {
115-
deps := dependencies.GetFromContext(ctx)
114+
func updateServiceNonInteractive(ctx context.Context, deps *dependencies.Dependencies, cliInput servicetypes.ServiceUpdateInput) (*client.Service, error) {
116115
serviceRepo := deps.ServiceRepo()
117116

118117
// Resolve service ID

cmd/serviceupdate_test.go

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,53 @@ package cmd
33
import (
44
"testing"
55

6+
"github.com/render-oss/cli/pkg/dependencies"
67
"github.com/spf13/cobra"
78
"github.com/stretchr/testify/require"
89
)
910

11+
func newServiceUpdateTestCmd() *cobra.Command {
12+
return newServiceUpdateCmd(dependencies.New(nil))
13+
}
14+
1015
func TestServiceUpdateCmdArgsValidation(t *testing.T) {
16+
cmd := newServiceUpdateTestCmd()
17+
1118
t.Run("rejects zero positional args", func(t *testing.T) {
12-
require.Error(t, ServiceUpdateCmd.Args(ServiceUpdateCmd, []string{}))
19+
require.Error(t, cmd.Args(cmd, []string{}))
1320
})
1421

1522
t.Run("accepts one positional arg", func(t *testing.T) {
16-
require.NoError(t, ServiceUpdateCmd.Args(ServiceUpdateCmd, []string{"my-service"}))
23+
require.NoError(t, cmd.Args(cmd, []string{"my-service"}))
1724
})
1825

1926
t.Run("rejects more than one positional arg", func(t *testing.T) {
20-
require.Error(t, ServiceUpdateCmd.Args(ServiceUpdateCmd, []string{"arg1", "arg2"}))
27+
require.Error(t, cmd.Args(cmd, []string{"arg1", "arg2"}))
2128
})
2229
}
2330

2431
func TestServiceUpdateAliasResolvesToUpdateCommand(t *testing.T) {
25-
plural, _, err := rootCmd.Find([]string{"services", "update"})
32+
root := newRootCmd()
33+
services := cobraServicesCommand()
34+
update := newServiceUpdateTestCmd()
35+
services.AddCommand(update)
36+
root.AddCommand(services)
37+
38+
plural, _, err := root.Find([]string{"services", "update"})
2639
require.NoError(t, err)
27-
require.Same(t, ServiceUpdateCmd, plural)
40+
require.Same(t, update, plural)
2841

29-
alias, _, err := rootCmd.Find([]string{"service", "update"})
42+
alias, _, err := root.Find([]string{"service", "update"})
3043
require.NoError(t, err)
31-
require.Same(t, ServiceUpdateCmd, alias)
44+
require.Same(t, update, alias)
3245
}
3346

3447
func TestServiceUpdateNoArgsValidationPreventsExecution(t *testing.T) {
48+
update := newServiceUpdateTestCmd()
3549
called := false
3650
cmd := &cobra.Command{
3751
Use: "update",
38-
Args: ServiceUpdateCmd.Args,
52+
Args: update.Args,
3953
RunE: func(_ *cobra.Command, _ []string) error {
4054
called = true
4155
return nil
@@ -49,7 +63,8 @@ func TestServiceUpdateNoArgsValidationPreventsExecution(t *testing.T) {
4963
}
5064

5165
func TestServiceUpdateFlagsRegistration(t *testing.T) {
52-
// Verify key flags are registered
66+
cmd := newServiceUpdateTestCmd()
67+
5368
tests := []struct {
5469
flagName string
5570
}{
@@ -81,24 +96,26 @@ func TestServiceUpdateFlagsRegistration(t *testing.T) {
8196

8297
for _, tt := range tests {
8398
t.Run(tt.flagName, func(t *testing.T) {
84-
flag := ServiceUpdateCmd.Flags().Lookup(tt.flagName)
99+
flag := cmd.Flags().Lookup(tt.flagName)
85100
require.NotNil(t, flag, "flag %s should be registered", tt.flagName)
86101
})
87102
}
88103
}
89104

90105
func TestServiceUpdateCommandStructure(t *testing.T) {
106+
cmd := newServiceUpdateTestCmd()
107+
91108
t.Run("command use string is update <service>", func(t *testing.T) {
92-
require.Equal(t, "update <service>", ServiceUpdateCmd.Use)
109+
require.Equal(t, "update <service>", cmd.Use)
93110
})
94111

95112
t.Run("command requires exactly 1 positional arg", func(t *testing.T) {
96-
require.Error(t, ServiceUpdateCmd.Args(ServiceUpdateCmd, []string{}))
97-
require.NoError(t, ServiceUpdateCmd.Args(ServiceUpdateCmd, []string{"service"}))
98-
require.Error(t, ServiceUpdateCmd.Args(ServiceUpdateCmd, []string{"arg1", "arg2"}))
113+
require.Error(t, cmd.Args(cmd, []string{}))
114+
require.NoError(t, cmd.Args(cmd, []string{"service"}))
115+
require.Error(t, cmd.Args(cmd, []string{"arg1", "arg2"}))
99116
})
100117

101118
t.Run("command has RunE defined", func(t *testing.T) {
102-
require.NotNil(t, ServiceUpdateCmd.RunE)
119+
require.NotNil(t, cmd.RunE)
103120
})
104121
}

0 commit comments

Comments
 (0)