diff --git a/args.go b/args.go index 0accf8fad..32c562728 100644 --- a/args.go +++ b/args.go @@ -58,6 +58,8 @@ const ( ArgNoPrefix = "no-prefix" // ArgAppForceRebuild forces a deployment rebuild ArgAppForceRebuild = "force-rebuild" + // ArgAppComponents is a list of components to restart. + ArgAppComponents = "components" // ArgAppAlertDestinations is a path to an app alert destination file. ArgAppAlertDestinations = "app-alert-destinations" // ArgClusterName is a cluster name argument. diff --git a/commands/apps.go b/commands/apps.go index 8a78ea94f..5be52ca81 100644 --- a/commands/apps.go +++ b/commands/apps.go @@ -127,6 +127,21 @@ This permanently deletes the app and all of its associated deployments.`, AddBoolFlag(deleteApp, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the App without a confirmation prompt") deleteApp.Example = `The following example deletes an app with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + `: doctl apps delete f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + restartApp := CmdBuilder( + cmd, + RunAppsRestart, + "restart ", + "Restarts an app", + `Restarts the specified app or some of its components.`, + Writer, + aliasOpt("r"), + displayerType(&displayers.Deployments{}), + ) + AddStringSliceFlag(restartApp, doctl.ArgAppComponents, "", nil, "The components to restart. If not provided, all components are restarted.") + AddBoolFlag(restartApp, doctl.ArgCommandWait, "", false, + "Boolean that specifies whether to wait for the restart to complete before allowing further terminal input. This can be helpful for scripting.") + restartApp.Example = `The following example restarts an app with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + `. Additionally, the command returns the app's ID and status: doctl apps restart f81d4fae-7dec-11d0-a765-00a0c91e6bf6 --format ID,Status` + deploymentCreate := CmdBuilder( cmd, RunAppsCreateDeployment, @@ -469,6 +484,48 @@ func RunAppsDelete(c *CmdConfig) error { return nil } +// RunAppsRestart restarts an app. +func RunAppsRestart(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + appID := c.Args[0] + components, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAppComponents) + if err != nil { + return err + } + + wait, err := c.Doit.GetBool(c.NS, doctl.ArgCommandWait) + if err != nil { + return err + } + + deployment, err := c.Apps().Restart(appID, components) + if err != nil { + return err + } + + var errs error + + if wait { + apps := c.Apps() + notice("Restart is in progress, waiting for the restart to complete") + err := waitForActiveDeployment(apps, appID, deployment.ID) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("app deployment couldn't enter `running` state: %v", err)) + if err := c.Display(displayers.Deployments{deployment}); err != nil { + errs = multierror.Append(errs, err) + } + return errs + } + deployment, _ = c.Apps().GetDeployment(appID, deployment.ID) + } + + notice("Restarted") + + return c.Display(displayers.Deployments{deployment}) +} + // RunAppsCreateDeployment creates a deployment for an app. func RunAppsCreateDeployment(c *CmdConfig) error { if len(c.Args) < 1 { diff --git a/commands/apps_test.go b/commands/apps_test.go index cea98533c..c4e705a03 100644 --- a/commands/apps_test.go +++ b/commands/apps_test.go @@ -36,6 +36,7 @@ func TestAppsCommand(t *testing.T) { "list-regions", "logs", "propose", + "restart", "spec", "tier", "list-alerts", @@ -344,6 +345,110 @@ func TestRunAppsCreateDeploymentWithWait(t *testing.T) { }) } +func TestRunAppsRestart(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + appID := uuid.New().String() + deployment := &godo.Deployment{ + ID: uuid.New().String(), + Spec: &testAppSpec, + Services: []*godo.DeploymentService{{ + Name: "service", + SourceCommitHash: "commit", + }}, + Cause: "Manual", + Phase: godo.DeploymentPhase_PendingDeploy, + Progress: &godo.DeploymentProgress{ + PendingSteps: 1, + RunningSteps: 0, + SuccessSteps: 0, + ErrorSteps: 0, + TotalSteps: 1, + + Steps: []*godo.DeploymentProgressStep{{ + Name: "name", + Status: "pending", + StartedAt: time.Now(), + }}, + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + tm.apps.EXPECT().Restart(appID, []string{"component1", "component2"}).Times(1).Return(deployment, nil) + + config.Args = append(config.Args, appID) + config.Doit.Set(config.NS, doctl.ArgAppComponents, []string{"component1", "component2"}) + + err := RunAppsRestart(config) + require.NoError(t, err) + }) +} + +func TestRunAppsRestartWithWait(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + appID := uuid.New().String() + deployment := &godo.Deployment{ + ID: uuid.New().String(), + Spec: &testAppSpec, + Services: []*godo.DeploymentService{{ + Name: "service", + SourceCommitHash: "commit", + }}, + Cause: "Manual", + Phase: godo.DeploymentPhase_PendingDeploy, + Progress: &godo.DeploymentProgress{ + PendingSteps: 1, + RunningSteps: 0, + SuccessSteps: 0, + ErrorSteps: 0, + TotalSteps: 1, + + Steps: []*godo.DeploymentProgressStep{{ + Name: "name", + Status: "pending", + StartedAt: time.Now(), + }}, + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + activeDeployment := &godo.Deployment{ + ID: uuid.New().String(), + Spec: &testAppSpec, + Services: []*godo.DeploymentService{{ + Name: "service", + SourceCommitHash: "commit", + }}, + Cause: "Manual", + Phase: godo.DeploymentPhase_Active, + Progress: &godo.DeploymentProgress{ + PendingSteps: 1, + RunningSteps: 0, + SuccessSteps: 1, + ErrorSteps: 0, + TotalSteps: 1, + + Steps: []*godo.DeploymentProgressStep{{ + Name: "name", + Status: "pending", + StartedAt: time.Now(), + }}, + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + tm.apps.EXPECT().Restart(appID, nil).Times(1).Return(deployment, nil) + tm.apps.EXPECT().GetDeployment(appID, deployment.ID).Times(2).Return(activeDeployment, nil) + + config.Args = append(config.Args, appID) + config.Doit.Set(config.NS, doctl.ArgCommandWait, true) + + err := RunAppsRestart(config) + require.NoError(t, err) + }) +} + func TestRunAppsGetDeployment(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { appID := uuid.New().String() diff --git a/do/apps.go b/do/apps.go index 184d2145a..d5bc0a4ad 100644 --- a/do/apps.go +++ b/do/apps.go @@ -28,6 +28,7 @@ type AppsService interface { Delete(appID string) error Propose(req *godo.AppProposeRequest) (*godo.AppProposeResponse, error) + Restart(appID string, components []string) (*godo.Deployment, error) CreateDeployment(appID string, forceRebuild bool) (*godo.Deployment, error) GetDeployment(appID, deploymentID string) (*godo.Deployment, error) ListDeployments(appID string) ([]*godo.Deployment, error) @@ -132,6 +133,16 @@ func (s *appsService) Propose(req *godo.AppProposeRequest) (*godo.AppProposeResp return res, nil } +func (s *appsService) Restart(appID string, components []string) (*godo.Deployment, error) { + deployment, _, err := s.client.Apps.Restart(s.ctx, appID, &godo.AppRestartRequest{ + Components: components, + }) + if err != nil { + return nil, err + } + return deployment, nil +} + func (s *appsService) CreateDeployment(appID string, forceRebuild bool) (*godo.Deployment, error) { deployment, _, err := s.client.Apps.CreateDeployment(s.ctx, appID, &godo.DeploymentCreateRequest{ ForceBuild: forceRebuild, diff --git a/do/mocks/AppsService.go b/do/mocks/AppsService.go index e71d8ef17..f4fdd9852 100644 --- a/do/mocks/AppsService.go +++ b/do/mocks/AppsService.go @@ -294,6 +294,21 @@ func (mr *MockAppsServiceMockRecorder) Propose(req any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Propose", reflect.TypeOf((*MockAppsService)(nil).Propose), req) } +// Restart mocks base method. +func (m *MockAppsService) Restart(appID string, components []string) (*godo.Deployment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Restart", appID, components) + ret0, _ := ret[0].(*godo.Deployment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Restart indicates an expected call of Restart. +func (mr *MockAppsServiceMockRecorder) Restart(appID, components any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restart", reflect.TypeOf((*MockAppsService)(nil).Restart), appID, components) +} + // Update mocks base method. func (m *MockAppsService) Update(appID string, req *godo.AppUpdateRequest) (*godo.App, error) { m.ctrl.T.Helper() diff --git a/go.mod b/go.mod index 324d29da5..cbc99b2f0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/blang/semver v3.5.1+incompatible github.com/creack/pty v1.1.21 - github.com/digitalocean/godo v1.130.0 + github.com/digitalocean/godo v1.130.1-0.20241119155329-45ad288c38bd github.com/docker/cli v24.0.5+incompatible github.com/docker/docker v25.0.6+incompatible github.com/docker/docker-credential-helpers v0.7.0 // indirect diff --git a/go.sum b/go.sum index ad87a3917..987729c07 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/digitalocean/godo v1.130.0 h1:DbJg0wvBxTkYjY5Q9S1mwzAZLd5Wht3r57yFH4yeMCk= -github.com/digitalocean/godo v1.130.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= +github.com/digitalocean/godo v1.130.1-0.20241119155329-45ad288c38bd h1:3TCd+SNAbaRHQSiWmMJWtPitvZt2lTq3th87CxMl9Xo= +github.com/digitalocean/godo v1.130.1-0.20241119155329-45ad288c38bd/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc= diff --git a/vendor/github.com/digitalocean/godo/apps.gen.go b/vendor/github.com/digitalocean/godo/apps.gen.go index 0232ae943..6fc029cb9 100644 --- a/vendor/github.com/digitalocean/godo/apps.gen.go +++ b/vendor/github.com/digitalocean/godo/apps.gen.go @@ -466,8 +466,7 @@ type AppLogDestinationSpecPapertrail struct { type AppMaintenanceSpec struct { // Indicates whether maintenance mode should be enabled for the app. Enabled bool `json:"enabled,omitempty"` - // Indicates whether the app should be archived. Setting this to true implies that enabled is set to true. - // Note that this feature is currently in closed beta. + // Indicates whether the app should be archived. Setting this to true implies that enabled is set to true. Note that this feature is currently in closed beta. Archive bool `json:"archive,omitempty"` } @@ -1004,6 +1003,7 @@ const ( DeploymentCauseDetailsDigitalOceanUserActionName_RollbackApp DeploymentCauseDetailsDigitalOceanUserActionName = "ROLLBACK_APP" DeploymentCauseDetailsDigitalOceanUserActionName_RevertAppRollback DeploymentCauseDetailsDigitalOceanUserActionName = "REVERT_APP_ROLLBACK" DeploymentCauseDetailsDigitalOceanUserActionName_UpgradeBuildpack DeploymentCauseDetailsDigitalOceanUserActionName = "UPGRADE_BUILDPACK" + DeploymentCauseDetailsDigitalOceanUserActionName_Restart DeploymentCauseDetailsDigitalOceanUserActionName = "RESTART" ) // AppDomain struct for AppDomain diff --git a/vendor/github.com/digitalocean/godo/apps.go b/vendor/github.com/digitalocean/godo/apps.go index 24fe738e4..97b0cbd73 100644 --- a/vendor/github.com/digitalocean/godo/apps.go +++ b/vendor/github.com/digitalocean/godo/apps.go @@ -35,6 +35,7 @@ type AppsService interface { Delete(ctx context.Context, appID string) (*Response, error) Propose(ctx context.Context, propose *AppProposeRequest) (*AppProposeResponse, *Response, error) + Restart(ctx context.Context, appID string, opts *AppRestartRequest) (*Deployment, *Response, error) GetDeployment(ctx context.Context, appID, deploymentID string) (*Deployment, *Response, error) ListDeployments(ctx context.Context, appID string, opts *ListOptions) ([]*Deployment, *Response, error) CreateDeployment(ctx context.Context, appID string, create ...*DeploymentCreateRequest) (*Deployment, *Response, error) @@ -95,6 +96,11 @@ type DeploymentCreateRequest struct { ForceBuild bool `json:"force_build"` } +// AppRestartRequest represents a request to restart an app. +type AppRestartRequest struct { + Components []string `json:"components"` +} + // AlertDestinationUpdateRequest represents a request to update alert destinations. type AlertDestinationUpdateRequest struct { Emails []string `json:"emails"` @@ -285,6 +291,22 @@ func (s *AppsServiceOp) Propose(ctx context.Context, propose *AppProposeRequest) return res, resp, nil } +// Restart restarts an app. +func (s *AppsServiceOp) Restart(ctx context.Context, appID string, opts *AppRestartRequest) (*Deployment, *Response, error) { + path := fmt.Sprintf("%s/%s/restart", appsBasePath, appID) + + req, err := s.client.NewRequest(ctx, http.MethodPost, path, opts) + if err != nil { + return nil, nil, err + } + root := new(deploymentRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Deployment, resp, nil +} + // GetDeployment gets an app deployment. func (s *AppsServiceOp) GetDeployment(ctx context.Context, appID, deploymentID string) (*Deployment, *Response, error) { path := fmt.Sprintf("%s/%s/deployments/%s", appsBasePath, appID, deploymentID) diff --git a/vendor/modules.txt b/vendor/modules.txt index 73832a0dd..98a5ad39e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -61,7 +61,7 @@ github.com/creack/pty # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew -# github.com/digitalocean/godo v1.130.0 +# github.com/digitalocean/godo v1.130.1-0.20241119155329-45ad288c38bd ## explicit; go 1.22 github.com/digitalocean/godo github.com/digitalocean/godo/metrics