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 }