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

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"text/template"
     6  
     7  	"github.com/aws/aws-sdk-go/aws"
     8  	"github.com/aws/aws-sdk-go/aws/awserr"
     9  	"github.com/aws/aws-sdk-go/service/ecs"
    10  	"github.com/hazelops/ize/internal/config"
    11  	"github.com/hazelops/ize/internal/requirements"
    12  	"github.com/hazelops/ize/pkg/ssmsession"
    13  	"github.com/pterm/pterm"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  type ConsoleOptions struct {
    19  	Config        *config.Project
    20  	AppName       string
    21  	EcsCluster    string
    22  	Task          string
    23  	CustomPrompt  bool
    24  	ContainerName string
    25  	Explain       bool
    26  }
    27  
    28  var explainConsoleTmpl = `
    29  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)
    30  
    31  aws ecs execute-command  \
    32      --interactive \
    33      --region {{.AwsRegion}} \
    34      --cluster {{.Env}}-{{.Namespace}} \
    35      --task $TASK_ID \
    36      --container {{svc}} \
    37      --command "/bin/sh"
    38  `
    39  
    40  func NewConsoleFlags(project *config.Project) *ConsoleOptions {
    41  	return &ConsoleOptions{
    42  		Config: project,
    43  	}
    44  }
    45  
    46  func NewCmdConsole(project *config.Project) *cobra.Command {
    47  	o := NewConsoleFlags(project)
    48  
    49  	cmd := &cobra.Command{
    50  		Use:               "console [app-name]",
    51  		Short:             "Connect to a container in the ECS",
    52  		Long:              "Connect to a container of the app via AWS SSM.\nTakes app name that is running on ECS as an argument",
    53  		Args:              cobra.MinimumNArgs(1),
    54  		ValidArgsFunction: config.GetApps,
    55  		RunE: func(cmd *cobra.Command, args []string) error {
    56  			cmd.SilenceUsage = true
    57  			err := o.Complete(cmd)
    58  			if err != nil {
    59  				return err
    60  			}
    61  
    62  			err = o.Validate()
    63  			if err != nil {
    64  				return err
    65  			}
    66  
    67  			err = o.Run()
    68  			if err != nil {
    69  				return err
    70  			}
    71  
    72  			return nil
    73  		},
    74  	}
    75  
    76  	cmd.Flags().StringVar(&o.EcsCluster, "ecs-cluster", "", "set ECS cluster name")
    77  	cmd.Flags().StringVar(&o.ContainerName, "container-name", "", "set container name")
    78  	cmd.Flags().StringVar(&o.Task, "task", "", "set task id")
    79  	cmd.Flags().BoolVar(&o.Explain, "explain", false, "bash alternative shown")
    80  	cmd.Flags().BoolVar(&o.CustomPrompt, "custom-prompt", false, "enable custom prompt in the console")
    81  
    82  	return cmd
    83  }
    84  
    85  func (o *ConsoleOptions) Complete(cmd *cobra.Command) error {
    86  	if err := requirements.CheckRequirements(requirements.WithSSMPlugin()); err != nil {
    87  		return err
    88  	}
    89  
    90  	if o.EcsCluster == "" {
    91  		o.EcsCluster = fmt.Sprintf("%s-%s", o.Config.Env, o.Config.Namespace)
    92  	}
    93  
    94  	if !o.CustomPrompt {
    95  		o.CustomPrompt = o.Config.CustomPrompt
    96  	}
    97  
    98  	o.AppName = cmd.Flags().Args()[0]
    99  
   100  	if len(o.ContainerName) == 0 {
   101  		o.ContainerName = o.AppName
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  func (o *ConsoleOptions) Validate() error {
   108  	if len(o.AppName) == 0 {
   109  		return fmt.Errorf("can't validate: app name must be specified")
   110  	}
   111  
   112  	return nil
   113  }
   114  
   115  func (o *ConsoleOptions) Run() error {
   116  	appName := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName)
   117  
   118  	if o.Explain {
   119  		err := o.Config.Generate(explainConsoleTmpl, template.FuncMap{
   120  			"svc": func() string {
   121  				return o.AppName
   122  			},
   123  		})
   124  		if err != nil {
   125  			return err
   126  		}
   127  
   128  		return nil
   129  	}
   130  
   131  	logrus.Infof("app name: %s, cluster name: %s", appName, o.EcsCluster)
   132  	logrus.Infof("region: %s, profile: %s", o.Config.AwsProfile, o.Config.AwsRegion)
   133  
   134  	s, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start("Getting access to container...")
   135  
   136  	if o.Task == "" {
   137  		lto, err := o.Config.AWSClient.ECSClient.ListTasks(&ecs.ListTasksInput{
   138  			Cluster:       &o.EcsCluster,
   139  			DesiredStatus: aws.String(ecs.DesiredStatusRunning),
   140  			ServiceName:   &appName,
   141  		})
   142  		if aerr, ok := err.(awserr.Error); ok {
   143  			switch aerr.Code() {
   144  			case "ClusterNotFoundException":
   145  				return fmt.Errorf("ECS cluster %s not found", o.EcsCluster)
   146  			default:
   147  				return err
   148  			}
   149  		}
   150  
   151  		logrus.Debugf("list task output: %s", lto)
   152  
   153  		if len(lto.TaskArns) == 0 {
   154  			return fmt.Errorf("running task not found")
   155  		}
   156  
   157  		o.Task = *lto.TaskArns[0]
   158  	}
   159  
   160  	s.UpdateText("Executing command...")
   161  	consoleCommand := `/bin/sh`
   162  
   163  	if o.CustomPrompt {
   164  		// This is ASCII Prompt string with colors. See https://dev.to/ifenna__/adding-colors-to-bash-scripts-48g4 for reference
   165  		// TODO: Make this customizable via a config
   166  		promptString := fmt.Sprintf(`\e[1;35m★\e[0m $ENV-$APP_NAME\n\e[1;33m\e[0m \w \e[1;34m❯\e[0m `)
   167  		consoleCommand = fmt.Sprintf(`/bin/sh -c '$(echo "export PS1=\"%s\"" > /etc/profile.d/ize.sh) /bin/bash --rcfile /etc/profile'`, promptString)
   168  	}
   169  
   170  	out, err := o.Config.AWSClient.ECSClient.ExecuteCommand(&ecs.ExecuteCommandInput{
   171  		Container:   &o.ContainerName,
   172  		Interactive: aws.Bool(true),
   173  		Cluster:     &o.EcsCluster,
   174  		Task:        &o.Task,
   175  		Command:     aws.String(consoleCommand),
   176  	})
   177  	if aerr, ok := err.(awserr.Error); ok {
   178  		switch aerr.Code() {
   179  		case "ClusterNotFoundException":
   180  			return fmt.Errorf("ECS cluster %s not found", o.EcsCluster)
   181  		default:
   182  			return err
   183  		}
   184  	}
   185  
   186  	s.Success()
   187  
   188  	ssmCmd := ssmsession.NewSSMPluginCommand(o.Config.AwsRegion)
   189  	err = ssmCmd.StartInteractive(out.Session)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	return nil
   195  }