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  }