github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/internal/commands/start.go (about) 1 package commands 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 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/aws/aws-sdk-go/service/ssm" 12 "github.com/aws/aws-sdk-go/service/ssm/ssmiface" 13 "github.com/hazelops/ize/internal/config" 14 "github.com/hazelops/ize/internal/requirements" 15 "github.com/hazelops/ize/pkg/templates" 16 "github.com/pterm/pterm" 17 "github.com/sirupsen/logrus" 18 "github.com/spf13/cobra" 19 "os" 20 "os/signal" 21 "strings" 22 "syscall" 23 ) 24 25 type StartOptions struct { 26 Config *config.Project 27 AppName string 28 EcsCluster string 29 } 30 31 type NetworkConfiguration struct { 32 SecurityGroups struct { 33 Value string `json:"value"` 34 } `json:"security_groups"` 35 Subnets struct { 36 Value [][]string `json:"value"` 37 } `json:"subnets"` 38 VpcPrivateSubnets struct { 39 Value []string `json:"value"` 40 } `json:"vpc_private_subnets"` 41 VpcPublicSubnets struct { 42 Value []string `json:"value"` 43 } `json:"vpc_public_subnets"` 44 } 45 46 var startExample = templates.Examples(` 47 # Connect to a container in the ECS via AWS SSM and run command. 48 ize start goblin 49 `) 50 51 func NewStartFlags(project *config.Project) *StartOptions { 52 return &StartOptions{ 53 Config: project, 54 } 55 } 56 57 func NewCmdStart(project *config.Project) *cobra.Command { 58 o := NewStartFlags(project) 59 60 cmd := &cobra.Command{ 61 Use: "start [app-name]", 62 Example: startExample, 63 Short: "Start ECS task", 64 Long: "Start ECS task and stream logs until it dies or canceled.\nIt uses app name as an argument.", 65 Args: cobra.MinimumNArgs(1), 66 ValidArgsFunction: config.GetApps, 67 RunE: func(cmd *cobra.Command, args []string) error { 68 cmd.SilenceUsage = true 69 err := o.Complete(cmd) 70 if err != nil { 71 return err 72 } 73 74 err = o.Validate() 75 if err != nil { 76 return err 77 } 78 79 err = o.Run() 80 if err != nil { 81 return err 82 } 83 84 return nil 85 }, 86 } 87 88 cmd.Flags().StringVar(&o.EcsCluster, "ecs-cluster", "", "set ECS cluster name") 89 90 return cmd 91 } 92 93 func (o *StartOptions) Complete(cmd *cobra.Command) 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 return nil 105 } 106 107 func (o *StartOptions) 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 getNetworkConfiguration(svc ssmiface.SSMAPI, env string) (NetworkConfiguration, error) { 116 resp, err := svc.GetParameter(&ssm.GetParameterInput{ 117 Name: aws.String(fmt.Sprintf("/%s/terraform-output", env)), 118 WithDecryption: aws.Bool(true), 119 }) 120 if err != nil { 121 return NetworkConfiguration{}, fmt.Errorf("can't get terraform output: %w", err) 122 } 123 124 var value []byte 125 126 value, err = base64.StdEncoding.DecodeString(*resp.Parameter.Value) 127 if err != nil { 128 return NetworkConfiguration{}, fmt.Errorf("can't get terraform output: %w", err) 129 } 130 131 var output NetworkConfiguration 132 133 err = json.Unmarshal(value, &output) 134 if err != nil { 135 return NetworkConfiguration{}, fmt.Errorf("can't get network configuration: %w", err) 136 } 137 138 return output, nil 139 } 140 141 func (o *StartOptions) Run() error { 142 ctx := context.Background() 143 144 appName := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName) 145 logGroup := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName) 146 147 logrus.Debugf("app name: %s, cluster name: %s", appName, o.EcsCluster) 148 logrus.Debugf("region: %s, profile: %s", o.Config.AwsProfile, o.Config.AwsRegion) 149 150 configuration, err := getNetworkConfiguration(o.Config.AWSClient.SSMClient, o.Config.Env) 151 if err != nil { 152 return err 153 } 154 155 logrus.Debugf("network configuration: %+v", configuration) 156 157 if len(configuration.VpcPrivateSubnets.Value) == 0 { 158 return fmt.Errorf("output private_subnets is missing. Please add it to your Terraform") 159 } 160 161 if len(configuration.SecurityGroups.Value) == 0 { 162 return fmt.Errorf("output security_groups is missing. Please add it to your Terraform") 163 } 164 165 out, err := o.Config.AWSClient.ECSClient.RunTaskWithContext(ctx, &ecs.RunTaskInput{ 166 TaskDefinition: &appName, 167 StartedBy: aws.String("IZE"), 168 Cluster: &o.EcsCluster, 169 NetworkConfiguration: &ecs.NetworkConfiguration{AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ 170 Subnets: aws.StringSlice(configuration.VpcPrivateSubnets.Value), 171 }}, 172 LaunchType: aws.String(ecs.LaunchTypeFargate), 173 }) 174 if aerr, ok := err.(awserr.Error); ok { 175 switch aerr.Code() { 176 case "ClusterNotFoundException": 177 return fmt.Errorf("ECS cluster %s not found", o.EcsCluster) 178 default: 179 return err 180 } 181 } 182 183 taskID := getTaskID(*out.Tasks[0].TaskArn) 184 185 c := make(chan os.Signal) 186 ch := make(chan bool) 187 errorChannel := make(chan error) 188 signal.Notify(c, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) 189 190 go func() { 191 s, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start(fmt.Sprintf("Please wait until task %s running...", appName)) 192 193 err := o.Config.AWSClient.ECSClient.WaitUntilTasksRunningWithContext(ctx, &ecs.DescribeTasksInput{ 194 Cluster: &o.EcsCluster, 195 Tasks: aws.StringSlice([]string{*out.Tasks[0].TaskArn}), 196 }) 197 if err != nil { 198 errorChannel <- err 199 } 200 201 s.Success() 202 pterm.DefaultSection.Println("Logs:") 203 204 var token *string 205 go GetLogs(o.Config.AWSClient.CloudWatchLogsClient, logGroup, fmt.Sprintf("main/%s/%s", o.AppName, taskID), token) 206 err = o.Config.AWSClient.ECSClient.WaitUntilTasksStoppedWithContext(ctx, &ecs.DescribeTasksInput{ 207 Cluster: &o.EcsCluster, 208 Tasks: aws.StringSlice([]string{*out.Tasks[0].TaskArn}), 209 }) 210 if err != nil { 211 errorChannel <- err 212 } 213 ch <- true 214 }() 215 216 select { 217 case <-c: 218 fmt.Print("\r") 219 _, err := o.Config.AWSClient.ECSClient.StopTask(&ecs.StopTaskInput{ 220 Cluster: &o.EcsCluster, 221 Reason: aws.String("Task stopped by IZE"), 222 Task: out.Tasks[0].TaskArn, 223 }) 224 if err != nil { 225 return err 226 } 227 pterm.Success.Printfln("Stop task %s by interrupt", appName) 228 case <-ch: 229 tasks, err := o.Config.AWSClient.ECSClient.DescribeTasks(&ecs.DescribeTasksInput{ 230 Cluster: &o.EcsCluster, 231 Tasks: aws.StringSlice([]string{taskID}), 232 }) 233 if err != nil { 234 return err 235 } 236 237 sr := *tasks.Tasks[0].StoppedReason 238 st := *tasks.Tasks[0].StopCode 239 logrus.Debugf("stop code: %s", st) 240 pterm.Success.Printfln("%s was stopped with reason: %s\n", appName, sr) 241 return nil 242 case err := <-errorChannel: 243 return err 244 } 245 246 return nil 247 } 248 249 func getTaskID(taskArn string) string { 250 split := strings.Split(taskArn, "/") 251 return split[len(split)-1] 252 }