Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,83 @@ func (a *App) ResizeProcess(processType string, cpu, memory int) error {
return a.SetScaleParameter(processType, nil, nil, &cpu, &memory)
}

// RestartProcess restarts the ECS service or tasks for the given processType.
// When force is false, it triggers a graceful rolling restart via UpdateService with
// ForceNewDeployment. When force is true, it stops all running tasks matching the
// processType tag, causing ECS to relaunch them.
func (a *App) RestartProcess(processType string, force bool) error {
if err := a.LoadSettings(); err != nil {
return fmt.Errorf("loading settings: %w", err)
}

ecsSvc := ecs.NewFromConfig(a.Session)

if !force {
serviceName := a.ServiceName(processType)
logrus.WithFields(logrus.Fields{"service": serviceName}).Debug("triggering rolling restart")

_, err := ecsSvc.UpdateService(context.Background(), &ecs.UpdateServiceInput{
Cluster: &a.Settings.Cluster.ARN,
Service: aws.String(serviceName),
ForceNewDeployment: true,
})
if err != nil {
return fmt.Errorf("updating service %s: %w", serviceName, err)
}

return nil
}

// Force restart: stop all matching tasks so ECS relaunches them.
tasks, err := a.DescribeTasks()
if err != nil {
return fmt.Errorf("describing tasks: %w", err)
}

var matchingARNs []string
for i := range tasks {
tagVal, tagErr := getTagFromTask(&tasks[i], "apppack:processType")
if tagErr != nil {
continue
}

if tagVal == processType {
matchingARNs = append(matchingARNs, *tasks[i].TaskArn)
}
}

if len(matchingARNs) == 0 {
return fmt.Errorf("no running tasks found for process type %q", processType)
}

for _, taskARN := range matchingARNs {
arn := taskARN // capture loop variable
logrus.WithFields(logrus.Fields{"task": arn}).Debug("stopping task")

_, err := ecsSvc.StopTask(context.Background(), &ecs.StopTaskInput{
Cluster: &a.Settings.Cluster.ARN,
Task: &arn,
Reason: aws.String("apppack ps restart --force"),
})
if err != nil {
return fmt.Errorf("stopping task %s: %w", arn, err)
}
}

return nil
}

// getTagFromTask returns the value of the named tag on an ECS task.
func getTagFromTask(task *ecstypes.Task, key string) (string, error) {
for _, t := range task.Tags {
if t.Key != nil && *t.Key == key && t.Value != nil {
return *t.Value, nil
}
}

return "", fmt.Errorf("tag %s not found on task", key)
}

func (a *App) ScaleProcess(processType string, minProcessCount, maxProcessCount int) error {
return a.SetScaleParameter(processType, &minProcessCount, &maxProcessCount, nil, nil)
}
Expand Down
51 changes: 49 additions & 2 deletions cmd/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/apppackio/apppack/app"
"github.com/apppackio/apppack/ui"
ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/aws/smithy-go"
"github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -309,6 +310,47 @@ apppack -a my-app ps scale worker 1-4 # autoscale worker service from 1 to 4 pr
},
}

// psRestartCmd represents the restart command
var psRestartCmd = &cobra.Command{
Use: "restart <process_type>",
Short: "restart the process for a given type",
Long: `Restart the ECS service for a given process type.

By default, a graceful rolling restart is performed (ForceNewDeployment). Use
--force to immediately stop all running containers for the process type; ECS
will relaunch them automatically.`,
Example: `apppack -a my-app ps restart web # graceful rolling restart
apppack -a my-app ps restart web --force # kill running containers (forced restart)`,
DisableFlagsInUseLine: true,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
processType := args[0]
ui.StartSpinner()
a, err := app.Init(AppName, UseAWSCredentials, SessionDurationSeconds)
checkErr(err)
if a.Pipeline && !a.IsReviewApp() {
checkErr(errors.New("pipelines don't directly run processes"))
}
err = a.RestartProcess(processType, psRestartForce)
ui.Spinner.Stop()
if err != nil {
// Older app stacks (created before the WebOperatorRole gained
// ecs:UpdateService/ecs:StopTask) will get AccessDenied. Point the
// user at the upgrade that grants the required permissions.
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "AccessDeniedException" {
printWarning(fmt.Sprintf("access denied -- the app stack may need to be upgraded: `apppack upgrade app %s`", AppName))
}
checkErr(err)
}
if psRestartForce {
printSuccess(fmt.Sprintf("forcefully restarted %s (running containers stopped; ECS will relaunch them)", processType))
} else {
printSuccess(fmt.Sprintf("triggered rolling restart of %s", processType))
}
},
}

// execCmd represents the exec command
var psExecCmd = &cobra.Command{
Use: "exec -- <command>",
Expand All @@ -325,8 +367,9 @@ var psExecCmd = &cobra.Command{
}

var (
scaleCPU float64
scaleMemory string
scaleCPU float64
scaleMemory string
psRestartForce bool
)

func init() {
Expand All @@ -342,6 +385,10 @@ func init() {
_ = psResizeCmd.MarkFlagRequired("memory")

psCmd.AddCommand(psScaleCmd)

psCmd.AddCommand(psRestartCmd)
psRestartCmd.Flags().BoolVar(&psRestartForce, "force", false, "forcefully restart by killing running containers instead of a graceful rolling restart")

psCmd.AddCommand(psExecCmd)
psExecCmd.PersistentFlags().BoolVarP(&shellRoot, "root", "r", false, "open shell as root user")
psExecCmd.PersistentFlags().BoolVarP(&shellLive, "live", "l", false, "connect to a live process")
Expand Down
Loading