Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IZE-592 IZE-665 start ecs task #499

Merged
merged 3 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/commands/ize.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func newRootCmd(project *config.Project) *cobra.Command {
NewCmdInit(),
NewCmdTunnel(project),
NewCmdExec(project),
NewCmdStart(project),
NewCmdConfig(),
NewCmdLogs(project),
NewDebugCmd(project),
Expand Down
9 changes: 8 additions & 1 deletion internal/commands/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"fmt"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"os"
"strings"
"time"
Expand Down Expand Up @@ -107,8 +108,14 @@ func (o *LogsOptions) Run() error {
var token *string
logStreamName := fmt.Sprintf("main/%s/%s", o.AppName, taskID)

GetLogs(o.Config.AWSClient.CloudWatchLogsClient, logGroup, logStreamName, token)

return nil
}

func GetLogs(clw cloudwatchlogsiface.CloudWatchLogsAPI, logGroup string, logStreamName string, token *string) {
for {
logEvents, err := o.Config.AWSClient.CloudWatchLogsClient.GetLogEvents(&cloudwatchlogs.GetLogEventsInput{
logEvents, err := clw.GetLogEvents(&cloudwatchlogs.GetLogEventsInput{
LogGroupName: &logGroup,
LogStreamName: &logStreamName,
NextToken: token,
Expand Down
252 changes: 252 additions & 0 deletions internal/commands/start.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package commands

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/hazelops/ize/internal/config"
"github.com/hazelops/ize/internal/requirements"
"github.com/hazelops/ize/pkg/templates"
"github.com/pterm/pterm"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
"os/signal"
"strings"
"syscall"
)

type StartOptions struct {
Config *config.Project
AppName string
EcsCluster string
}

type NetworkConfiguration struct {
SecurityGroups struct {
Value string `json:"value"`
} `json:"security_groups"`
Subnets struct {
Value [][]string `json:"value"`
} `json:"subnets"`
VpcPrivateSubnets struct {
Value []string `json:"value"`
} `json:"vpc_private_subnets"`
VpcPublicSubnets struct {
Value []string `json:"value"`
} `json:"vpc_public_subnets"`
}

var startExample = templates.Examples(`
# Connect to a container in the ECS via AWS SSM and run command.
ize start goblin
`)

func NewStartFlags(project *config.Project) *StartOptions {
return &StartOptions{
Config: project,
}
}

func NewCmdStart(project *config.Project) *cobra.Command {
o := NewStartFlags(project)

cmd := &cobra.Command{
Use: "start [app-name]",
Example: startExample,
Short: "Start ECS task",
Long: "Start ECS task and stream logs until it dies or canceled.\nIt uses app name as an argument.",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: config.GetApps,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
err := o.Complete(cmd)
if err != nil {
return err
}

err = o.Validate()
if err != nil {
return err
}

err = o.Run()
if err != nil {
return err
}

return nil
},
}

cmd.Flags().StringVar(&o.EcsCluster, "ecs-cluster", "", "set ECS cluster name")

return cmd
}

func (o *StartOptions) Complete(cmd *cobra.Command) error {
if err := requirements.CheckRequirements(requirements.WithSSMPlugin()); err != nil {
return err
}

if o.EcsCluster == "" {
o.EcsCluster = fmt.Sprintf("%s-%s", o.Config.Env, o.Config.Namespace)
}

o.AppName = cmd.Flags().Args()[0]

return nil
}

func (o *StartOptions) Validate() error {
if len(o.AppName) == 0 {
return fmt.Errorf("can't validate: app name must be specified")
}

return nil
}

func getNetworkConfiguration(svc ssmiface.SSMAPI, env string) (NetworkConfiguration, error) {
resp, err := svc.GetParameter(&ssm.GetParameterInput{
Name: aws.String(fmt.Sprintf("/%s/terraform-output", env)),
WithDecryption: aws.Bool(true),
})
if err != nil {
return NetworkConfiguration{}, fmt.Errorf("can't get terraform output: %w", err)
}

var value []byte

value, err = base64.StdEncoding.DecodeString(*resp.Parameter.Value)
if err != nil {
return NetworkConfiguration{}, fmt.Errorf("can't get terraform output: %w", err)
}

var output NetworkConfiguration

err = json.Unmarshal(value, &output)
if err != nil {
return NetworkConfiguration{}, fmt.Errorf("can't get network configuration: %w", err)
}

return output, nil
}

func (o *StartOptions) Run() error {
ctx := context.Background()

appName := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName)
logGroup := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName)

logrus.Debugf("app name: %s, cluster name: %s", appName, o.EcsCluster)
logrus.Debugf("region: %s, profile: %s", o.Config.AwsProfile, o.Config.AwsRegion)

configuration, err := getNetworkConfiguration(o.Config.AWSClient.SSMClient, o.Config.Env)
if err != nil {
return err
}

logrus.Debugf("network configuration: %+v", configuration)

if len(configuration.VpcPrivateSubnets.Value) == 0 {
return fmt.Errorf("output private_subnets is missing. Please add it to your Terraform")
}

if len(configuration.SecurityGroups.Value) == 0 {
return fmt.Errorf("output security_groups is missing. Please add it to your Terraform")
}

out, err := o.Config.AWSClient.ECSClient.RunTaskWithContext(ctx, &ecs.RunTaskInput{
TaskDefinition: &appName,
StartedBy: aws.String("IZE"),
Cluster: &o.EcsCluster,
NetworkConfiguration: &ecs.NetworkConfiguration{AwsvpcConfiguration: &ecs.AwsVpcConfiguration{
Subnets: aws.StringSlice(configuration.VpcPrivateSubnets.Value),
}},
LaunchType: aws.String(ecs.LaunchTypeFargate),
})
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case "ClusterNotFoundException":
return fmt.Errorf("ECS cluster %s not found", o.EcsCluster)
default:
return err
}
}

taskID := getTaskID(*out.Tasks[0].TaskArn)

c := make(chan os.Signal)
ch := make(chan bool)
errorChannel := make(chan error)
signal.Notify(c, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM)

go func() {
s, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start(fmt.Sprintf("Please wait until task %s running...", appName))

err := o.Config.AWSClient.ECSClient.WaitUntilTasksRunningWithContext(ctx, &ecs.DescribeTasksInput{
Cluster: &o.EcsCluster,
Tasks: aws.StringSlice([]string{*out.Tasks[0].TaskArn}),
})
if err != nil {
errorChannel <- err
}

s.Success()
pterm.DefaultSection.Println("Logs:")

var token *string
go GetLogs(o.Config.AWSClient.CloudWatchLogsClient, logGroup, fmt.Sprintf("main/%s/%s", o.AppName, taskID), token)
err = o.Config.AWSClient.ECSClient.WaitUntilTasksStoppedWithContext(ctx, &ecs.DescribeTasksInput{
Cluster: &o.EcsCluster,
Tasks: aws.StringSlice([]string{*out.Tasks[0].TaskArn}),
})
if err != nil {
errorChannel <- err
}
ch <- true
}()

select {
case <-c:
fmt.Print("\r")
_, err := o.Config.AWSClient.ECSClient.StopTask(&ecs.StopTaskInput{
Cluster: &o.EcsCluster,
Reason: aws.String("Task stopped by IZE"),
Task: out.Tasks[0].TaskArn,
})
if err != nil {
return err
}
pterm.Success.Printfln("Stop task %s by interrupt", appName)
case <-ch:
tasks, err := o.Config.AWSClient.ECSClient.DescribeTasks(&ecs.DescribeTasksInput{
Cluster: &o.EcsCluster,
Tasks: aws.StringSlice([]string{taskID}),
})
if err != nil {
return err
}

sr := *tasks.Tasks[0].StoppedReason
st := *tasks.Tasks[0].StopCode
logrus.Debugf("stop code: %s", st)
pterm.Success.Printfln("%s was stopped with reason: %s\n", appName, sr)
return nil
case err := <-errorChannel:
return err
}

return nil
}

func getTaskID(taskArn string) string {
split := strings.Split(taskArn, "/")
return split[len(split)-1]
}
4 changes: 2 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type Config struct {
}

func (p *Project) GetConfig() error {
switch viper.GetString("log-level") {
switch viper.GetString("log_level") {
psihachina marked this conversation as resolved.
Show resolved Hide resolved
case "info":
logrus.SetLevel(logrus.InfoLevel)
case "debug":
Expand Down Expand Up @@ -129,7 +129,7 @@ func (p *Project) GetConfig() error {
}

func (p *Project) GetTestConfig() error {
switch viper.GetString("log-level") {
switch viper.GetString("log_level") {
case "info":
logrus.SetLevel(logrus.InfoLevel)
case "debug":
Expand Down