From 9f90c0cf26e684be5a0cff300298e75958b78945 Mon Sep 17 00:00:00 2001 From: Peter Baumgartner Date: Fri, 22 Jul 2022 16:45:38 -0600 Subject: [PATCH] Adds `ps restart` command --- app/app.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++++----- cmd/ps.go | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/app/app.go b/app/app.go index ed1d9bd..ace53ad 100644 --- a/app/app.go +++ b/app/app.go @@ -11,6 +11,7 @@ import ( "time" "github.com/apppackio/apppack/auth" + "github.com/apppackio/apppack/stringslice" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/request" @@ -254,14 +255,16 @@ func (a *App) ReviewAppSettings() (*Settings, error) { return &i.Settings, nil } -// TaskDefinition gets the Task Definition for a specific task type -func (a *App) TaskDefinition(name string) (*ecs.TaskDefinition, error) { - var family string +func (a *App) ServiceName(processType string) string { if a.IsReviewApp() { - family = fmt.Sprintf("%s-pr%s-%s", a.Name, *a.ReviewApp, name) - } else { - family = fmt.Sprintf("%s-%s", a.Name, name) + return fmt.Sprintf("%s-pr%s-%s", a.Name, *a.ReviewApp, processType) } + return fmt.Sprintf("%s-%s", a.Name, processType) +} + +// TaskDefinition gets the Task Definition for a specific task type +func (a *App) TaskDefinition(name string) (*ecs.TaskDefinition, error) { + family := a.ServiceName(name) ecsSvc := ecs.New(a.Session) // verify task exists task, err := ecsSvc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ @@ -863,6 +866,65 @@ func (a *App) ScaleProcess(processType string, minProcessCount, maxProcessCount return a.SetScaleParameter(processType, &minProcessCount, &maxProcessCount, nil, nil) } +func (a *App) ReplaceProcess(processType string) error { + err := a.LoadSettings() + if err != nil { + return err + } + ecsSvc := ecs.New(a.Session) + service := a.ServiceName(processType) + _, err = ecsSvc.UpdateService(&ecs.UpdateServiceInput{ + Cluster: &a.Settings.Cluster.ARN, + Service: &service, + ForceNewDeployment: aws.Bool(true), + }) + return err +} + +func (a *App) StopProcesses(processType string) ([]*string, error) { + err := a.LoadSettings() + if err != nil { + return nil, err + } + ecsSvc := ecs.New(a.Session) + taskArnList, err := ecsSvc.ListTasks(&ecs.ListTasksInput{ + Cluster: &a.Settings.Cluster.ARN, + ServiceName: aws.String(a.ServiceName(processType)), + }) + if err != nil { + return nil, err + } + if len(taskArnList.TaskArns) == 0 { + return nil, fmt.Errorf("no processes found for %s", processType) + } + tasks, err := ecsSvc.DescribeTasks(&ecs.DescribeTasksInput{ + Cluster: &a.Settings.Cluster.ARN, + Tasks: taskArnList.TaskArns, + }) + if err != nil { + return nil, err + } + stoppedTasks := []*string{} + stoppingStatuses := []string{"DEACTIVATING", "STOPPING", "DEPROVISIONING", "STOPPED"} + for _, task := range tasks.Tasks { + if stringslice.Contains(*task.LastStatus, stoppingStatuses) { + continue + } + _, err = ecsSvc.StopTask(&ecs.StopTaskInput{ + Cluster: task.ClusterArn, + Task: task.TaskArn, + }) + if err != nil { + return nil, err + } + stoppedTasks = append(stoppedTasks, task.TaskArn) + } + if len(stoppedTasks) == 0 { + return nil, fmt.Errorf("all processes already stopped or stopping for %s", processType) + } + return stoppedTasks, nil +} + // SetScaleParameter updates process count and cpu/ram with any non-nil values provided // if it is not yet set, the defaults from ECSConfig will be used func (a *App) SetScaleParameter(processType string, minProcessCount, maxProcessCount, cpu, memory *int) error { diff --git a/cmd/ps.go b/cmd/ps.go index 3d0e8c5..d5fd297 100644 --- a/cmd/ps.go +++ b/cmd/ps.go @@ -23,6 +23,7 @@ import ( "github.com/apppackio/apppack/app" "github.com/apppackio/apppack/ui" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ecs" "github.com/dustin/go-humanize" "github.com/logrusorgru/aurora" @@ -236,8 +237,50 @@ Requires installation of Amazon's SSM Session Manager. https://docs.aws.amazon.c }, } +// psRestartCmd represents the restart command +var psRestartCmd = &cobra.Command{ + Use: "restart ", + Short: "restart the process(es) for a given service", + DisableFlagsInUseLine: true, + Example: "apppack -a my-app ps restart web", + Long: "Performs a zero-downtime rolling replacement of all the processes for the service. This should not be necessary during normal operation (config changes and deployments do this automatically. If `-f/--force` is used, all current processes will be killed immediately, resulting in some downtime before the replacements startup.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + processType := args[0] + a, err := app.Init(AppName, UseAWSCredentials, SessionDurationSeconds) + checkErr(err) + if a.Pipeline { + checkErr(fmt.Errorf("cannot restart a pipeline -- choose a specific review app instead")) + } + if forceRestart { + tasks, err := a.StopProcesses(processType) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if aerr.Code() == ecs.ErrCodeAccessDeniedException { + printWarning(fmt.Sprintf("access denied: you may need to upgrade the app stack: `apppack upgrade app %s`", a.Name)) + } + } + checkErr(err) + } + printSuccess(fmt.Sprintf("forcing restart of %d %s processes", len(tasks), processType)) + } else { + err = a.ReplaceProcess(processType) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if aerr.Code() == ecs.ErrCodeAccessDeniedException { + printWarning(fmt.Sprintf("access denied: you may need to upgrade the app stack: `apppack upgrade app %s`", a.Name)) + } + } + checkErr(err) + } + printSuccess(fmt.Sprintf("replacing %s processes", processType)) + } + }, +} + var scaleCPU float64 var scaleMemory string +var forceRestart bool func init() { rootCmd.AddCommand(psCmd) @@ -255,4 +298,7 @@ func init() { psExecCmd.PersistentFlags().BoolVarP(&shellLive, "live", "l", false, "connect to a live process") psExecCmd.Flags().Float64Var(&shellCpu, "cpu", 0.5, "CPU cores available for task") psExecCmd.Flags().StringVar(&shellMem, "memory", "1G", "memory (e.g. '2G', '512M') available for task") + + psCmd.AddCommand(psRestartCmd) + psRestartCmd.Flags().BoolVar(&forceRestart, "force", false, "force restart of all processes") }