github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/internal/commands/exec.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"text/template"
     7  
     8  	"github.com/aws/aws-sdk-go/aws"
     9  	"github.com/aws/aws-sdk-go/aws/awserr"
    10  	"github.com/aws/aws-sdk-go/service/ecs"
    11  	"github.com/hazelops/ize/internal/config"
    12  	"github.com/hazelops/ize/internal/requirements"
    13  	"github.com/hazelops/ize/pkg/ssmsession"
    14  	"github.com/hazelops/ize/pkg/templates"
    15  	"github.com/pterm/pterm"
    16  	"github.com/sirupsen/logrus"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  type ExecOptions struct {
    21  	Config        *config.Project
    22  	AppName       string
    23  	EcsCluster    string
    24  	Command       []string
    25  	Task          string
    26  	ContainerName string
    27  	Explain       bool
    28  }
    29  
    30  var explainExecTmpl = `
    31  TASK_ID=$(aws ecs list-tasks --cluster {{.Env}}-{{.Namespace}} --service-name {{.Env}}-{{svc}} --desired-status "RUNNING" | jq -r '.taskArns[]' | cut -d'/' -f3 | head -n 1)
    32  
    33  aws ecs execute-command  \
    34      --interactive \
    35      --region {{.AwsRegion}} \
    36      --cluster {{.Env}}-{{.Namespace}} \
    37      --task $TASK_ID \
    38      --container {{svc}} \
    39      --command {{command}}
    40  `
    41  
    42  var execExample = templates.Examples(`
    43  	# Connect to a container in the ECS via AWS SSM and run command.
    44  	ize exec goblin ps aux
    45  `)
    46  
    47  func NewExecFlags(project *config.Project) *ExecOptions {
    48  	return &ExecOptions{
    49  		Config: project,
    50  	}
    51  }
    52  
    53  func NewCmdExec(project *config.Project) *cobra.Command {
    54  	o := NewExecFlags(project)
    55  
    56  	cmd := &cobra.Command{
    57  		Use:               "exec [app-name] -- [commands]",
    58  		Example:           execExample,
    59  		Short:             "Execute command in ECS container",
    60  		Long:              "Connect to a container in the ECS via AWS SSM and run command.\nIt uses app name as an argument.",
    61  		Args:              cobra.MinimumNArgs(1),
    62  		ValidArgsFunction: config.GetApps,
    63  		RunE: func(cmd *cobra.Command, args []string) error {
    64  			cmd.SilenceUsage = true
    65  			argsLenAtDash := cmd.ArgsLenAtDash()
    66  			err := o.Complete(cmd, args, argsLenAtDash)
    67  			if err != nil {
    68  				return err
    69  			}
    70  
    71  			err = o.Validate()
    72  			if err != nil {
    73  				return err
    74  			}
    75  
    76  			err = o.Run()
    77  			if err != nil {
    78  				return err
    79  			}
    80  
    81  			return nil
    82  		},
    83  	}
    84  
    85  	cmd.Flags().StringVar(&o.EcsCluster, "ecs-cluster", "", "set ECS cluster name")
    86  	cmd.Flags().StringVar(&o.Task, "task", "", "set task id")
    87  	cmd.Flags().StringVar(&o.ContainerName, "container-name", "", "set container name")
    88  	cmd.Flags().BoolVar(&o.Explain, "explain", false, "bash alternative shown")
    89  
    90  	return cmd
    91  }
    92  
    93  func (o *ExecOptions) Complete(cmd *cobra.Command, args []string, argsLenAtDash int) error {
    94  	if err := requirements.CheckRequirements(requirements.WithSSMPlugin()); err != nil {
    95  		return err
    96  	}
    97  
    98  	if o.EcsCluster == "" {
    99  		o.EcsCluster = fmt.Sprintf("%s-%s", o.Config.Env, o.Config.Namespace)
   100  	}
   101  
   102  	o.AppName = cmd.Flags().Args()[0]
   103  
   104  	if len(o.ContainerName) == 0 {
   105  		o.ContainerName = o.AppName
   106  	}
   107  
   108  	if argsLenAtDash > -1 {
   109  		o.Command = args[argsLenAtDash:]
   110  	}
   111  
   112  	return nil
   113  }
   114  
   115  func (o *ExecOptions) Validate() error {
   116  	if len(o.AppName) == 0 {
   117  		return fmt.Errorf("can't validate: app name must be specified")
   118  	}
   119  
   120  	if len(o.Command) == 0 {
   121  		return fmt.Errorf("can't validate: you must specify at least one command for the container")
   122  	}
   123  
   124  	return nil
   125  }
   126  
   127  func (o *ExecOptions) Run() error {
   128  	appName := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName)
   129  
   130  	if o.Explain {
   131  		err := o.Config.Generate(explainExecTmpl, template.FuncMap{
   132  			"svc": func() string {
   133  				return o.AppName
   134  			},
   135  			"command": func() string {
   136  				return strings.Join(o.Command, " ")
   137  			},
   138  		})
   139  		if err != nil {
   140  			return err
   141  		}
   142  
   143  		return nil
   144  	}
   145  
   146  	logrus.Infof("app name: %s, cluster name: %s", appName, o.EcsCluster)
   147  	logrus.Infof("region: %s, profile: %s", o.Config.AwsProfile, o.Config.AwsRegion)
   148  
   149  	s, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start("Getting access to container...")
   150  
   151  	if o.Task == "" {
   152  		lto, err := o.Config.AWSClient.ECSClient.ListTasks(&ecs.ListTasksInput{
   153  			Cluster:       &o.EcsCluster,
   154  			DesiredStatus: aws.String(ecs.DesiredStatusRunning),
   155  			ServiceName:   &appName,
   156  		})
   157  		if aerr, ok := err.(awserr.Error); ok {
   158  			switch aerr.Code() {
   159  			case "ClusterNotFoundException":
   160  				return fmt.Errorf("ECS cluster %s not found", o.EcsCluster)
   161  			default:
   162  				return err
   163  			}
   164  		}
   165  
   166  		logrus.Debugf("list task output: %s", lto)
   167  
   168  		if len(lto.TaskArns) == 0 {
   169  			return fmt.Errorf("running task not found")
   170  		}
   171  
   172  		o.Task = *lto.TaskArns[0]
   173  	}
   174  
   175  	s.UpdateText("Executing command...")
   176  
   177  	out, err := o.Config.AWSClient.ECSClient.ExecuteCommand(&ecs.ExecuteCommandInput{
   178  		Container:   &o.AppName,
   179  		Interactive: aws.Bool(true),
   180  		Cluster:     &o.EcsCluster,
   181  		Task:        &o.Task,
   182  		Command:     aws.String(strings.Join(o.Command, " ")),
   183  	})
   184  	if aerr, ok := err.(awserr.Error); ok {
   185  		switch aerr.Code() {
   186  		case "ClusterNotFoundException":
   187  			return fmt.Errorf("ECS cluster %s not found", o.EcsCluster)
   188  		default:
   189  			return err
   190  		}
   191  	}
   192  
   193  	s.Success()
   194  
   195  	ssmCmd := ssmsession.NewSSMPluginCommand(o.Config.AwsRegion)
   196  	err = ssmCmd.Start(out.Session)
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	return nil
   202  }