From f1828565fbe4002db9227f05c49fbfb8d8cdfd07 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Tue, 16 Apr 2024 13:31:02 +1200 Subject: [PATCH 01/14] fargate support --- cmd/runFargate.go | 60 +++++++++++++++ lib/runFargate.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 cmd/runFargate.go create mode 100644 lib/runFargate.go diff --git a/cmd/runFargate.go b/cmd/runFargate.go new file mode 100644 index 0000000..476f754 --- /dev/null +++ b/cmd/runFargate.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "os" + + "github.com/apex/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/springload/ecs-tool/lib" +) + +var runCmd = &cobra.Command{ + Use: "runFargate", + Short: "Runs a command", + Long: `Runs the specified command on an ECS cluster, optionally catching its output. + +It can modify the container command. +`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var containerName string + var commandArgs []string + if name := viper.GetString("container_name"); name == "" { + containerName = args[0] + commandArgs = args[1:] + } else { + containerName = name + commandArgs = args + } + + exitCode, err := lib.RunTask( + viper.GetString("profile"), + viper.GetString("cluster"), + viper.GetString("run.service"), + viper.GetString("task_definition"), + viper.GetString("image_tag"), + viper.GetStringSlice("image_tags"), + viper.GetString("workdir"), + containerName, + viper.GetString("log_group"), + viper.GetString("run.launch_type"), + commandArgs, + ) + if err != nil { + log.WithError(err).Error("Can't run task") + } + os.Exit(exitCode) + }, +} + +func init() { + rootCmd.AddCommand(runCmd) + runCmd.PersistentFlags().StringP("log_group", "l", "", "Name of the log group to get output") + runCmd.PersistentFlags().StringP("container_name", "", "", "Name of the container to modify parameters for") + runCmd.PersistentFlags().StringP("task_definition", "t", "", "name of task definition to use (required)") + viper.BindPFlag("log_group", runCmd.PersistentFlags().Lookup("log_group")) + viper.BindPFlag("container_name", runCmd.PersistentFlags().Lookup("container_name")) + viper.BindPFlag("task_definition", runCmd.PersistentFlags().Lookup("task_definition")) + viper.SetDefault("run.launch_type", "EC2") +} diff --git a/lib/runFargate.go b/lib/runFargate.go new file mode 100644 index 0000000..60e2643 --- /dev/null +++ b/lib/runFargate.go @@ -0,0 +1,185 @@ +package lib + +import ( + "fmt" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" +) + +// RunTask runs the specified one-off task in the cluster using the task definition +func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, imageTags []string, workDir, containerName, awslogGroup, launchType string, args []string) (exitCode int, err error) { + err = makeSession(profile) + if err != nil { + return 1, err + } + ctx := log.WithFields(&log.Fields{"task_definition": taskDefinitionName}) + + svc := ecs.New(localSession) + + describeResult, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ + TaskDefinition: aws.String(taskDefinitionName), + }) + if err != nil { + ctx.WithError(err).Error("Can't get task definition") + return 1, err + } + taskDefinition := describeResult.TaskDefinition + + var foundContainerName bool + if err := modifyContainerDefinitionImages(imageTag, imageTags, workDir, taskDefinition.ContainerDefinitions, ctx); err != nil { + return 1, err + } + for n, containerDefinition := range taskDefinition.ContainerDefinitions { + if aws.StringValue(containerDefinition.Name) == containerName { + foundContainerName = true + taskDefinition.ContainerDefinitions[n].Command = aws.StringSlice(args) + if awslogGroup != "" { + // modify log output driver to capture output to a predefined CloudWatch log + taskDefinition.ContainerDefinitions[n].LogConfiguration = &ecs.LogConfiguration{ + LogDriver: aws.String("awslogs"), + Options: map[string]*string{ + "awslogs-region": localSession.Config.Region, + "awslogs-group": aws.String(awslogGroup), + "awslogs-stream-prefix": aws.String(cluster), + }, + } + } + } + } + if !foundContainerName { + err := fmt.Errorf("Can't find container with specified name in the task definition") + ctx.WithFields(log.Fields{"container_name": containerName}).Error(err.Error()) + return 1, err + } + registerResult, err := svc.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{ + ContainerDefinitions: taskDefinition.ContainerDefinitions, + Cpu: taskDefinition.Cpu, + ExecutionRoleArn: taskDefinition.ExecutionRoleArn, + Family: taskDefinition.Family, + Memory: taskDefinition.Memory, + NetworkMode: taskDefinition.NetworkMode, + PlacementConstraints: taskDefinition.PlacementConstraints, + RequiresCompatibilities: taskDefinition.Compatibilities, + TaskRoleArn: taskDefinition.TaskRoleArn, + Volumes: taskDefinition.Volumes, + }) + if err != nil { + ctx.WithError(err).Error("Can't register task definition") + return 1, err + } + ctx.WithField( + "task_definition_arn", + aws.StringValue(registerResult.TaskDefinition.TaskDefinitionArn), + ).Debug("Registered the task definition") + + // deregister the task definition + defer func() { + ctx = ctx.WithFields(log.Fields{"task_definition_arn": aws.StringValue(registerResult.TaskDefinition.TaskDefinitionArn)}) + ctx.Debug("Deregistered the task definition") + _, err = svc.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{ + TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, + }) + if err != nil { + ctx.WithError(err).Error("Can't deregister task definition") + } + }() + + runTaskInput := ecs.RunTaskInput{ + Cluster: aws.String(cluster), + TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, + Count: aws.Int64(1), + StartedBy: aws.String("go-deploy"), + LaunchType: aws.String(launchType), + } + + if service != "" { + services, err := svc.DescribeServices(&ecs.DescribeServicesInput{ + Cluster: aws.String(cluster), + Services: []*string{aws.String(service)}, + }) + if err != nil { + ctx.WithError(err).Error("Can't get service") + return 1, err + } + + runTaskInput.NetworkConfiguration = services.Services[0].NetworkConfiguration + } + + runResult, err := svc.RunTask(&runTaskInput) + if err != nil { + ctx.WithError(err).Error("Can't run specified task") + return 1, err + } + + // if there are no running/pending tasks, then it failed to start + if len(runResult.Tasks) == 0 { + ctx.Error("No tasks could be run. Please check if the ECS cluster has enough resources") + return 1, err + } + // the task should be in PENDING state at this point + + ctx.Info("Waiting for the task to finish") + var tasks []*string + for _, task := range runResult.Tasks { + tasks = append(tasks, task.TaskArn) + ctx.WithField("task_arn", aws.StringValue(task.TaskArn)).Debug("Started task") + } + tasksInput := &ecs.DescribeTasksInput{ + Cluster: aws.String(cluster), + Tasks: tasks, + } + err = svc.WaitUntilTasksStopped(tasksInput) + if err != nil { + ctx.WithError(err).Error("The waiter has been finished with an error") + exitCode = 3 + } + tasksOutput, err := svc.DescribeTasks(tasksInput) + if err != nil { + ctx.WithError(err).Error("Can't describe stopped tasks") + return 1, err + } + for _, task := range tasksOutput.Tasks { + for _, container := range task.Containers { + ctx := log.WithFields(log.Fields{ + "container_name": aws.StringValue(container.Name), + }) + reason := aws.StringValue(container.Reason) + if len(reason) != 0 { + exitCode = 11 + ctx = ctx.WithField("reason", reason) + } else { + ctx = ctx.WithField("exit_code", aws.Int64Value(container.ExitCode)) + + } + if aws.Int64Value(container.ExitCode) == 0 && len(reason) == 0 { + ctx.Info("Container exited") + } else { + ctx.Error("Container exited") + } + if aws.StringValue(container.Name) == containerName { + if len(reason) == 0 { + exitCode = int(aws.Int64Value(container.ExitCode)) + if awslogGroup != "" { + // get log output + taskUUID, err := parseTaskUUID(container.TaskArn) + if err != nil { + log.WithFields(log.Fields{"task_arn": aws.StringValue(container.TaskArn)}).WithError(err).Error("Can't parse task uuid") + exitCode = 10 + continue + } + err = fetchCloudWatchLog(cluster, containerName, awslogGroup, taskUUID, false, ctx) + if err != nil { + log.WithError(err).Error("Can't fetch the logs") + exitCode = 10 + } + } + } + } + } + } + + return + +} From c2c4b14d163fd23458447561254f834766d1b4b6 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Tue, 16 Apr 2024 13:38:36 +1200 Subject: [PATCH 02/14] fargate support --- cmd/exec.go | 69 ++++++++++ cmd/root.go | 5 + cmd/run.go | 5 +- cmd/runFargate.go | 97 +++++++------ lib/exec.go | 49 +++++++ lib/run.go | 7 +- lib/runFargate.go | 339 ++++++++++++++++++++++++++++------------------ 7 files changed, 386 insertions(+), 185 deletions(-) create mode 100644 cmd/exec.go create mode 100644 lib/exec.go diff --git a/cmd/exec.go b/cmd/exec.go new file mode 100644 index 0000000..6ef43d3 --- /dev/null +++ b/cmd/exec.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "os" + "github.com/apex/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/springload/ecs-tool/lib" +) + +// execCmd executes a command in an existing ECS Fargate container. +var execCmd = &cobra.Command{ + Use: "exec", + Short: "Executes a command in an existing ECS Fargate container", + Long: `Executes a specified command in a running container on an ECS Fargate cluster. + This command allows for interactive sessions and command execution in Fargate.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // Command to be executed within the container + command := args[0] + + // Establish an AWS session using the specified profile and region from configuration + sess, err := session.NewSessionWithOptions(session.Options{ + Profile: viper.GetString("profile"), + Config: aws.Config{ + Region: aws.String(viper.GetString("region")), + }, + }) + if err != nil { + log.WithError(err).Error("Failed to create AWS session") + os.Exit(1) + } + + // Create a new ECS service client with the session + svc := ecs.New(sess) + + // Execute the command in the specified ECS container using the ECS service client + err = lib.ExecuteCommandInContainer(svc, viper.GetString("cluster"), viper.GetString("service_name"), viper.GetString("container_name"), command) + if err != nil { + log.WithError(err).Error("Failed to execute command in ECS Fargate container") + os.Exit(1) + } else { + log.Info("Command executed successfully in ECS Fargate container") + os.Exit(0) + } + }, +} + +func init() { + rootCmd.AddCommand(execCmd) + execCmd.Flags().String("profile", "", "AWS profile to use") + execCmd.Flags().String("region", "", "AWS region to operate in") + execCmd.Flags().String("cluster", "", "Name of the ECS cluster") + execCmd.Flags().String("service_name", "", "Name of the ECS service") + execCmd.Flags().String("container_name", "", "Name of the container in the task") + + viper.BindPFlag("profile", execCmd.Flags().Lookup("profile")) + viper.BindPFlag("region", execCmd.Flags().Lookup("region")) + viper.BindPFlag("cluster", execCmd.Flags().Lookup("cluster")) + viper.BindPFlag("service_name", execCmd.Flags().Lookup("service_name")) + viper.BindPFlag("container_name", execCmd.Flags().Lookup("container_name")) + + // Set default values or read from a configuration file + viper.SetDefault("region", "us-east-1") + viper.SetDefault("container_name", "default-container") +} diff --git a/cmd/root.go b/cmd/root.go index 00d0860..138fe6e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -51,12 +51,17 @@ func init() { rootCmd.PersistentFlags().StringP("workdir", "w", "", "Set working directory") rootCmd.PersistentFlags().StringP("image_tag", "", "", "Overrides the docker image tag in all container definitions. Overrides \"--image-tags\" flag.") rootCmd.PersistentFlags().StringSliceP("image_tags", "", []string{}, "Modifies the docker image tags in container definitions. Can be specified several times, one for each container definition. Also takes comma-separated values in one tag. I.e. if there are 2 containers and --image-tags is set once to \"new\", then the image tag of the first container will be modified, leaving the second one untouched. Gets overridden by \"--image-tag\". If you have 3 container definitions and want to modify tags for the 1st and the 3rd, but leave the 2nd unchanged, specify it as \"--image_tags first_tag,,last_tag\".") + rootCmd.PersistentFlags().StringP("task_definition", "t", "", "Name of the ECS task definition to use (required)") + + viper.BindPFlag("profile", rootCmd.PersistentFlags().Lookup("profile")) viper.BindPFlag("cluster", rootCmd.PersistentFlags().Lookup("cluster")) viper.BindPFlag("workdir", rootCmd.PersistentFlags().Lookup("workdir")) viper.BindPFlag("image_tag", rootCmd.PersistentFlags().Lookup("image_tag")) viper.BindPFlag("image_tags", rootCmd.PersistentFlags().Lookup("image_tags")) + viper.BindPFlag("task_definition", rootCmd.PersistentFlags().Lookup("task_definition")) + } diff --git a/cmd/run.go b/cmd/run.go index cc94327..5966d70 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/springload/ecs-tool/lib" + "fmt" ) var runCmd = &cobra.Command{ @@ -52,9 +53,9 @@ func init() { rootCmd.AddCommand(runCmd) runCmd.PersistentFlags().StringP("log_group", "l", "", "Name of the log group to get output") runCmd.PersistentFlags().StringP("container_name", "", "", "Name of the container to modify parameters for") - runCmd.PersistentFlags().StringP("task_definition", "t", "", "name of task definition to use (required)") viper.BindPFlag("log_group", runCmd.PersistentFlags().Lookup("log_group")) viper.BindPFlag("container_name", runCmd.PersistentFlags().Lookup("container_name")) - viper.BindPFlag("task_definition", runCmd.PersistentFlags().Lookup("task_definition")) + //viper.BindPFlag("task_definition", runCmd.PersistentFlags().Lookup("task_definition")) viper.SetDefault("run.launch_type", "EC2") + fmt.Println("Default launch_type set to:", viper.GetString("run.launch_type")) } diff --git a/cmd/runFargate.go b/cmd/runFargate.go index 476f754..4cfc090 100644 --- a/cmd/runFargate.go +++ b/cmd/runFargate.go @@ -1,60 +1,57 @@ package cmd import ( - "os" + "os" - "github.com/apex/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/springload/ecs-tool/lib" + "github.com/apex/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/springload/ecs-tool/lib" ) -var runCmd = &cobra.Command{ - Use: "runFargate", - Short: "Runs a command", - Long: `Runs the specified command on an ECS cluster, optionally catching its output. - -It can modify the container command. -`, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - var containerName string - var commandArgs []string - if name := viper.GetString("container_name"); name == "" { - containerName = args[0] - commandArgs = args[1:] - } else { - containerName = name - commandArgs = args - } - - exitCode, err := lib.RunTask( - viper.GetString("profile"), - viper.GetString("cluster"), - viper.GetString("run.service"), - viper.GetString("task_definition"), - viper.GetString("image_tag"), - viper.GetStringSlice("image_tags"), - viper.GetString("workdir"), - containerName, - viper.GetString("log_group"), - viper.GetString("run.launch_type"), - commandArgs, - ) - if err != nil { - log.WithError(err).Error("Can't run task") - } - os.Exit(exitCode) - }, +var runFargateCmd = &cobra.Command{ + Use: "runFargate", + Short: "Runs a command in Fargate mode", + Long: `Runs the specified command on an ECS cluster, optionally catching its output. + +This command is specifically tailored for future Fargate-specific functionality but currently duplicates the 'run' command.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var containerName string + var commandArgs []string + if name := viper.GetString("container_name"); name == "" { + containerName = args[0] + commandArgs = args[1:] + } else { + containerName = name + commandArgs = args + } + + exitCode, err := lib.RunFargate( + viper.GetString("profile"), + viper.GetString("cluster"), + viper.GetString("run.service"), + viper.GetString("task_definition"), + viper.GetString("image_tag"), + viper.GetStringSlice("image_tags"), + viper.GetString("workdir"), + containerName, + viper.GetString("log_group"), + viper.GetString("run.launch_type"), + viper.GetString("run.security_group_filter"), + commandArgs, + ) + if err != nil { + log.WithError(err).Error("Can't run task in Fargate mode") + } + os.Exit(exitCode) + }, } func init() { - rootCmd.AddCommand(runCmd) - runCmd.PersistentFlags().StringP("log_group", "l", "", "Name of the log group to get output") - runCmd.PersistentFlags().StringP("container_name", "", "", "Name of the container to modify parameters for") - runCmd.PersistentFlags().StringP("task_definition", "t", "", "name of task definition to use (required)") - viper.BindPFlag("log_group", runCmd.PersistentFlags().Lookup("log_group")) - viper.BindPFlag("container_name", runCmd.PersistentFlags().Lookup("container_name")) - viper.BindPFlag("task_definition", runCmd.PersistentFlags().Lookup("task_definition")) - viper.SetDefault("run.launch_type", "EC2") + rootCmd.AddCommand(runFargateCmd) + viper.SetDefault("run.security_group_filter", "*ec2*") + viper.SetDefault("run.launch_type", "FARGATE") + + } diff --git a/lib/exec.go b/lib/exec.go new file mode 100644 index 0000000..b5f8238 --- /dev/null +++ b/lib/exec.go @@ -0,0 +1,49 @@ +package lib + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + //"github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/ecs/ecsiface" +) + +// FindLatestTaskArn finds the latest task ARN for the specified service within a cluster +func FindLatestTaskArn(svc ecsiface.ECSAPI, clusterName, serviceName string) (string, error) { + input := &ecs.ListTasksInput{ + Cluster: aws.String(clusterName), + ServiceName: aws.String(serviceName), + DesiredStatus: aws.String("RUNNING"), + MaxResults: aws.Int64(1), + } + + result, err := svc.ListTasks(input) + if err != nil || len(result.TaskArns) == 0 { + return "", fmt.Errorf("no running tasks found for service %s on cluster %s", serviceName, clusterName) + } + + return aws.StringValue(result.TaskArns[0]), nil +} + +// ExecuteCommandInContainer executes a specified command in a running container on an ECS Fargate cluster. +func ExecuteCommandInContainer(svc ecsiface.ECSAPI, cluster, serviceName, containerName, command string) error { + taskArn, err := FindLatestTaskArn(svc, cluster, serviceName) + if err != nil { + return err + } + + input := &ecs.ExecuteCommandInput{ + Cluster: aws.String(cluster), + Task: aws.String(taskArn), + Container: aws.String(containerName), + Interactive: aws.Bool(true), + Command: aws.String(command), + } + + _, err = svc.ExecuteCommand(input) + if err != nil { + return fmt.Errorf("failed to execute command: %v", err) + } + + return nil +} diff --git a/lib/run.go b/lib/run.go index f0768b8..4d6e548 100644 --- a/lib/run.go +++ b/lib/run.go @@ -10,12 +10,15 @@ import ( // RunTask runs the specified one-off task in the cluster using the task definition func RunTask(profile, cluster, service, taskDefinitionName, imageTag string, imageTags []string, workDir, containerName, awslogGroup, launchType string, args []string) (exitCode int, err error) { + ctx := log.WithFields(log.Fields{ + "task_definition": taskDefinitionName, + "launch_type": launchType, + }) err = makeSession(profile) if err != nil { return 1, err } - ctx := log.WithFields(&log.Fields{"task_definition": taskDefinitionName}) - + svc := ecs.New(localSession) describeResult, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ diff --git a/lib/runFargate.go b/lib/runFargate.go index 60e2643..babd767 100644 --- a/lib/runFargate.go +++ b/lib/runFargate.go @@ -1,146 +1,172 @@ package lib import ( - "fmt" - - "github.com/apex/log" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ecs" + "fmt" + "github.com/apex/log" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/ec2" + "strings" ) -// RunTask runs the specified one-off task in the cluster using the task definition -func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, imageTags []string, workDir, containerName, awslogGroup, launchType string, args []string) (exitCode int, err error) { - err = makeSession(profile) - if err != nil { - return 1, err - } - ctx := log.WithFields(&log.Fields{"task_definition": taskDefinitionName}) +// RunFargate runs the specified one-off task in the cluster using the task definition +func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, imageTags []string, workDir, containerName, awslogGroup, launchType string, securityGroupFilter string, args []string) (exitCode int, err error) { + err = makeSession(profile) + if err != nil { + return 1, err + } + ctx := log.WithFields(log.Fields{"task_definition": taskDefinitionName}) - svc := ecs.New(localSession) + svc := ecs.New(localSession) + svcEC2 := ec2.New(localSession) // Assuming makeSession initializes localSession - describeResult, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ - TaskDefinition: aws.String(taskDefinitionName), - }) - if err != nil { - ctx.WithError(err).Error("Can't get task definition") - return 1, err - } - taskDefinition := describeResult.TaskDefinition + // Fetch subnets and security groups + subnets, err := fetchSubnetsByTag(svcEC2, "Tier", "private") + if err != nil { + log.WithError(err).Error("Failed to fetch subnets by tag") + return 1, err + } - var foundContainerName bool - if err := modifyContainerDefinitionImages(imageTag, imageTags, workDir, taskDefinition.ContainerDefinitions, ctx); err != nil { - return 1, err - } - for n, containerDefinition := range taskDefinition.ContainerDefinitions { - if aws.StringValue(containerDefinition.Name) == containerName { - foundContainerName = true - taskDefinition.ContainerDefinitions[n].Command = aws.StringSlice(args) - if awslogGroup != "" { - // modify log output driver to capture output to a predefined CloudWatch log - taskDefinition.ContainerDefinitions[n].LogConfiguration = &ecs.LogConfiguration{ - LogDriver: aws.String("awslogs"), - Options: map[string]*string{ - "awslogs-region": localSession.Config.Region, - "awslogs-group": aws.String(awslogGroup), - "awslogs-stream-prefix": aws.String(cluster), - }, - } - } - } - } - if !foundContainerName { - err := fmt.Errorf("Can't find container with specified name in the task definition") - ctx.WithFields(log.Fields{"container_name": containerName}).Error(err.Error()) - return 1, err - } - registerResult, err := svc.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{ - ContainerDefinitions: taskDefinition.ContainerDefinitions, - Cpu: taskDefinition.Cpu, - ExecutionRoleArn: taskDefinition.ExecutionRoleArn, - Family: taskDefinition.Family, - Memory: taskDefinition.Memory, - NetworkMode: taskDefinition.NetworkMode, - PlacementConstraints: taskDefinition.PlacementConstraints, - RequiresCompatibilities: taskDefinition.Compatibilities, - TaskRoleArn: taskDefinition.TaskRoleArn, - Volumes: taskDefinition.Volumes, - }) - if err != nil { - ctx.WithError(err).Error("Can't register task definition") - return 1, err - } - ctx.WithField( - "task_definition_arn", - aws.StringValue(registerResult.TaskDefinition.TaskDefinitionArn), - ).Debug("Registered the task definition") - - // deregister the task definition - defer func() { - ctx = ctx.WithFields(log.Fields{"task_definition_arn": aws.StringValue(registerResult.TaskDefinition.TaskDefinitionArn)}) - ctx.Debug("Deregistered the task definition") - _, err = svc.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{ - TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, - }) - if err != nil { - ctx.WithError(err).Error("Can't deregister task definition") - } - }() - - runTaskInput := ecs.RunTaskInput{ - Cluster: aws.String(cluster), - TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, - Count: aws.Int64(1), - StartedBy: aws.String("go-deploy"), - LaunchType: aws.String(launchType), - } +securityGroups, err := fetchSecurityGroupsByName(svcEC2, securityGroupFilter) + if err != nil { + log.WithError(err).Error("Failed to fetch security groups by name") + return 1, err + } - if service != "" { - services, err := svc.DescribeServices(&ecs.DescribeServicesInput{ - Cluster: aws.String(cluster), - Services: []*string{aws.String(service)}, - }) - if err != nil { - ctx.WithError(err).Error("Can't get service") - return 1, err - } + // Set up network configuration + networkConfiguration := &ecs.NetworkConfiguration{ + AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ + Subnets: subnets, + SecurityGroups: securityGroups, + AssignPublicIp: aws.String("ENABLED"), // or "ENABLED" if public IP is needed + }, + } - runTaskInput.NetworkConfiguration = services.Services[0].NetworkConfiguration - } - runResult, err := svc.RunTask(&runTaskInput) - if err != nil { - ctx.WithError(err).Error("Can't run specified task") - return 1, err - } + ctx.WithFields(log.Fields{ + "Cluster": aws.StringValue(aws.String(cluster)), + "TaskDefinition": aws.StringValue(aws.String(taskDefinitionName)), + "LaunchType": aws.StringValue(aws.String(launchType)), + "Subnets": fmt.Sprint(subnets), + "SecurityGroups": fmt.Sprint(securityGroups), + "AssignPublicIP": aws.StringValue(networkConfiguration.AwsvpcConfiguration.AssignPublicIp), +}).Info("Attempting to launch task") - // if there are no running/pending tasks, then it failed to start - if len(runResult.Tasks) == 0 { - ctx.Error("No tasks could be run. Please check if the ECS cluster has enough resources") - return 1, err - } - // the task should be in PENDING state at this point + describeResult, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ + TaskDefinition: aws.String(taskDefinitionName), + }) + if err != nil { + ctx.WithError(err).Error("Can't get task definition") + return 1, err + } + taskDefinition := describeResult.TaskDefinition - ctx.Info("Waiting for the task to finish") - var tasks []*string - for _, task := range runResult.Tasks { - tasks = append(tasks, task.TaskArn) - ctx.WithField("task_arn", aws.StringValue(task.TaskArn)).Debug("Started task") - } - tasksInput := &ecs.DescribeTasksInput{ - Cluster: aws.String(cluster), - Tasks: tasks, - } - err = svc.WaitUntilTasksStopped(tasksInput) - if err != nil { - ctx.WithError(err).Error("The waiter has been finished with an error") - exitCode = 3 - } - tasksOutput, err := svc.DescribeTasks(tasksInput) - if err != nil { - ctx.WithError(err).Error("Can't describe stopped tasks") - return 1, err - } - for _, task := range tasksOutput.Tasks { + var foundContainerName bool + if err := modifyContainerDefinitionImages(imageTag, imageTags, workDir, taskDefinition.ContainerDefinitions, ctx); err != nil { + return 1, err + } + for n, containerDefinition := range taskDefinition.ContainerDefinitions { + if aws.StringValue(containerDefinition.Name) == containerName { + foundContainerName = true + // Use shell execution to interpret the command with any arguments + commandLine := strings.Join(args, " ") // Join args into a single command line + containerDefinition.Command = []*string{aws.String("sh"), aws.String("-c"), aws.String(commandLine)} + if awslogGroup != "" { + containerDefinition.LogConfiguration = &ecs.LogConfiguration{ + LogDriver: aws.String("awslogs"), + Options: map[string]*string{ + "awslogs-region": localSession.Config.Region, + "awslogs-group": aws.String(awslogGroup), + "awslogs-stream-prefix": aws.String(cluster), + }, + } + } + taskDefinition.ContainerDefinitions[n] = containerDefinition // Update the container definition + + } +} + if !foundContainerName { + err := fmt.Errorf("Can't find container with specified name in the task definition") + ctx.WithFields(log.Fields{"container_name": containerName}).Error(err.Error()) + return 1, err + } + + registerResult, err := svc.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{ + ContainerDefinitions: taskDefinition.ContainerDefinitions, + Cpu: taskDefinition.Cpu, + ExecutionRoleArn: taskDefinition.ExecutionRoleArn, + Family: taskDefinition.Family, + Memory: taskDefinition.Memory, + NetworkMode: taskDefinition.NetworkMode, + PlacementConstraints: taskDefinition.PlacementConstraints, + RequiresCompatibilities: taskDefinition.Compatibilities, + TaskRoleArn: taskDefinition.TaskRoleArn, + Volumes: taskDefinition.Volumes, + }) + if err != nil { + ctx.WithError(err).Error("Can't register task definition") + return 1, err + } + ctx.WithField("task_definition_arn", aws.StringValue(registerResult.TaskDefinition.TaskDefinitionArn)).Debug("Registered the task definition") + + // Deregister the task definition + defer func() { + _, err = svc.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{ + TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, + }) + if err != nil { + ctx.WithError(err).Error("Can't deregister task definition") + } + }() + + // Run the task with network configuration + runTaskInput := ecs.RunTaskInput{ + Cluster: aws.String(cluster), + TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, + Count: aws.Int64(1), + StartedBy: aws.String("go-deploy"), + LaunchType: aws.String(launchType), + NetworkConfiguration: networkConfiguration, + } + + runResult, err := svc.RunTask(&runTaskInput) + if err != nil { + ctx.WithError(err).Error("Can't run specified task") + return 1, err + } + if len(runResult.Tasks) == 0 { + ctx.Error("No tasks could be run. Please check if the ECS cluster has enough resources") + return 1, err + } + + ctx.Info("Waiting for the task to finish") + var tasks []*string + for _, task := range runResult.Tasks { + tasks = append(tasks, task.TaskArn) + ctx.WithField("task_arn", aws.StringValue(task.TaskArn)).Debug("Started task") + } + tasksInput := &ecs.DescribeTasksInput{ + Cluster: aws.String(cluster), + Tasks: tasks, + } + err = svc.WaitUntilTasksStopped(tasksInput) + if err != nil { + ctx.WithError(err).Error("The waiter has been finished with an error") + exitCode = 3 + return exitCode, err + } + + tasksOutput, err := svc.DescribeTasks(tasksInput) + if err != nil { + ctx.WithError(err).Error("Can't describe stopped tasks") + return 1, err + } + + + + + +for _, task := range tasksOutput.Tasks { for _, container := range task.Containers { ctx := log.WithFields(log.Fields{ "container_name": aws.StringValue(container.Name), @@ -158,9 +184,11 @@ func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, } else { ctx.Error("Container exited") } + if aws.StringValue(container.Name) == containerName { if len(reason) == 0 { exitCode = int(aws.Int64Value(container.ExitCode)) + if awslogGroup != "" { // get log output taskUUID, err := parseTaskUUID(container.TaskArn) @@ -180,6 +208,55 @@ func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, } } - return + return exitCode, nil +} + + + +// fetchSubnetsByTag fetches subnet IDs by a specific tag name and value +func fetchSubnetsByTag(svc *ec2.EC2, tagKey, tagValue string) ([]*string, error) { + input := &ec2.DescribeSubnetsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String(fmt.Sprintf("tag:%s", tagKey)), + Values: []*string{aws.String(tagValue)}, + }, + }, + } + + result, err := svc.DescribeSubnets(input) + if err != nil { + return nil, fmt.Errorf("error describing subnets: %w", err) + } + + var subnets []*string + for _, subnet := range result.Subnets { + subnets = append(subnets, subnet.SubnetId) + } + + return subnets, nil +} + + +func fetchSecurityGroupsByName(svc *ec2.EC2, securityGroupFilter string) ([]*string, error) { + input := &ec2.DescribeSecurityGroupsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("group-name"), + Values: []*string{aws.String(securityGroupFilter)}, + }, + }, + } + + result, err := svc.DescribeSecurityGroups(input) + if err != nil { + return nil, fmt.Errorf("error describing security groups: %w", err) + } + + var securityGroups []*string + for _, sg := range result.SecurityGroups { + securityGroups = append(securityGroups, sg.GroupId) + } + return securityGroups, nil } From 117ab1be947e930f7181d9a023f11cca8477d4c4 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Thu, 18 Apr 2024 12:40:19 +1200 Subject: [PATCH 03/14] start working on exec --- cmd/exec.go | 69 +++++++++++++++++------------------------------ cmd/run.go | 2 +- cmd/runFargate.go | 6 ++--- lib/exec.go | 60 +++++++++++++++++++++++++++++++++++------ lib/runFargate.go | 52 ++--------------------------------- lib/util.go | 48 +++++++++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 107 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index 6ef43d3..78eaba3 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -2,68 +2,49 @@ package cmd import ( "os" + "github.com/apex/log" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ecs" "github.com/springload/ecs-tool/lib" ) -// execCmd executes a command in an existing ECS Fargate container. var execCmd = &cobra.Command{ Use: "exec", Short: "Executes a command in an existing ECS Fargate container", - Long: `Executes a specified command in a running container on an ECS Fargate cluster. - This command allows for interactive sessions and command execution in Fargate.`, + + Long: `Executes a specified command in a running container on an ECS Fargate cluster.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - // Command to be executed within the container - command := args[0] - - // Establish an AWS session using the specified profile and region from configuration - sess, err := session.NewSessionWithOptions(session.Options{ - Profile: viper.GetString("profile"), - Config: aws.Config{ - Region: aws.String(viper.GetString("region")), - }, - }) - if err != nil { - log.WithError(err).Error("Failed to create AWS session") - os.Exit(1) + viper.SetDefault("run.launch_type", "FARGATE") + var containerName string + var commandArgs []string + if name := viper.GetString("container_name"); name == "" { + containerName = args[0] + commandArgs = args[1:] + } else { + containerName = name + commandArgs = args } - // Create a new ECS service client with the session - svc := ecs.New(sess) - - // Execute the command in the specified ECS container using the ECS service client - err = lib.ExecuteCommandInContainer(svc, viper.GetString("cluster"), viper.GetString("service_name"), viper.GetString("container_name"), command) + exitCode, err := lib.ExecFargate( + viper.GetString("profile"), + viper.GetString("cluster"), + viper.GetString("run.service"), + viper.GetString("workdir"), + containerName, + viper.GetString("log_group"), + viper.GetString("run.launch_type"), + viper.GetString("run.security_group_filter"), + commandArgs, + ) if err != nil { - log.WithError(err).Error("Failed to execute command in ECS Fargate container") - os.Exit(1) - } else { - log.Info("Command executed successfully in ECS Fargate container") - os.Exit(0) + log.WithError(err).Error("Can't run task in Fargate mode") } + os.Exit(exitCode) }, } func init() { rootCmd.AddCommand(execCmd) - execCmd.Flags().String("profile", "", "AWS profile to use") - execCmd.Flags().String("region", "", "AWS region to operate in") - execCmd.Flags().String("cluster", "", "Name of the ECS cluster") - execCmd.Flags().String("service_name", "", "Name of the ECS service") - execCmd.Flags().String("container_name", "", "Name of the container in the task") - - viper.BindPFlag("profile", execCmd.Flags().Lookup("profile")) - viper.BindPFlag("region", execCmd.Flags().Lookup("region")) - viper.BindPFlag("cluster", execCmd.Flags().Lookup("cluster")) - viper.BindPFlag("service_name", execCmd.Flags().Lookup("service_name")) - viper.BindPFlag("container_name", execCmd.Flags().Lookup("container_name")) - - // Set default values or read from a configuration file - viper.SetDefault("region", "us-east-1") - viper.SetDefault("container_name", "default-container") } diff --git a/cmd/run.go b/cmd/run.go index 5966d70..fc353f2 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -19,6 +19,7 @@ It can modify the container command. `, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + viper.SetDefault("run.launch_type", "EC2") var containerName string var commandArgs []string if name := viper.GetString("container_name"); name == "" { @@ -56,6 +57,5 @@ func init() { viper.BindPFlag("log_group", runCmd.PersistentFlags().Lookup("log_group")) viper.BindPFlag("container_name", runCmd.PersistentFlags().Lookup("container_name")) //viper.BindPFlag("task_definition", runCmd.PersistentFlags().Lookup("task_definition")) - viper.SetDefault("run.launch_type", "EC2") fmt.Println("Default launch_type set to:", viper.GetString("run.launch_type")) } diff --git a/cmd/runFargate.go b/cmd/runFargate.go index 4cfc090..b47fd18 100644 --- a/cmd/runFargate.go +++ b/cmd/runFargate.go @@ -17,6 +17,8 @@ var runFargateCmd = &cobra.Command{ This command is specifically tailored for future Fargate-specific functionality but currently duplicates the 'run' command.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + viper.SetDefault("run.launch_type", "FARGATE") + viper.SetDefault("run.security_group_filter", "ec2") var containerName string var commandArgs []string if name := viper.GetString("container_name"); name == "" { @@ -50,8 +52,4 @@ This command is specifically tailored for future Fargate-specific functionality func init() { rootCmd.AddCommand(runFargateCmd) - viper.SetDefault("run.security_group_filter", "*ec2*") - viper.SetDefault("run.launch_type", "FARGATE") - - } diff --git a/lib/exec.go b/lib/exec.go index b5f8238..05edafb 100644 --- a/lib/exec.go +++ b/lib/exec.go @@ -2,14 +2,33 @@ package lib import ( "fmt" + "github.com/apex/log" "github.com/aws/aws-sdk-go/aws" - //"github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/ecs/ecsiface" ) -// FindLatestTaskArn finds the latest task ARN for the specified service within a cluster -func FindLatestTaskArn(svc ecsiface.ECSAPI, clusterName, serviceName string) (string, error) { +// InitializeSession creates an AWS session for ECS interaction +func InitializeSession(region, profile string) (*session.Session, error) { + sess, err := session.NewSessionWithOptions(session.Options{ + Profile: profile, + Config: aws.Config{ + }, + }) + if err != nil { + log.WithError(err).Error("Failed to create AWS session") + return nil, err + } + return sess, nil +} + +// FindLatestTaskArn locates the most recent task ARN for a specified ECS service +func FindLatestTaskArn(clusterName, serviceName string) (string, error) { + if serviceName == "" { + return "", fmt.Errorf("service name cannot be empty") + } + input := &ecs.ListTasksInput{ Cluster: aws.String(clusterName), ServiceName: aws.String(serviceName), @@ -18,20 +37,35 @@ func FindLatestTaskArn(svc ecsiface.ECSAPI, clusterName, serviceName string) (st } result, err := svc.ListTasks(input) - if err != nil || len(result.TaskArns) == 0 { + if err != nil { + log.WithError(err).Error("Error listing tasks") + return "", fmt.Errorf("error listing tasks for service %s on cluster %s: %v", serviceName, clusterName, err) + } + if len(result.TaskArns) == 0 { + log.WithFields(log.Fields{ + "cluster": clusterName, + "service": serviceName, + }).Error("No running tasks found") return "", fmt.Errorf("no running tasks found for service %s on cluster %s", serviceName, clusterName) } + log.WithFields(log.Fields{ + "taskArn": aws.StringValue(result.TaskArns[0]), + }).Info("Found latest task ARN") return aws.StringValue(result.TaskArns[0]), nil } -// ExecuteCommandInContainer executes a specified command in a running container on an ECS Fargate cluster. -func ExecuteCommandInContainer(svc ecsiface.ECSAPI, cluster, serviceName, containerName, command string) error { - taskArn, err := FindLatestTaskArn(svc, cluster, serviceName) +// ExecuteCommandInContainer runs a command in a specified container on an ECS Fargate service +func ExecFargate(rofile, cluster, service, container, workDir, containerName, awslogGroup, launchType string, command string) (exitCode int, err error) { + if service == "" { + return fmt.Errorf("service name cannot be empty") + } + + taskArn, err := FindLatestTaskArn(cluster, serviceName) if err != nil { return err } - + fmt.Println(taskArn , "we are here") input := &ecs.ExecuteCommandInput{ Cluster: aws.String(cluster), Task: aws.String(taskArn), @@ -42,8 +76,18 @@ func ExecuteCommandInContainer(svc ecsiface.ECSAPI, cluster, serviceName, contai _, err = svc.ExecuteCommand(input) if err != nil { + log.WithError(err).WithFields(log.Fields{ + "taskArn": taskArn, + "containerName": containerName, + "command": command, + }).Error("Failed to execute command") return fmt.Errorf("failed to execute command: %v", err) } + log.WithFields(log.Fields{ + "taskArn": taskArn, + "containerName": containerName, + "command": command, + }).Info("Command executed successfully") return nil } diff --git a/lib/runFargate.go b/lib/runFargate.go index babd767..d3264f4 100644 --- a/lib/runFargate.go +++ b/lib/runFargate.go @@ -17,6 +17,7 @@ func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, } ctx := log.WithFields(log.Fields{"task_definition": taskDefinitionName}) + svc := ecs.New(localSession) svcEC2 := ec2.New(localSession) // Assuming makeSession initializes localSession @@ -26,13 +27,11 @@ func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, log.WithError(err).Error("Failed to fetch subnets by tag") return 1, err } - securityGroups, err := fetchSecurityGroupsByName(svcEC2, securityGroupFilter) - if err != nil { + if err != nil { log.WithError(err).Error("Failed to fetch security groups by name") return 1, err } - // Set up network configuration networkConfiguration := &ecs.NetworkConfiguration{ AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ @@ -213,50 +212,3 @@ for _, task := range tasksOutput.Tasks { -// fetchSubnetsByTag fetches subnet IDs by a specific tag name and value -func fetchSubnetsByTag(svc *ec2.EC2, tagKey, tagValue string) ([]*string, error) { - input := &ec2.DescribeSubnetsInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String(fmt.Sprintf("tag:%s", tagKey)), - Values: []*string{aws.String(tagValue)}, - }, - }, - } - - result, err := svc.DescribeSubnets(input) - if err != nil { - return nil, fmt.Errorf("error describing subnets: %w", err) - } - - var subnets []*string - for _, subnet := range result.Subnets { - subnets = append(subnets, subnet.SubnetId) - } - - return subnets, nil -} - - -func fetchSecurityGroupsByName(svc *ec2.EC2, securityGroupFilter string) ([]*string, error) { - input := &ec2.DescribeSecurityGroupsInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("group-name"), - Values: []*string{aws.String(securityGroupFilter)}, - }, - }, - } - - result, err := svc.DescribeSecurityGroups(input) - if err != nil { - return nil, fmt.Errorf("error describing security groups: %w", err) - } - - var securityGroups []*string - for _, sg := range result.SecurityGroups { - securityGroups = append(securityGroups, sg.GroupId) - } - - return securityGroups, nil -} diff --git a/lib/util.go b/lib/util.go index 032d468..ad6a292 100644 --- a/lib/util.go +++ b/lib/util.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/ec2" ) var localSession *session.Session @@ -133,3 +134,50 @@ func modifyContainerDefinitionImages(imageTag string, imageTags []string, workDi } return nil } + +// fetchSubnetsByTag fetches subnet IDs by a specific tag name and value +func fetchSubnetsByTag(svc *ec2.EC2, tagKey, tagValue string) ([]*string, error) { + input := &ec2.DescribeSubnetsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String(fmt.Sprintf("tag:%s", tagKey)), + Values: []*string{aws.String(tagValue)}, + }, + }, + } + + result, err := svc.DescribeSubnets(input) + if err != nil { + return nil, fmt.Errorf("error describing subnets: %w", err) + } + + var subnets []*string + for _, subnet := range result.Subnets { + subnets = append(subnets, subnet.SubnetId) + } + + return subnets, nil +} + + +func fetchSecurityGroupsByName(svc *ec2.EC2, securityGroupFilter string) ([]*string, error) { + // Describe all security groups + input := &ec2.DescribeSecurityGroupsInput{} + + result, err := svc.DescribeSecurityGroups(input) + if err != nil { + return nil, fmt.Errorf("error describing security groups: %w", err) + } + + var securityGroups []*string + // Loop through the security groups and add those that contain the filter in their name + for _, sg := range result.SecurityGroups { + if strings.Contains(*sg.GroupName, securityGroupFilter) { + securityGroups = append(securityGroups, sg.GroupId) + } + } + + // Return the filtered list of security group IDs + return securityGroups, nil +} + From 02f844adbe6121de2baf47a3a2b360503098787c Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Mon, 22 Apr 2024 15:25:20 +1200 Subject: [PATCH 04/14] exec command --- cmd/exec.go | 16 +++--- go.mod | 48 ++++++++++++++++-- go.sum | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++-- lib/exec.go | 120 +++++++++++++++++++------------------------- 4 files changed, 239 insertions(+), 86 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index 78eaba3..6e10363 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -1,7 +1,8 @@ package cmd import ( - "os" + //"os" + "strings" "github.com/apex/log" "github.com/spf13/cobra" @@ -27,21 +28,20 @@ var execCmd = &cobra.Command{ commandArgs = args } - exitCode, err := lib.ExecFargate( + // Join the commandArgs to form a single command string + commandString := strings.Join(commandArgs, " ") + + err := lib.ExecFargate( viper.GetString("profile"), viper.GetString("cluster"), viper.GetString("run.service"), - viper.GetString("workdir"), containerName, - viper.GetString("log_group"), - viper.GetString("run.launch_type"), - viper.GetString("run.security_group_filter"), - commandArgs, + commandString, // Pass the combined command string ) if err != nil { log.WithError(err).Error("Can't run task in Fargate mode") } - os.Exit(exitCode) + //os.Exit(exitCode) }, } diff --git a/go.mod b/go.mod index 04198dc..5e0f355 100644 --- a/go.mod +++ b/go.mod @@ -6,31 +6,69 @@ require ( github.com/Shopify/ejson v1.2.1 github.com/apex/log v1.0.0 github.com/aws/aws-sdk-go v1.43.24 + github.com/gorilla/websocket v1.5.1 github.com/imdario/mergo v0.3.11 github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.0.2 - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 + golang.org/x/crypto v0.14.0 ) require ( github.com/BurntSushi/toml v0.3.1 // indirect + github.com/Songmu/flextime v0.1.0 // indirect + github.com/Songmu/prompter v0.5.1 // indirect + github.com/alecthomas/kong v0.7.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.26.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.31.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.41.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.26.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect + github.com/aws/smithy-go v1.20.2 // indirect + github.com/creack/pty v1.1.20 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/fujiwara/ecsta v0.4.5 // indirect + github.com/fujiwara/tracer v1.0.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/itchyny/gojq v0.12.11 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/magiconair/properties v1.8.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.3.4 // indirect + github.com/samber/lo v1.36.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/afero v1.1.1 // indirect github.com/spf13/cast v1.2.0 // indirect github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect github.com/spf13/pflag v1.0.1 // indirect - github.com/stretchr/testify v1.5.1 // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect - golang.org/x/text v0.3.7 // indirect - gopkg.in/yaml.v2 v2.3.0 // indirect + github.com/stretchr/testify v1.8.0 // indirect + github.com/tkuchiki/go-timezone v0.2.2 // indirect + github.com/tkuchiki/parsetime v0.3.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 0dea81f..2c2ef2c 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,55 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Shopify/ejson v1.2.1 h1:Dx0Ipn0mUgrZlzIa5oIUrH0rdSmBOyod/UJmQQK1KHo= github.com/Shopify/ejson v1.2.1/go.mod h1:J8cw5GOA0l/aMOPp+uDfwNYVbeqIaBhzRkv1+76UCvk= +github.com/Songmu/flextime v0.1.0 h1:sss5IALl84LbvU/cS5D1cKNd5ffT94N2BZwC+esgAJI= +github.com/Songmu/flextime v0.1.0/go.mod h1:ofUSZ/qj7f1BfQQ6rEH4ovewJ0SZmLOjBF1xa8iE87Q= +github.com/Songmu/prompter v0.5.1 h1:IAsttKsOZWSDw7bV1mtGn9TAmLFAjXbp9I/eYmUUogo= +github.com/Songmu/prompter v0.5.1/go.mod h1:CS3jEPD6h9IaLaG6afrl1orTgII9+uDWuw95dr6xHSw= +github.com/alecthomas/kong v0.7.0 h1:YIjJUiR7AcmHxL87UlbPn0gyIGwl4+nYND0OQ4ojP7k= +github.com/alecthomas/kong v0.7.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/apex/log v1.0.0 h1:5UWeZC54mWVtOGSCjtuvDPgY/o0QxmjQgvYZ27pLVGQ= github.com/apex/log v1.0.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY= github.com/aws/aws-sdk-go v1.43.24 h1:7c2PniJ0wpmWsIA6OtYBw6wS7DF0IjbhvPq+0ZQYNXw= github.com/aws/aws-sdk-go v1.43.24/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.3 h1:dKuc2jdp10y13dEEvPqWxqLoc0vF3Z9FC45MvuQSxOA= +github.com/aws/aws-sdk-go-v2/config v1.26.3/go.mod h1:Bxgi+DeeswYofcYO0XyGClwlrq3DZEXli0kLf4hkGA0= +github.com/aws/aws-sdk-go-v2/credentials v1.16.14 h1:mMDTwwYO9A0/JbOCOG7EOZHtYM+o7OfGWfu0toa23VE= +github.com/aws/aws-sdk-go-v2/credentials v1.16.14/go.mod h1:cniAUh3ErQPHtCQGPT5ouvSAQ0od8caTO9OOuufZOAE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.31.0 h1:Rk+Ft0Mu/eiNt2iJ2oS8Gf1h5m6q5crwS8cmlTylnvM= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.31.0/go.mod h1:jZNaJEtn9TLi3pfxycLz79HVkKxP8ZdYm92iaNFgBsA= +github.com/aws/aws-sdk-go-v2/service/ecs v1.41.7 h1:aFdgmJ8G385PVC9mp8b9roGGHU/XbrKEQTbzl6V0GbE= +github.com/aws/aws-sdk-go-v2/service/ecs v1.41.7/go.mod h1:rcFIIrVk3NGCT3BV84HQM3ut+Dr1PO71UvvT8GeLAv4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= +github.com/aws/aws-sdk-go-v2/service/sns v1.26.7 h1:DylmW2c1Z7qGxN3Y02k+voPbtM1mh7Rp+gV+7maG5io= +github.com/aws/aws-sdk-go-v2/service/sns v1.26.7/go.mod h1:mLFiISZfiZAqZEfPWUsZBK8gD4dYCKuKAfapV+KrIVQ= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/crackcomm/go-clitable v0.0.0-20151121230230-53bcff2fea36/go.mod h1:XiV36mPegOHv+dlkCSCazuGdQR2BUTgIZ2FKqTTHles= +github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4= +github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,14 +58,26 @@ github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKB github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fujiwara/ecsta v0.4.5 h1:82M3oL6n+eNd3JgPiFOOKkIYq46ggnts2HbuHOtX3rI= +github.com/fujiwara/ecsta v0.4.5/go.mod h1:SwSlCJuhiVrWfpYSaaHUSZVrs6Ey6nJ9V3flWNX7ndk= +github.com/fujiwara/tracer v1.0.2 h1:ztstnson+QwOpO69Jir4nkUKlYgse3vJ28FO2eOUPk0= +github.com/fujiwara/tracer v1.0.2/go.mod h1:r2QzBEBNsW9OhmoVdmTANG+GEmxWNZk7317/mnW2yIw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw= +github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -29,14 +86,28 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/goveralls v0.0.9/go.mod h1:FRbM1PS8oVsOe9JtdzAAXM+DsvDMMHcM1C7drGJD8HY= github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 h1:/rdJjIiKG5rRdwG5yxHmSE/7ZREjpyC0kL7GxGT/qJw= github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= +github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw= +github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -54,28 +125,90 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q= +github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY= +github.com/tkuchiki/parsetime v0.3.0 h1:cvblFQlPeAPJL8g6MgIGCHnnmHSZvluuY+hexoZCNqc= +github.com/tkuchiki/parsetime v0.3.0/go.mod h1:OJkQmIrf5Ao7R+WYIdITPOfDVj8LmnHGCfQ8DTs3LCA= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/exec.go b/lib/exec.go index 05edafb..7d928c1 100644 --- a/lib/exec.go +++ b/lib/exec.go @@ -1,93 +1,75 @@ package lib import ( + "context" "fmt" "github.com/apex/log" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ecs" - "github.com/aws/aws-sdk-go/service/ecs/ecsiface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ecs" + "github.com/fujiwara/ecsta" ) -// InitializeSession creates an AWS session for ECS interaction -func InitializeSession(region, profile string) (*session.Session, error) { - sess, err := session.NewSessionWithOptions(session.Options{ - Profile: profile, - Config: aws.Config{ - }, - }) - if err != nil { - log.WithError(err).Error("Failed to create AWS session") - return nil, err +var sessionInstance *ecs.Client +var sessionConfig aws.Config // Variable for session configuration + +// InitAWS initializes a new AWS session with the specified profile +func InitAWS(profile string) error { + if sessionInstance == nil { + cfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithSharedConfigProfile(profile), + ) + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + sessionInstance = ecs.NewFromConfig(cfg) + sessionConfig = cfg // Save session configuration } - return sess, nil + return nil } -// FindLatestTaskArn locates the most recent task ARN for a specified ECS service -func FindLatestTaskArn(clusterName, serviceName string) (string, error) { - if serviceName == "" { - return "", fmt.Errorf("service name cannot be empty") +// ExecFargate executes a command in a specified container on an ECS Fargate service +func ExecFargate(profile, cluster, service, containerName, command string) error { + if err := InitAWS(profile); err != nil { + return fmt.Errorf("failed to initialize AWS session: %w", err) } - input := &ecs.ListTasksInput{ - Cluster: aws.String(clusterName), - ServiceName: aws.String(serviceName), - DesiredStatus: aws.String("RUNNING"), - MaxResults: aws.Int64(1), + region := sessionConfig.Region // Use the saved region from session configuration + ecstaApp, err := ecsta.New(context.TODO(), region, cluster) + if err != nil { + return fmt.Errorf("failed to create ecsta application: %w", err) } + service = "app" - result, err := svc.ListTasks(input) - if err != nil { - log.WithError(err).Error("Error listing tasks") - return "", fmt.Errorf("error listing tasks for service %s on cluster %s: %v", serviceName, clusterName, err) + entrypoint := "/usr/bin/ssm-parent" + configPath := "/app/.ssm-parent.yaml" + fullCommand := fmt.Sprintf("%s -c %s run -- %s", entrypoint, configPath, command) + execOpt := ecsta.ExecOption{ + Service: aws.String(service), + Container: containerName, + Command: fullCommand, } - if len(result.TaskArns) == 0 { - log.WithFields(log.Fields{ - "cluster": clusterName, - "service": serviceName, - }).Error("No running tasks found") - return "", fmt.Errorf("no running tasks found for service %s on cluster %s", serviceName, clusterName) + + if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { + return fmt.Errorf("failed to execute command: %w", err) } - log.WithFields(log.Fields{ - "taskArn": aws.StringValue(result.TaskArns[0]), - }).Info("Found latest task ARN") - return aws.StringValue(result.TaskArns[0]), nil + log.Info("Command executed successfully") + return nil } -// ExecuteCommandInContainer runs a command in a specified container on an ECS Fargate service -func ExecFargate(rofile, cluster, service, container, workDir, containerName, awslogGroup, launchType string, command string) (exitCode int, err error) { - if service == "" { - return fmt.Errorf("service name cannot be empty") - } - - taskArn, err := FindLatestTaskArn(cluster, serviceName) +// FindLatestTaskArn locates the most recent task ARN for a specified ECS service +func FindLatestTaskArn(client *ecs.Client, clusterName, serviceName string) (string, error) { + resp, err := client.ListTasks(context.TODO(), &ecs.ListTasksInput{ + Cluster: aws.String(clusterName), + ServiceName: aws.String(serviceName), + }) if err != nil { - return err + return "", fmt.Errorf("error listing tasks: %w", err) } - fmt.Println(taskArn , "we are here") - input := &ecs.ExecuteCommandInput{ - Cluster: aws.String(cluster), - Task: aws.String(taskArn), - Container: aws.String(containerName), - Interactive: aws.Bool(true), - Command: aws.String(command), + if len(resp.TaskArns) == 0 { + return "", fmt.Errorf("no tasks found for service %s on cluster %s", serviceName, clusterName) } - _, err = svc.ExecuteCommand(input) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "taskArn": taskArn, - "containerName": containerName, - "command": command, - }).Error("Failed to execute command") - return fmt.Errorf("failed to execute command: %v", err) - } - - log.WithFields(log.Fields{ - "taskArn": taskArn, - "containerName": containerName, - "command": command, - }).Info("Command executed successfully") - return nil + return resp.TaskArns[0], nil } From b71fd012d20a7a11f0edcd72d3908d5fe6cba733 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Mon, 22 Apr 2024 16:07:51 +1200 Subject: [PATCH 05/14] exec command --- cmd/exec.go | 1 - lib/exec.go | 20 ++------------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index 6e10363..63dd4ec 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -41,7 +41,6 @@ var execCmd = &cobra.Command{ if err != nil { log.WithError(err).Error("Can't run task in Fargate mode") } - //os.Exit(exitCode) }, } diff --git a/lib/exec.go b/lib/exec.go index 7d928c1..60bfb38 100644 --- a/lib/exec.go +++ b/lib/exec.go @@ -13,7 +13,7 @@ import ( var sessionInstance *ecs.Client var sessionConfig aws.Config // Variable for session configuration -// InitAWS initializes a new AWS session with the specified profile +// InitAWS initializes a new AWS session with the specified profile for Ecsta realization func InitAWS(profile string) error { if sessionInstance == nil { cfg, err := config.LoadDefaultConfig(context.TODO(), @@ -45,11 +45,9 @@ func ExecFargate(profile, cluster, service, containerName, command string) error configPath := "/app/.ssm-parent.yaml" fullCommand := fmt.Sprintf("%s -c %s run -- %s", entrypoint, configPath, command) execOpt := ecsta.ExecOption{ - Service: aws.String(service), - Container: containerName, Command: fullCommand, } - + fmt.Println(execOpt) if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { return fmt.Errorf("failed to execute command: %w", err) } @@ -58,18 +56,4 @@ func ExecFargate(profile, cluster, service, containerName, command string) error return nil } -// FindLatestTaskArn locates the most recent task ARN for a specified ECS service -func FindLatestTaskArn(client *ecs.Client, clusterName, serviceName string) (string, error) { - resp, err := client.ListTasks(context.TODO(), &ecs.ListTasksInput{ - Cluster: aws.String(clusterName), - ServiceName: aws.String(serviceName), - }) - if err != nil { - return "", fmt.Errorf("error listing tasks: %w", err) - } - if len(resp.TaskArns) == 0 { - return "", fmt.Errorf("no tasks found for service %s on cluster %s", serviceName, clusterName) - } - return resp.TaskArns[0], nil -} From 1a85bf6fda0a11dae6b7533724383b9e36d076d6 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Mon, 22 Apr 2024 19:20:49 +1200 Subject: [PATCH 06/14] exec fixed --- cmd/exec.go | 6 +----- lib/exec.go | 7 ++++--- lib/runFargate.go | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index 63dd4ec..e6496a8 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -18,13 +18,11 @@ var execCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { viper.SetDefault("run.launch_type", "FARGATE") - var containerName string + //var containerName string var commandArgs []string if name := viper.GetString("container_name"); name == "" { - containerName = args[0] commandArgs = args[1:] } else { - containerName = name commandArgs = args } @@ -34,8 +32,6 @@ var execCmd = &cobra.Command{ err := lib.ExecFargate( viper.GetString("profile"), viper.GetString("cluster"), - viper.GetString("run.service"), - containerName, commandString, // Pass the combined command string ) if err != nil { diff --git a/lib/exec.go b/lib/exec.go index 60bfb38..685da17 100644 --- a/lib/exec.go +++ b/lib/exec.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/fujiwara/ecsta" + "os" ) var sessionInstance *ecs.Client @@ -22,6 +23,7 @@ func InitAWS(profile string) error { if err != nil { return fmt.Errorf("failed to load configuration: %w", err) } + os.Setenv("AWS_PROFILE", profile) //required for aws sdk sessionInstance = ecs.NewFromConfig(cfg) sessionConfig = cfg // Save session configuration } @@ -29,7 +31,8 @@ func InitAWS(profile string) error { } // ExecFargate executes a command in a specified container on an ECS Fargate service -func ExecFargate(profile, cluster, service, containerName, command string) error { +func ExecFargate(profile, cluster, command string) error { + if err := InitAWS(profile); err != nil { return fmt.Errorf("failed to initialize AWS session: %w", err) } @@ -39,7 +42,6 @@ func ExecFargate(profile, cluster, service, containerName, command string) error if err != nil { return fmt.Errorf("failed to create ecsta application: %w", err) } - service = "app" entrypoint := "/usr/bin/ssm-parent" configPath := "/app/.ssm-parent.yaml" @@ -47,7 +49,6 @@ func ExecFargate(profile, cluster, service, containerName, command string) error execOpt := ecsta.ExecOption{ Command: fullCommand, } - fmt.Println(execOpt) if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { return fmt.Errorf("failed to execute command: %w", err) } diff --git a/lib/runFargate.go b/lib/runFargate.go index d3264f4..8048dfd 100644 --- a/lib/runFargate.go +++ b/lib/runFargate.go @@ -63,7 +63,7 @@ securityGroups, err := fetchSecurityGroupsByName(svcEC2, securityGroupFilter) var foundContainerName bool if err := modifyContainerDefinitionImages(imageTag, imageTags, workDir, taskDefinition.ContainerDefinitions, ctx); err != nil { return 1, err - } + } for n, containerDefinition := range taskDefinition.ContainerDefinitions { if aws.StringValue(containerDefinition.Name) == containerName { foundContainerName = true From c49a42d1c6ff56407d65af188b7278847c4b7609 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Mon, 22 Apr 2024 19:26:56 +1200 Subject: [PATCH 07/14] bump go --- .github/workflows/build_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index a686d0d..d38a6e8 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -16,7 +16,7 @@ jobs: name: set up Go uses: actions/setup-go@v1 with: - go-version: 1.15.x + go-version: 1.21.x - name: cache modules uses: actions/cache@v2 From 3db714dad7291111b4a1a9110e37ae1c2ed4e6a7 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Tue, 23 Apr 2024 09:37:14 +1200 Subject: [PATCH 08/14] go version for releaser --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2f42ad..27f667e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,12 +17,12 @@ jobs: name: set up Go uses: actions/setup-go@v1 with: - go-version: 1.17.x + go-version: 1.21.x - name: run GoReleaser uses: goreleaser/goreleaser-action@v1 with: version: latest - args: release --rm-dist + args: release --clean env: GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} From 35b8d6f5bf4bc5b73567f95b60be51a3c3f613fa Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Fri, 10 May 2024 11:37:48 +1200 Subject: [PATCH 09/14] public subnets support for Runfargate --- lib/runFargate.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/runFargate.go b/lib/runFargate.go index 8048dfd..bb6687a 100644 --- a/lib/runFargate.go +++ b/lib/runFargate.go @@ -24,9 +24,17 @@ func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, // Fetch subnets and security groups subnets, err := fetchSubnetsByTag(svcEC2, "Tier", "private") if err != nil { - log.WithError(err).Error("Failed to fetch subnets by tag") + log.WithError(err).Error("Failed to fetch subnets by private tag") return 1, err } + if len(subnets) == 0 { + subnets, err = fetchSubnetsByTag(svcEC2, "Tier", "public") + + if err != nil { + log.WithError(err).Error("Failed to fetch subnets by public tag") + return 1, err + } +} securityGroups, err := fetchSecurityGroupsByName(svcEC2, securityGroupFilter) if err != nil { log.WithError(err).Error("Failed to fetch security groups by name") From f661b9da8156028073d4848eb2f538755da3ce04 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Fri, 10 May 2024 12:06:54 +1200 Subject: [PATCH 10/14] Readme update --- README.md | 21 +++++++++++++++++++++ cmd/runFargate.go | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 880db55..f12cddd 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,27 @@ So that `--cluster` can be set by `ECS_CLUSTER` environmental variable, or `--ta Also, `ecs-tool` exit code is the same as the container exit code. +### runFargate + +The runFargate function is a command that is integrated into the ecs-tool utility. This tool simplifies running commands on an AWS ECS (Elastic Container Service) cluster with Fargate. + +That normany use subnet with 'private' 'Tier' tag but if there is zero proivate subnets that will use 'public' + +``` +ecs-tool runFargate -e "preview" -- env +``` + +### EXEC + +ecs-tool exec Executes a specified command in a running container on an ECS Fargate cluster and get the output. +That function use existing container, so it's faster than runFargate +This command also could connect to fargate existing task: + +``` +ecs-tool exec -e "preview" /bin/sh +``` + + ### SSH 'SSH' access availabe to developers using `ecs-tool ssh` diff --git a/cmd/runFargate.go b/cmd/runFargate.go index b47fd18..36676ba 100644 --- a/cmd/runFargate.go +++ b/cmd/runFargate.go @@ -14,7 +14,7 @@ var runFargateCmd = &cobra.Command{ Short: "Runs a command in Fargate mode", Long: `Runs the specified command on an ECS cluster, optionally catching its output. -This command is specifically tailored for future Fargate-specific functionality but currently duplicates the 'run' command.`, +This command is specifically tailored for future Fargate-specific functionality.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { viper.SetDefault("run.launch_type", "FARGATE") From 51cd96be7b762f0e2c72226efb7c65ddd3d18841 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Tue, 25 Nov 2025 16:15:40 +1300 Subject: [PATCH 11/14] temp commit with exex improvements --- cmd/exec.go | 10 +- go.mod | 15 +-- go.sum | 51 +++---- lib/exec.go | 376 +++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 385 insertions(+), 67 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index e6496a8..43362ad 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -1,7 +1,7 @@ package cmd import ( - //"os" + "os" "strings" "github.com/apex/log" @@ -33,13 +33,19 @@ var execCmd = &cobra.Command{ viper.GetString("profile"), viper.GetString("cluster"), commandString, // Pass the combined command string + viper.GetString("task_id"), // Optional: will auto-extract task definition + viper.GetString("task_definition"), // Optional: for extracting entrypoint + viper.GetString("container_name"), // Optional: for extracting entrypoint ) if err != nil { - log.WithError(err).Error("Can't run task in Fargate mode") + log.WithError(err).Error("Can't execute command in Fargate mode") + os.Exit(1) } }, } func init() { rootCmd.AddCommand(execCmd) + execCmd.PersistentFlags().StringP("task_id", "", "", "Task ID to use (will auto-extract task definition)") + viper.BindPFlag("task_id", execCmd.PersistentFlags().Lookup("task_id")) } diff --git a/go.mod b/go.mod index 5e0f355..53e4878 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,15 @@ module github.com/springload/ecs-tool -go 1.17 +go 1.21 require ( github.com/Shopify/ejson v1.2.1 github.com/apex/log v1.0.0 github.com/aws/aws-sdk-go v1.43.24 - github.com/gorilla/websocket v1.5.1 + github.com/aws/aws-sdk-go-v2 v1.26.1 + github.com/aws/aws-sdk-go-v2/config v1.26.3 + github.com/aws/aws-sdk-go-v2/service/ecs v1.41.7 + github.com/fujiwara/ecsta v0.4.5 github.com/imdario/mergo v0.3.11 github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.0.2 @@ -18,16 +21,13 @@ require ( github.com/Songmu/flextime v0.1.0 // indirect github.com/Songmu/prompter v0.5.1 // indirect github.com/alecthomas/kong v0.7.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect - github.com/aws/aws-sdk-go-v2/config v1.26.3 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.16.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.31.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ecs v1.41.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect github.com/aws/aws-sdk-go-v2/service/sns v1.26.7 // indirect @@ -37,12 +37,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect github.com/aws/smithy-go v1.20.2 // indirect github.com/creack/pty v1.1.20 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect github.com/fsnotify/fsnotify v1.4.7 // indirect - github.com/fujiwara/ecsta v0.4.5 // indirect github.com/fujiwara/tracer v1.0.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/itchyny/gojq v0.12.11 // indirect @@ -62,11 +59,9 @@ require ( github.com/spf13/cast v1.2.0 // indirect github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect github.com/spf13/pflag v1.0.1 // indirect - github.com/stretchr/testify v1.8.0 // indirect github.com/tkuchiki/go-timezone v0.2.2 // indirect github.com/tkuchiki/parsetime v0.3.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect diff --git a/go.sum b/go.sum index 2c2ef2c..76e468b 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,12 @@ github.com/Songmu/flextime v0.1.0 h1:sss5IALl84LbvU/cS5D1cKNd5ffT94N2BZwC+esgAJI github.com/Songmu/flextime v0.1.0/go.mod h1:ofUSZ/qj7f1BfQQ6rEH4ovewJ0SZmLOjBF1xa8iE87Q= github.com/Songmu/prompter v0.5.1 h1:IAsttKsOZWSDw7bV1mtGn9TAmLFAjXbp9I/eYmUUogo= github.com/Songmu/prompter v0.5.1/go.mod h1:CS3jEPD6h9IaLaG6afrl1orTgII9+uDWuw95dr6xHSw= +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= github.com/alecthomas/kong v0.7.0 h1:YIjJUiR7AcmHxL87UlbPn0gyIGwl4+nYND0OQ4ojP7k= github.com/alecthomas/kong v0.7.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/apex/log v1.0.0 h1:5UWeZC54mWVtOGSCjtuvDPgY/o0QxmjQgvYZ27pLVGQ= github.com/apex/log v1.0.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY= github.com/aws/aws-sdk-go v1.43.24 h1:7c2PniJ0wpmWsIA6OtYBw6wS7DF0IjbhvPq+0ZQYNXw= @@ -62,14 +66,14 @@ github.com/fujiwara/ecsta v0.4.5 h1:82M3oL6n+eNd3JgPiFOOKkIYq46ggnts2HbuHOtX3rI= github.com/fujiwara/ecsta v0.4.5/go.mod h1:SwSlCJuhiVrWfpYSaaHUSZVrs6Ey6nJ9V3flWNX7ndk= github.com/fujiwara/tracer v1.0.2 h1:ztstnson+QwOpO69Jir4nkUKlYgse3vJ28FO2eOUPk0= github.com/fujiwara/tracer v1.0.2/go.mod h1:r2QzBEBNsW9OhmoVdmTANG+GEmxWNZk7317/mnW2yIw= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -84,6 +88,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= @@ -95,6 +101,8 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/goveralls v0.0.9/go.mod h1:FRbM1PS8oVsOe9JtdzAAXM+DsvDMMHcM1C7drGJD8HY= github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 h1:/rdJjIiKG5rRdwG5yxHmSE/7ZREjpyC0kL7GxGT/qJw= github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= @@ -125,21 +133,18 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q= github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY= github.com/tkuchiki/parsetime v0.3.0 h1:cvblFQlPeAPJL8g6MgIGCHnnmHSZvluuY+hexoZCNqc= github.com/tkuchiki/parsetime v0.3.0/go.mod h1:OJkQmIrf5Ao7R+WYIdITPOfDVj8LmnHGCfQ8DTs3LCA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= @@ -147,23 +152,13 @@ golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -171,24 +166,16 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -197,18 +184,16 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/exec.go b/lib/exec.go index 685da17..1d4765e 100644 --- a/lib/exec.go +++ b/lib/exec.go @@ -1,18 +1,22 @@ package lib import ( - "context" - "fmt" - "github.com/apex/log" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ecs" - "github.com/fujiwara/ecsta" - "os" + "context" + "fmt" + "os" + "strings" + + "github.com/apex/log" + awsv2 "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + ecsv2 "github.com/aws/aws-sdk-go-v2/service/ecs" + "github.com/aws/aws-sdk-go/aws" + ecsv1 "github.com/aws/aws-sdk-go/service/ecs" + "github.com/fujiwara/ecsta" ) -var sessionInstance *ecs.Client -var sessionConfig aws.Config // Variable for session configuration +var sessionInstance *ecsv2.Client +var sessionConfig awsv2.Config // Variable for session configuration // InitAWS initializes a new AWS session with the specified profile for Ecsta realization func InitAWS(profile string) error { @@ -24,37 +28,365 @@ func InitAWS(profile string) error { return fmt.Errorf("failed to load configuration: %w", err) } os.Setenv("AWS_PROFILE", profile) //required for aws sdk - sessionInstance = ecs.NewFromConfig(cfg) + sessionInstance = ecsv2.NewFromConfig(cfg) sessionConfig = cfg // Save session configuration } return nil } +// getTaskDefinitionFromTaskID gets the task definition ARN from a task ID and extracts the family name +func getTaskDefinitionFromTaskID(profile, cluster, taskID string) (taskDefinitionName string, err error) { + err = makeSession(profile) + if err != nil { + return "", fmt.Errorf("failed to create session: %w", err) + } + + svc := ecsv1.New(localSession) + + // List tasks to find the one matching the task ID + listResult, err := svc.ListTasks(&ecsv1.ListTasksInput{ + Cluster: aws.String(cluster), + }) + if err != nil { + return "", fmt.Errorf("failed to list tasks: %w", err) + } + + if len(listResult.TaskArns) == 0 { + return "", fmt.Errorf("no tasks found in cluster") + } + + // Find task that matches the task ID (task ID is usually a prefix of the full ARN) + var matchingTaskArn *string + for _, taskArn := range listResult.TaskArns { + taskArnStr := aws.StringValue(taskArn) + // Task ID is usually the last part of the ARN after the last / + parts := strings.Split(taskArnStr, "/") + if len(parts) > 0 { + taskArnID := parts[len(parts)-1] + // Check if task ID matches (could be full ID or partial) + if strings.HasPrefix(taskArnID, taskID) || strings.HasPrefix(taskID, taskArnID) { + matchingTaskArn = taskArn + break + } + } + } + + if matchingTaskArn == nil { + return "", fmt.Errorf("task ID %s not found in cluster", taskID) + } + + // Describe the task to get task definition ARN + describeResult, err := svc.DescribeTasks(&ecsv1.DescribeTasksInput{ + Cluster: aws.String(cluster), + Tasks: []*string{matchingTaskArn}, + }) + if err != nil { + return "", fmt.Errorf("failed to describe task: %w", err) + } + + if len(describeResult.Tasks) == 0 { + return "", fmt.Errorf("task not found") + } + + taskDefinitionArn := aws.StringValue(describeResult.Tasks[0].TaskDefinitionArn) + + // Extract task definition family name from ARN + // ARN format: arn:aws:ecs:region:account:task-definition/family:revision + parts := strings.Split(taskDefinitionArn, "/") + if len(parts) < 2 { + return "", fmt.Errorf("invalid task definition ARN format: %s", taskDefinitionArn) + } + + // Get family:revision and extract just the family name + familyRevision := parts[1] + familyParts := strings.Split(familyRevision, ":") + taskDefinitionName = familyParts[0] + + return taskDefinitionName, nil +} + +// extractEntrypointFromTaskDefinition extracts ssm-parent entrypoint and config from task definition +func extractEntrypointFromTaskDefinition(profile, cluster, taskDefinitionName, containerName string) (entrypoint string, configPath string, err error) { + // Use AWS SDK v1 for compatibility with existing code + err = makeSession(profile) + if err != nil { + return "", "", fmt.Errorf("failed to create session: %w", err) + } + + svc := ecsv1.New(localSession) + + describeResult, err := svc.DescribeTaskDefinition(&ecsv1.DescribeTaskDefinitionInput{ + TaskDefinition: aws.String(taskDefinitionName), + }) + if err != nil { + return "", "", fmt.Errorf("failed to describe task definition: %w", err) + } + + // Find the container definition + for _, containerDef := range describeResult.TaskDefinition.ContainerDefinitions { + if aws.StringValue(containerDef.Name) == containerName { + // Check EntryPoint field + if len(containerDef.EntryPoint) > 0 { + // EntryPoint is typically: ["/sbin/ssm-parent", "run", "-e", "-p", "...", "--", "su-exec", "www"] + // We want to extract the ssm-parent path (first element) and config if present + entrypoint = aws.StringValue(containerDef.EntryPoint[0]) + + // Look for -c flag in EntryPoint to find config path + for i, arg := range containerDef.EntryPoint { + if i > 0 && aws.StringValue(arg) == "-c" && i+1 < len(containerDef.EntryPoint) { + configPath = aws.StringValue(containerDef.EntryPoint[i+1]) + break + } + } + + return entrypoint, configPath, nil + } + } + } + + return "", "", fmt.Errorf("container %s not found or has no entrypoint", containerName) +} + // ExecFargate executes a command in a specified container on an ECS Fargate service -func ExecFargate(profile, cluster, command string) error { +// taskID, taskDefinitionName and containerName are optional - if provided, will extract entrypoint from task definition +func ExecFargate(profile, cluster, command string, taskID, taskDefinitionName, containerName string) error { if err := InitAWS(profile); err != nil { return fmt.Errorf("failed to initialize AWS session: %w", err) } - region := sessionConfig.Region // Use the saved region from session configuration - ecstaApp, err := ecsta.New(context.TODO(), region, cluster) + // Get region from session config (already a string in awsv2.Config) + regionStr := sessionConfig.Region + ecstaApp, err := ecsta.New(context.TODO(), regionStr, cluster) if err != nil { return fmt.Errorf("failed to create ecsta application: %w", err) } - entrypoint := "/usr/bin/ssm-parent" - configPath := "/app/.ssm-parent.yaml" - fullCommand := fmt.Sprintf("%s -c %s run -- %s", entrypoint, configPath, command) + // Try to get task definition from task ID if provided + if taskID != "" && taskDefinitionName == "" { + extractedTaskDef, err := getTaskDefinitionFromTaskID(profile, cluster, taskID) + if err == nil { + taskDefinitionName = extractedTaskDef + log.WithFields(log.Fields{ + "task_id": taskID, + "task_definition": taskDefinitionName, + }).Debug("Extracted task definition from task ID") + } else { + log.WithError(err).Debug("Could not extract task definition from task ID, will use path fallback") + } + } + + // Try to extract entrypoint and config from task definition if provided + var entrypointPaths []string + var configPaths []string + extractionSucceeded := false + + if taskDefinitionName != "" && containerName != "" { + extractedEntrypoint, extractedConfig, err := extractEntrypointFromTaskDefinition(profile, cluster, taskDefinitionName, containerName) + if err == nil { + extractionSucceeded = true + // Only try the extracted entrypoint - don't try unknown fallback paths + // Unknown paths can kill the ECS Exec session if they don't exist + entrypointPaths = []string{extractedEntrypoint} + + // Detect -c flag support: if extractedConfig is not empty, container supports -c + supportsCFlag := (extractedConfig != "") + + if supportsCFlag { + // Container supports -c flag, use extracted config and fallbacks + configPaths = []string{ + extractedConfig, // Use extracted config first + "/app/.ssm-parent.yaml", + "/.ssm-parent.yaml", + "", + } + } else { + // Container does NOT support -c flag, skip all -c attempts + configPaths = []string{} // Empty - will skip -c format entirely + log.Debug("Container does not support -c flag (not found in ENTRYPOINT), skipping all -c attempts") + } + + log.WithFields(log.Fields{ + "entrypoint": extractedEntrypoint, + "config": extractedConfig, + "supports_c_flag": supportsCFlag, + }).Debug("Extracted entrypoint and config from task definition") + } else { + log.WithError(err).Debug("Could not extract entrypoint from task definition, skipping ssm-parent (will use direct exec)") + } + } + + // When extraction fails, skip ssm-parent entirely to avoid session kills + // Unknown entrypoint paths can cause "fork/exec ... no such file" which kills the session + if !extractionSucceeded { + // Entrypoint extraction failed - skip ssm-parent, go straight to direct exec + entrypointPaths = []string{} // Empty - will skip ssm-parent entirely + configPaths = []string{} // Empty - will skip -c format entirely + log.Debug("Entrypoint extraction failed — skipping ssm-parent to avoid session kills, will use direct exec") + } + + var ssmParentTried bool + var ssmParentErr error + var ssmParentSucceeded bool + + // Try with ssm-parent first (preferred for env vars) + // Support both formats: with -c flag (springload) and without (madewithwagtail) + // Each entrypoint/config combination is tried exactly once + for _, entrypoint := range entrypointPaths { + ssmParentTried = true + var cFlagNotSupported bool + var entrypointFound bool + var triedWithoutCFlag bool + + // First, try with -c flag format (for projects like springload) + // Format: ssm-parent -c .ssm-parent.yaml run -- + // Only try if we have config paths and haven't determined -c is unsupported + if !cFlagNotSupported && len(configPaths) > 0 { + for _, configPath := range configPaths { + if configPath == "" { + continue // Skip empty config path when trying -c format + } + + // If we already determined -c is not supported, skip remaining config paths + if cFlagNotSupported { + break + } + + fullCommand := fmt.Sprintf("%s -c %s run -- %s", entrypoint, configPath, command) + + execOpt := ecsta.ExecOption{ + Command: fullCommand, + } + + ctx := log.WithFields(log.Fields{ + "entrypoint": entrypoint, + "config_path": configPath, + "format": "with -c flag", + "command": fullCommand, + }) + ctx.Debug("Attempting to execute command with ssm-parent (-c flag format)") + + if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { + ssmParentErr = err + errMsg := strings.ToLower(err.Error()) + // If we get "unknown shorthand flag", this ssm-parent doesn't support -c flag + // Skip all remaining config paths and try without -c for this entrypoint + if strings.Contains(errMsg, "unknown shorthand flag") { + ctx.WithError(err).Debug("ssm-parent doesn't support -c flag, skipping remaining configs and will try without -c") + cFlagNotSupported = true + break // Break out of config loop immediately + } + // If entrypoint not found, break config loop and move to next entrypoint + // (trying other config paths is pointless since the binary itself is missing) + if strings.Contains(errMsg, "no such file or directory") || + strings.Contains(errMsg, "not found") || + strings.Contains(errMsg, "fork/exec") { + ctx.WithError(err).Debug("entrypoint not found, breaking config loop (will try without -c, then next entrypoint if that also fails)") + break // Break config loop - we'll try without -c for this entrypoint, then move to next if that fails + } + // Other errors: try without -c flag for this entrypoint + ctx.WithError(err).Debug("ssm-parent -c format failed with other error, will try without -c") + cFlagNotSupported = true + break + } else { + // RunExec returned nil - command may have succeeded or failed silently + // Mark that we tried this entrypoint and it exists + entrypointFound = true + ssmParentSucceeded = true // Track that ssm-parent didn't error (may have worked) + log.Debug("ssm-parent -c format attempt returned success (may have succeeded or failed silently)") + // Break out of config loop - we've found a working entrypoint/config combination + // We'll still try direct execution as fallback to ensure working session + break + } + } + } + + // Second, try without -c flag format (for projects like madewithwagtail) + // Format: ssm-parent run -- + // Try this if: + // 1. -c flag is not supported (detected above) + // 2. No config paths available + // 3. We haven't tried without -c yet for this entrypoint + if !triedWithoutCFlag && (cFlagNotSupported || len(configPaths) == 0 || (len(configPaths) == 1 && configPaths[0] == "")) { + triedWithoutCFlag = true + fullCommand := fmt.Sprintf("%s run -- %s", entrypoint, command) + + execOpt := ecsta.ExecOption{ + Command: fullCommand, + } + + ctx := log.WithFields(log.Fields{ + "entrypoint": entrypoint, + "format": "without -c flag", + "command": fullCommand, + }) + ctx.Debug("Attempting to execute command with ssm-parent (simple format)") + + if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { + ssmParentErr = err + errMsg := strings.ToLower(err.Error()) + // If entrypoint not found, try next entrypoint path + if strings.Contains(errMsg, "no such file or directory") || + strings.Contains(errMsg, "not found") || + strings.Contains(errMsg, "fork/exec") { + ctx.WithError(err).Debug("ssm-parent not found, trying next entrypoint path") + continue // Move to next entrypoint + } + // Other errors: also try next entrypoint + ctx.WithError(err).Debug("ssm-parent execution failed, trying next entrypoint") + continue + } else { + // RunExec returned nil - command may have succeeded or failed silently + entrypointFound = true + ssmParentSucceeded = true // Track that ssm-parent didn't error (may have worked) + log.Debug("ssm-parent simple format attempt returned success (may have succeeded or failed silently)") + // Continue to direct execution fallback to ensure working session + } + } + + // If we found the entrypoint (even if execution may have failed silently), + // we've tried both formats for this entrypoint, so move on + if entrypointFound { + // We've exhausted options for this entrypoint, continue to direct execution + break + } + } + + // Always try direct execution as fallback to handle silent failures + // This ensures we get a working session even if ssm-parent failed silently + // According to plan: "Always try direct execution after ssm-parent attempts (even if they return success)" + if ssmParentTried { + if ssmParentSucceeded { + log.Debug("ssm-parent appears to have succeeded, but attempting direct execution to ensure working session") + } else { + log.Debug("Attempting direct execution to verify/fallback from ssm-parent (handling silent failures)") + } + } else { + log.Debug("Attempting to execute command directly (ssm-parent not available)") + } + execOpt := ecsta.ExecOption{ - Command: fullCommand, + Command: command, } + if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { + if ssmParentErr != nil { + return fmt.Errorf("failed to execute command: ssm-parent error (%v), direct execution error (%w)", ssmParentErr, err) + } return fmt.Errorf("failed to execute command: %w", err) } - - log.Info("Command executed successfully") + + // Direct execution succeeded - this ensures we have a working session + // Note: If ssm-parent also worked, we prefer direct execution here because + // we can't verify ssm-parent actually worked due to silent failures + if ssmParentTried { + if ssmParentSucceeded { + log.Info("Command executed successfully (direct execution - ssm-parent may have also worked)") + } else { + log.Info("Command executed successfully (direct execution - ssm-parent may have failed silently)") + } + } else { + log.Info("Command executed successfully (direct execution)") + } return nil } - - From e6cca828d9d9874e034b677404bd8a14e7d7ea1a Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Wed, 26 Nov 2025 10:39:21 +1300 Subject: [PATCH 12/14] exec linting commit --- cmd/exec.go | 16 +- lib/exec.go | 591 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 357 insertions(+), 250 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index 43362ad..594fa53 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -29,14 +29,14 @@ var execCmd = &cobra.Command{ // Join the commandArgs to form a single command string commandString := strings.Join(commandArgs, " ") - err := lib.ExecFargate( - viper.GetString("profile"), - viper.GetString("cluster"), - commandString, // Pass the combined command string - viper.GetString("task_id"), // Optional: will auto-extract task definition - viper.GetString("task_definition"), // Optional: for extracting entrypoint - viper.GetString("container_name"), // Optional: for extracting entrypoint - ) + err := lib.ExecFargate(lib.ExecConfig{ + Profile: viper.GetString("profile"), + Cluster: viper.GetString("cluster"), + Command: commandString, + TaskID: viper.GetString("task_id"), + TaskDefinitionName: viper.GetString("task_definition"), + ContainerName: viper.GetString("container_name"), + }) if err != nil { log.WithError(err).Error("Can't execute command in Fargate mode") os.Exit(1) diff --git a/lib/exec.go b/lib/exec.go index 1d4765e..a8e633d 100644 --- a/lib/exec.go +++ b/lib/exec.go @@ -11,10 +11,19 @@ import ( "github.com/aws/aws-sdk-go-v2/config" ecsv2 "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" ecsv1 "github.com/aws/aws-sdk-go/service/ecs" "github.com/fujiwara/ecsta" ) +// Error message constants for error detection +const ( + ErrUnknownFlag = "unknown shorthand flag" + ErrNoFile = "no such file or directory" + ErrNotFound = "not found" + ErrForkExec = "fork/exec" +) + var sessionInstance *ecsv2.Client var sessionConfig awsv2.Config // Variable for session configuration @@ -63,8 +72,8 @@ func getTaskDefinitionFromTaskID(profile, cluster, taskID string) (taskDefinitio parts := strings.Split(taskArnStr, "/") if len(parts) > 0 { taskArnID := parts[len(parts)-1] - // Check if task ID matches (could be full ID or partial) - if strings.HasPrefix(taskArnID, taskID) || strings.HasPrefix(taskID, taskArnID) { + // Check if task ID matches (task ID is always a prefix of the ARN ID) + if strings.HasPrefix(taskArnID, taskID) { matchingTaskArn = taskArn break } @@ -90,10 +99,16 @@ func getTaskDefinitionFromTaskID(profile, cluster, taskID string) (taskDefinitio taskDefinitionArn := aws.StringValue(describeResult.Tasks[0].TaskDefinitionArn) - // Extract task definition family name from ARN + // Extract task definition family name from ARN using proper ARN parsing // ARN format: arn:aws:ecs:region:account:task-definition/family:revision - parts := strings.Split(taskDefinitionArn, "/") - if len(parts) < 2 { + parsed, err := arn.Parse(taskDefinitionArn) + if err != nil { + return "", fmt.Errorf("invalid task definition ARN: %w", err) + } + + // parsed.Resource == "task-definition/family:revision" + parts := strings.Split(parsed.Resource, "/") + if len(parts) != 2 { return "", fmt.Errorf("invalid task definition ARN format: %s", taskDefinitionArn) } @@ -147,246 +162,338 @@ func extractEntrypointFromTaskDefinition(profile, cluster, taskDefinitionName, c return "", "", fmt.Errorf("container %s not found or has no entrypoint", containerName) } +// SSMParentConfig holds the configuration for ssm-parent execution +type SSMParentConfig struct { + EntrypointPaths []string + ConfigPaths []string + SupportsCFlag bool + ExtractionSucceeded bool +} + +// execResult holds the result of an execution attempt +type execResult struct { + succeeded bool + err error +} + +// determineSSMParentConfig extracts entrypoint and config from task definition +// and determines the appropriate ssm-parent configuration +func determineSSMParentConfig(profile, cluster, taskDefinitionName, containerName string) SSMParentConfig { + config := SSMParentConfig{ + EntrypointPaths: []string{}, + ConfigPaths: []string{}, + } + + if taskDefinitionName != "" && containerName != "" { + extractedEntrypoint, extractedConfig, err := extractEntrypointFromTaskDefinition(profile, cluster, taskDefinitionName, containerName) + if err == nil { + config.ExtractionSucceeded = true + // Only try the extracted entrypoint - don't try unknown fallback paths + // Unknown paths can kill the ECS Exec session if they don't exist + config.EntrypointPaths = []string{extractedEntrypoint} + + // Detect -c flag support: if extractedConfig is not empty, container supports -c + config.SupportsCFlag = (extractedConfig != "") + + if config.SupportsCFlag { + // Container supports -c flag, use extracted config and fallbacks + config.ConfigPaths = []string{ + extractedConfig, // Use extracted config first + "/app/.ssm-parent.yaml", + "/.ssm-parent.yaml", + "", + } + } else { + // Container does NOT support -c flag, skip all -c attempts + config.ConfigPaths = []string{} // Empty - will skip -c format entirely + log.Debug("Container does not support -c flag (not found in ENTRYPOINT), skipping all -c attempts") + } + + log.WithFields(log.Fields{ + "entrypoint": extractedEntrypoint, + "config": extractedConfig, + "supports_c_flag": config.SupportsCFlag, + }).Debug("Extracted entrypoint and config from task definition") + } else { + log.WithError(err).Debug("Could not extract entrypoint from task definition, skipping ssm-parent (will use direct exec)") + } + } + + // When extraction fails, skip ssm-parent entirely to avoid session kills + // Unknown entrypoint paths can cause "fork/exec ... no such file" which kills the session + if !config.ExtractionSucceeded { + // Entrypoint extraction failed - skip ssm-parent, go straight to direct exec + config.EntrypointPaths = []string{} // Empty - will skip ssm-parent entirely + config.ConfigPaths = []string{} // Empty - will skip -c format entirely + log.Debug("Entrypoint extraction failed — skipping ssm-parent to avoid session kills, will use direct exec") + } + + return config +} + +// isUnknownFlagError checks if the error indicates an unknown flag +func isUnknownFlagError(err error) bool { + if err == nil { + return false + } + errMsg := strings.ToLower(err.Error()) + return strings.Contains(errMsg, ErrUnknownFlag) +} + +// isEntrypointNotFoundError checks if the error indicates the entrypoint was not found +func isEntrypointNotFoundError(err error) bool { + if err == nil { + return false + } + errMsg := strings.ToLower(err.Error()) + return strings.Contains(errMsg, ErrNoFile) || + strings.Contains(errMsg, ErrNotFound) || + strings.Contains(errMsg, ErrForkExec) +} + +// resolveTaskDefinitionName extracts task definition name from task ID if needed +func resolveTaskDefinitionName(cfg ExecConfig) string { + taskDefinitionName := cfg.TaskDefinitionName + if cfg.TaskID != "" && taskDefinitionName == "" { + extractedTaskDef, err := getTaskDefinitionFromTaskID(cfg.Profile, cfg.Cluster, cfg.TaskID) + if err == nil { + taskDefinitionName = extractedTaskDef + log.WithFields(log.Fields{ + "task_id": cfg.TaskID, + "task_definition": taskDefinitionName, + }).Debug("Extracted task definition from task ID") + } else { + log.WithError(err).Debug("Could not extract task definition from task ID, will use path fallback") + } + } + return taskDefinitionName +} + +// createEcstaApp creates and initializes the ecsta application +func createEcstaApp(cfg ExecConfig) (*ecsta.Ecsta, error) { + if err := InitAWS(cfg.Profile); err != nil { + return nil, fmt.Errorf("failed to initialize AWS session: %w", err) + } + + // Get region from session config (already a string in awsv2.Config) + regionStr := sessionConfig.Region + ecstaApp, err := ecsta.New(context.TODO(), regionStr, cfg.Cluster) + if err != nil { + return nil, fmt.Errorf("failed to create ecsta application: %w", err) + } + return ecstaApp, nil +} + +// trySSMParentWithConfig attempts to execute command using ssm-parent with -c flag +func trySSMParentWithConfig(ecstaApp *ecsta.Ecsta, entrypoint string, configPaths []string, command string) execResult { + for _, configPath := range configPaths { + if configPath == "" { + continue // Skip empty config path when trying -c format + } + + fullCommand := fmt.Sprintf("%s -c %s run -- %s", entrypoint, configPath, command) + + execOpt := ecsta.ExecOption{ + Command: fullCommand, + } + + ctx := log.WithFields(log.Fields{ + "entrypoint": entrypoint, + "config_path": configPath, + "format": "with -c flag", + "command": fullCommand, + }) + ctx.Debug("Attempting to execute command with ssm-parent (-c flag format)") + + if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { + // If we get "unknown shorthand flag", this ssm-parent doesn't support -c flag + if isUnknownFlagError(err) { + ctx.WithError(err).Debug("ssm-parent doesn't support -c flag, skipping remaining configs and will try without -c") + return execResult{succeeded: false, err: err} + } + // If entrypoint not found, break config loop and move to next entrypoint + if isEntrypointNotFoundError(err) { + ctx.WithError(err).Debug("entrypoint not found, breaking config loop") + return execResult{succeeded: false, err: err} + } + // Other errors: try without -c flag for this entrypoint + ctx.WithError(err).Debug("ssm-parent -c format failed with other error, will try without -c") + return execResult{succeeded: false, err: err} + } + + // RunExec returned nil - command may have succeeded or failed silently + log.Debug("ssm-parent -c format attempt returned success (may have succeeded or failed silently)") + return execResult{succeeded: true, err: nil} + } + + // No config paths worked + return execResult{succeeded: false, err: fmt.Errorf("no valid config paths")} +} + +// trySSMParentWithoutConfig attempts to execute command using ssm-parent without -c flag +func trySSMParentWithoutConfig(ecstaApp *ecsta.Ecsta, entrypoint string, command string) execResult { + fullCommand := fmt.Sprintf("%s run -- %s", entrypoint, command) + + execOpt := ecsta.ExecOption{ + Command: fullCommand, + } + + ctx := log.WithFields(log.Fields{ + "entrypoint": entrypoint, + "format": "without -c flag", + "command": fullCommand, + }) + ctx.Debug("Attempting to execute command with ssm-parent (simple format)") + + if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { + // If entrypoint not found, try next entrypoint path + if isEntrypointNotFoundError(err) { + ctx.WithError(err).Debug("ssm-parent not found, trying next entrypoint path") + return execResult{succeeded: false, err: err} + } + // Other errors: also try next entrypoint + ctx.WithError(err).Debug("ssm-parent execution failed, trying next entrypoint") + return execResult{succeeded: false, err: err} + } + + // RunExec returned nil - command may have succeeded or failed silently + log.Debug("ssm-parent simple format attempt returned success (may have succeeded or failed silently)") + return execResult{succeeded: true, err: nil} +} + +// trySSMParent attempts to execute command using ssm-parent with all available entrypoints +func trySSMParent(ecstaApp *ecsta.Ecsta, ssmConfig SSMParentConfig, command string) execResult { + entrypointPaths := ssmConfig.EntrypointPaths + configPaths := ssmConfig.ConfigPaths + + if len(entrypointPaths) == 0 { + // No entrypoints to try + return execResult{succeeded: false, err: nil} + } + + var lastErr error + + for _, entrypoint := range entrypointPaths { + // First, try with -c flag format if config paths are available + if len(configPaths) > 0 { + result := trySSMParentWithConfig(ecstaApp, entrypoint, configPaths, command) + if result.succeeded { + return result + } + // If unknown flag error, skip remaining configs and try without -c + if result.err != nil && isUnknownFlagError(result.err) { + // Try without -c flag for this entrypoint + result = trySSMParentWithoutConfig(ecstaApp, entrypoint, command) + if result.succeeded { + return result + } + lastErr = result.err + // If entrypoint not found, try next entrypoint + if isEntrypointNotFoundError(result.err) { + continue + } + // Other errors: also try next entrypoint + continue + } + // If entrypoint not found, try next entrypoint + if result.err != nil && isEntrypointNotFoundError(result.err) { + lastErr = result.err + continue + } + lastErr = result.err + } + + // Try without -c flag format + result := trySSMParentWithoutConfig(ecstaApp, entrypoint, command) + if result.succeeded { + return result + } + lastErr = result.err + // If entrypoint not found, try next entrypoint + if result.err != nil && isEntrypointNotFoundError(result.err) { + continue + } + } + + // All attempts failed + return execResult{succeeded: false, err: lastErr} +} + +// tryDirectExecution attempts to execute command directly without ssm-parent +func tryDirectExecution(ecstaApp *ecsta.Ecsta, command string, ssmTried, ssmSucceeded bool) execResult { + // Log appropriate debug message + if ssmTried { + if ssmSucceeded { + log.Debug("ssm-parent appears to have succeeded, but attempting direct execution to ensure working session") + } else { + log.Debug("Attempting direct execution to verify/fallback from ssm-parent (handling silent failures)") + } + } else { + log.Debug("Attempting to execute command directly (ssm-parent not available)") + } + + execOpt := ecsta.ExecOption{ + Command: command, + } + + if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { + return execResult{succeeded: false, err: err} + } + + return execResult{succeeded: true, err: nil} +} + +// handleExecutionResults determines the final outcome from ssm-parent and direct execution results +func handleExecutionResults(ssmResult, directResult execResult) error { + // Always prefer direct execution result if it succeeded + if directResult.succeeded { + // Log appropriate message based on whether ssm-parent was attempted + if ssmResult.succeeded { + log.Info("Command executed successfully (direct execution - ssm-parent may have also worked)") + } else if ssmResult.err != nil { + log.Info("Command executed successfully (direct execution - ssm-parent may have failed silently)") + } else { + // ssmResult.err == nil and !succeeded means no ssm-parent attempt was made + log.Info("Command executed successfully (direct execution)") + } + return nil +} + + // Direct execution failed + if ssmResult.err != nil { + return fmt.Errorf("failed to execute command: ssm-parent error (%v), direct execution error (%w)", ssmResult.err, directResult.err) + } + return fmt.Errorf("failed to execute command: %w", directResult.err) +} + +// ExecConfig holds configuration for ExecFargate +type ExecConfig struct { + Profile string + Cluster string + Command string + TaskID string + TaskDefinitionName string + ContainerName string +} + // ExecFargate executes a command in a specified container on an ECS Fargate service // taskID, taskDefinitionName and containerName are optional - if provided, will extract entrypoint from task definition -func ExecFargate(profile, cluster, command string, taskID, taskDefinitionName, containerName string) error { - - if err := InitAWS(profile); err != nil { - return fmt.Errorf("failed to initialize AWS session: %w", err) - } +func ExecFargate(cfg ExecConfig) error { + // Setup + ecstaApp, err := createEcstaApp(cfg) + if err != nil { + return err + } - // Get region from session config (already a string in awsv2.Config) - regionStr := sessionConfig.Region - ecstaApp, err := ecsta.New(context.TODO(), regionStr, cluster) - if err != nil { - return fmt.Errorf("failed to create ecsta application: %w", err) - } + taskDefName := resolveTaskDefinitionName(cfg) + ssmConfig := determineSSMParentConfig(cfg.Profile, cfg.Cluster, taskDefName, cfg.ContainerName) - // Try to get task definition from task ID if provided - if taskID != "" && taskDefinitionName == "" { - extractedTaskDef, err := getTaskDefinitionFromTaskID(profile, cluster, taskID) - if err == nil { - taskDefinitionName = extractedTaskDef - log.WithFields(log.Fields{ - "task_id": taskID, - "task_definition": taskDefinitionName, - }).Debug("Extracted task definition from task ID") - } else { - log.WithError(err).Debug("Could not extract task definition from task ID, will use path fallback") - } - } - - // Try to extract entrypoint and config from task definition if provided - var entrypointPaths []string - var configPaths []string - extractionSucceeded := false - - if taskDefinitionName != "" && containerName != "" { - extractedEntrypoint, extractedConfig, err := extractEntrypointFromTaskDefinition(profile, cluster, taskDefinitionName, containerName) - if err == nil { - extractionSucceeded = true - // Only try the extracted entrypoint - don't try unknown fallback paths - // Unknown paths can kill the ECS Exec session if they don't exist - entrypointPaths = []string{extractedEntrypoint} - - // Detect -c flag support: if extractedConfig is not empty, container supports -c - supportsCFlag := (extractedConfig != "") - - if supportsCFlag { - // Container supports -c flag, use extracted config and fallbacks - configPaths = []string{ - extractedConfig, // Use extracted config first - "/app/.ssm-parent.yaml", - "/.ssm-parent.yaml", - "", - } - } else { - // Container does NOT support -c flag, skip all -c attempts - configPaths = []string{} // Empty - will skip -c format entirely - log.Debug("Container does not support -c flag (not found in ENTRYPOINT), skipping all -c attempts") - } - - log.WithFields(log.Fields{ - "entrypoint": extractedEntrypoint, - "config": extractedConfig, - "supports_c_flag": supportsCFlag, - }).Debug("Extracted entrypoint and config from task definition") - } else { - log.WithError(err).Debug("Could not extract entrypoint from task definition, skipping ssm-parent (will use direct exec)") - } - } - - // When extraction fails, skip ssm-parent entirely to avoid session kills - // Unknown entrypoint paths can cause "fork/exec ... no such file" which kills the session - if !extractionSucceeded { - // Entrypoint extraction failed - skip ssm-parent, go straight to direct exec - entrypointPaths = []string{} // Empty - will skip ssm-parent entirely - configPaths = []string{} // Empty - will skip -c format entirely - log.Debug("Entrypoint extraction failed — skipping ssm-parent to avoid session kills, will use direct exec") - } + // Execute + ssmResult := trySSMParent(ecstaApp, ssmConfig, cfg.Command) + ssmTried := len(ssmConfig.EntrypointPaths) > 0 + ssmSucceeded := ssmResult.succeeded + directResult := tryDirectExecution(ecstaApp, cfg.Command, ssmTried, ssmSucceeded) - var ssmParentTried bool - var ssmParentErr error - var ssmParentSucceeded bool - - // Try with ssm-parent first (preferred for env vars) - // Support both formats: with -c flag (springload) and without (madewithwagtail) - // Each entrypoint/config combination is tried exactly once - for _, entrypoint := range entrypointPaths { - ssmParentTried = true - var cFlagNotSupported bool - var entrypointFound bool - var triedWithoutCFlag bool - - // First, try with -c flag format (for projects like springload) - // Format: ssm-parent -c .ssm-parent.yaml run -- - // Only try if we have config paths and haven't determined -c is unsupported - if !cFlagNotSupported && len(configPaths) > 0 { - for _, configPath := range configPaths { - if configPath == "" { - continue // Skip empty config path when trying -c format - } - - // If we already determined -c is not supported, skip remaining config paths - if cFlagNotSupported { - break - } - - fullCommand := fmt.Sprintf("%s -c %s run -- %s", entrypoint, configPath, command) - - execOpt := ecsta.ExecOption{ - Command: fullCommand, - } - - ctx := log.WithFields(log.Fields{ - "entrypoint": entrypoint, - "config_path": configPath, - "format": "with -c flag", - "command": fullCommand, - }) - ctx.Debug("Attempting to execute command with ssm-parent (-c flag format)") - - if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { - ssmParentErr = err - errMsg := strings.ToLower(err.Error()) - // If we get "unknown shorthand flag", this ssm-parent doesn't support -c flag - // Skip all remaining config paths and try without -c for this entrypoint - if strings.Contains(errMsg, "unknown shorthand flag") { - ctx.WithError(err).Debug("ssm-parent doesn't support -c flag, skipping remaining configs and will try without -c") - cFlagNotSupported = true - break // Break out of config loop immediately - } - // If entrypoint not found, break config loop and move to next entrypoint - // (trying other config paths is pointless since the binary itself is missing) - if strings.Contains(errMsg, "no such file or directory") || - strings.Contains(errMsg, "not found") || - strings.Contains(errMsg, "fork/exec") { - ctx.WithError(err).Debug("entrypoint not found, breaking config loop (will try without -c, then next entrypoint if that also fails)") - break // Break config loop - we'll try without -c for this entrypoint, then move to next if that fails - } - // Other errors: try without -c flag for this entrypoint - ctx.WithError(err).Debug("ssm-parent -c format failed with other error, will try without -c") - cFlagNotSupported = true - break - } else { - // RunExec returned nil - command may have succeeded or failed silently - // Mark that we tried this entrypoint and it exists - entrypointFound = true - ssmParentSucceeded = true // Track that ssm-parent didn't error (may have worked) - log.Debug("ssm-parent -c format attempt returned success (may have succeeded or failed silently)") - // Break out of config loop - we've found a working entrypoint/config combination - // We'll still try direct execution as fallback to ensure working session - break - } - } - } - - // Second, try without -c flag format (for projects like madewithwagtail) - // Format: ssm-parent run -- - // Try this if: - // 1. -c flag is not supported (detected above) - // 2. No config paths available - // 3. We haven't tried without -c yet for this entrypoint - if !triedWithoutCFlag && (cFlagNotSupported || len(configPaths) == 0 || (len(configPaths) == 1 && configPaths[0] == "")) { - triedWithoutCFlag = true - fullCommand := fmt.Sprintf("%s run -- %s", entrypoint, command) - - execOpt := ecsta.ExecOption{ - Command: fullCommand, - } - - ctx := log.WithFields(log.Fields{ - "entrypoint": entrypoint, - "format": "without -c flag", - "command": fullCommand, - }) - ctx.Debug("Attempting to execute command with ssm-parent (simple format)") - - if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { - ssmParentErr = err - errMsg := strings.ToLower(err.Error()) - // If entrypoint not found, try next entrypoint path - if strings.Contains(errMsg, "no such file or directory") || - strings.Contains(errMsg, "not found") || - strings.Contains(errMsg, "fork/exec") { - ctx.WithError(err).Debug("ssm-parent not found, trying next entrypoint path") - continue // Move to next entrypoint - } - // Other errors: also try next entrypoint - ctx.WithError(err).Debug("ssm-parent execution failed, trying next entrypoint") - continue - } else { - // RunExec returned nil - command may have succeeded or failed silently - entrypointFound = true - ssmParentSucceeded = true // Track that ssm-parent didn't error (may have worked) - log.Debug("ssm-parent simple format attempt returned success (may have succeeded or failed silently)") - // Continue to direct execution fallback to ensure working session - } - } - - // If we found the entrypoint (even if execution may have failed silently), - // we've tried both formats for this entrypoint, so move on - if entrypointFound { - // We've exhausted options for this entrypoint, continue to direct execution - break - } - } - - // Always try direct execution as fallback to handle silent failures - // This ensures we get a working session even if ssm-parent failed silently - // According to plan: "Always try direct execution after ssm-parent attempts (even if they return success)" - if ssmParentTried { - if ssmParentSucceeded { - log.Debug("ssm-parent appears to have succeeded, but attempting direct execution to ensure working session") - } else { - log.Debug("Attempting direct execution to verify/fallback from ssm-parent (handling silent failures)") - } - } else { - log.Debug("Attempting to execute command directly (ssm-parent not available)") - } - - execOpt := ecsta.ExecOption{ - Command: command, - } - - if err := ecstaApp.RunExec(context.Background(), &execOpt); err != nil { - if ssmParentErr != nil { - return fmt.Errorf("failed to execute command: ssm-parent error (%v), direct execution error (%w)", ssmParentErr, err) - } - return fmt.Errorf("failed to execute command: %w", err) - } - - // Direct execution succeeded - this ensures we have a working session - // Note: If ssm-parent also worked, we prefer direct execution here because - // we can't verify ssm-parent actually worked due to silent failures - if ssmParentTried { - if ssmParentSucceeded { - log.Info("Command executed successfully (direct execution - ssm-parent may have also worked)") - } else { - log.Info("Command executed successfully (direct execution - ssm-parent may have failed silently)") - } - } else { - log.Info("Command executed successfully (direct execution)") - } - return nil + // Handle results + return handleExecutionResults(ssmResult, directResult) } From dce0f5eaf93b4bf368bb42220e32e30ee5613876 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Wed, 26 Nov 2025 10:44:44 +1300 Subject: [PATCH 13/14] gh actions fix --- .github/workflows/build_test.yml | 21 ++++++++------------- .github/workflows/release.yml | 4 ++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index d38a6e8..89ded47 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -9,25 +9,20 @@ jobs: name: test if ecs-tool can be built runs-on: ubuntu-latest steps: - - - name: checkout - uses: actions/checkout@v2 - - - name: set up Go - uses: actions/setup-go@v1 + - name: checkout + uses: actions/checkout@v4 + - name: set up Go + uses: actions/setup-go@v5 with: go-version: 1.21.x - - - name: cache modules - uses: actions/cache@v2 + - name: cache modules + uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - - name: download dependencies + - name: download dependencies run: go mod download - - - name: build the app + - name: build the app run: go build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20c1123..fc26338 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,12 +10,12 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 # needed for tags - name: set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v5 with: go-version: 1.21.x - From 960393892c43749907f5dd1fca7bde415fa7f767 Mon Sep 17 00:00:00 2001 From: Andrei Vsiakikh Date: Wed, 26 Nov 2025 13:50:28 +1300 Subject: [PATCH 14/14] Refactor exec command and improve code quality - Refactor ExecFargate for KISS/DRY compliance: * Extract helper functions (trySSMParent, tryDirectExecution, etc.) * Replace boolean flags with execResult struct * Reduce ExecFargate from ~180 lines to ~20 lines * Improve error handling and readability - Fix container name handling in exec command: * Add validation when container_name flag is not provided * Ensure command is not empty after container name extraction * Provide clear error messages for invalid usage - Remove debug print statement from cmd/run.go - Fix redundant string wrapping in lib/runFargate.go: * Remove unnecessary aws.String()/aws.StringValue() wrapping * Use strings directly in log fields - Add documentation comment about public IP usage in Fargate * Note that IPv6 implementation will change this behavior - Fix linter warnings and improve code consistency --- cmd/exec.go | 17 ++- cmd/run.go | 2 - lib/exec.go | 286 ++++++++++++++++++++------------------- lib/runFargate.go | 333 +++++++++++++++++++++++----------------------- lib/util.go | 80 ++++++----- 5 files changed, 368 insertions(+), 350 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index 594fa53..c354436 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -18,14 +18,27 @@ var execCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { viper.SetDefault("run.launch_type", "FARGATE") - //var containerName string + var containerName string var commandArgs []string if name := viper.GetString("container_name"); name == "" { + // If container_name not provided via flag, use first arg as container name + if len(args) < 2 { + log.Error("When --container_name is not provided, at least 2 arguments are required: ") + os.Exit(1) + } + containerName = args[0] commandArgs = args[1:] } else { + containerName = name commandArgs = args } + // Validate that we have a command to execute + if len(commandArgs) == 0 { + log.Error("No command provided to execute") + os.Exit(1) + } + // Join the commandArgs to form a single command string commandString := strings.Join(commandArgs, " ") @@ -35,7 +48,7 @@ var execCmd = &cobra.Command{ Command: commandString, TaskID: viper.GetString("task_id"), TaskDefinitionName: viper.GetString("task_definition"), - ContainerName: viper.GetString("container_name"), + ContainerName: containerName, }) if err != nil { log.WithError(err).Error("Can't execute command in Fargate mode") diff --git a/cmd/run.go b/cmd/run.go index fc353f2..1803979 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/springload/ecs-tool/lib" - "fmt" ) var runCmd = &cobra.Command{ @@ -57,5 +56,4 @@ func init() { viper.BindPFlag("log_group", runCmd.PersistentFlags().Lookup("log_group")) viper.BindPFlag("container_name", runCmd.PersistentFlags().Lookup("container_name")) //viper.BindPFlag("task_definition", runCmd.PersistentFlags().Lookup("task_definition")) - fmt.Println("Default launch_type set to:", viper.GetString("run.launch_type")) } diff --git a/lib/exec.go b/lib/exec.go index a8e633d..1080a7a 100644 --- a/lib/exec.go +++ b/lib/exec.go @@ -29,144 +29,144 @@ var sessionConfig awsv2.Config // Variable for session configuration // InitAWS initializes a new AWS session with the specified profile for Ecsta realization func InitAWS(profile string) error { - if sessionInstance == nil { - cfg, err := config.LoadDefaultConfig(context.TODO(), - config.WithSharedConfigProfile(profile), - ) - if err != nil { - return fmt.Errorf("failed to load configuration: %w", err) - } - os.Setenv("AWS_PROFILE", profile) //required for aws sdk - sessionInstance = ecsv2.NewFromConfig(cfg) - sessionConfig = cfg // Save session configuration - } - return nil + if sessionInstance == nil { + cfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithSharedConfigProfile(profile), + ) + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + os.Setenv("AWS_PROFILE", profile) //required for aws sdk + sessionInstance = ecsv2.NewFromConfig(cfg) + sessionConfig = cfg // Save session configuration + } + return nil } // getTaskDefinitionFromTaskID gets the task definition ARN from a task ID and extracts the family name func getTaskDefinitionFromTaskID(profile, cluster, taskID string) (taskDefinitionName string, err error) { - err = makeSession(profile) - if err != nil { - return "", fmt.Errorf("failed to create session: %w", err) - } - - svc := ecsv1.New(localSession) - - // List tasks to find the one matching the task ID - listResult, err := svc.ListTasks(&ecsv1.ListTasksInput{ - Cluster: aws.String(cluster), - }) - if err != nil { - return "", fmt.Errorf("failed to list tasks: %w", err) - } - - if len(listResult.TaskArns) == 0 { - return "", fmt.Errorf("no tasks found in cluster") - } - - // Find task that matches the task ID (task ID is usually a prefix of the full ARN) - var matchingTaskArn *string - for _, taskArn := range listResult.TaskArns { - taskArnStr := aws.StringValue(taskArn) - // Task ID is usually the last part of the ARN after the last / - parts := strings.Split(taskArnStr, "/") - if len(parts) > 0 { - taskArnID := parts[len(parts)-1] - // Check if task ID matches (task ID is always a prefix of the ARN ID) - if strings.HasPrefix(taskArnID, taskID) { - matchingTaskArn = taskArn - break - } - } - } - - if matchingTaskArn == nil { - return "", fmt.Errorf("task ID %s not found in cluster", taskID) - } - - // Describe the task to get task definition ARN - describeResult, err := svc.DescribeTasks(&ecsv1.DescribeTasksInput{ - Cluster: aws.String(cluster), - Tasks: []*string{matchingTaskArn}, - }) - if err != nil { - return "", fmt.Errorf("failed to describe task: %w", err) - } - - if len(describeResult.Tasks) == 0 { - return "", fmt.Errorf("task not found") - } - - taskDefinitionArn := aws.StringValue(describeResult.Tasks[0].TaskDefinitionArn) - - // Extract task definition family name from ARN using proper ARN parsing - // ARN format: arn:aws:ecs:region:account:task-definition/family:revision - parsed, err := arn.Parse(taskDefinitionArn) - if err != nil { - return "", fmt.Errorf("invalid task definition ARN: %w", err) - } - - // parsed.Resource == "task-definition/family:revision" - parts := strings.Split(parsed.Resource, "/") - if len(parts) != 2 { - return "", fmt.Errorf("invalid task definition ARN format: %s", taskDefinitionArn) - } - - // Get family:revision and extract just the family name - familyRevision := parts[1] - familyParts := strings.Split(familyRevision, ":") - taskDefinitionName = familyParts[0] - - return taskDefinitionName, nil + err = makeSession(profile) + if err != nil { + return "", fmt.Errorf("failed to create session: %w", err) + } + + svc := ecsv1.New(localSession) + + // List tasks to find the one matching the task ID + listResult, err := svc.ListTasks(&ecsv1.ListTasksInput{ + Cluster: aws.String(cluster), + }) + if err != nil { + return "", fmt.Errorf("failed to list tasks: %w", err) + } + + if len(listResult.TaskArns) == 0 { + return "", fmt.Errorf("no tasks found in cluster") + } + + // Find task that matches the task ID (task ID is usually a prefix of the full ARN) + var matchingTaskArn *string + for _, taskArn := range listResult.TaskArns { + taskArnStr := aws.StringValue(taskArn) + // Task ID is usually the last part of the ARN after the last / + parts := strings.Split(taskArnStr, "/") + if len(parts) > 0 { + taskArnID := parts[len(parts)-1] + // Check if task ID matches (task ID is always a prefix of the ARN ID) + if strings.HasPrefix(taskArnID, taskID) { + matchingTaskArn = taskArn + break + } + } + } + + if matchingTaskArn == nil { + return "", fmt.Errorf("task ID %s not found in cluster", taskID) + } + + // Describe the task to get task definition ARN + describeResult, err := svc.DescribeTasks(&ecsv1.DescribeTasksInput{ + Cluster: aws.String(cluster), + Tasks: []*string{matchingTaskArn}, + }) + if err != nil { + return "", fmt.Errorf("failed to describe task: %w", err) + } + + if len(describeResult.Tasks) == 0 { + return "", fmt.Errorf("task not found") + } + + taskDefinitionArn := aws.StringValue(describeResult.Tasks[0].TaskDefinitionArn) + + // Extract task definition family name from ARN using proper ARN parsing + // ARN format: arn:aws:ecs:region:account:task-definition/family:revision + parsed, err := arn.Parse(taskDefinitionArn) + if err != nil { + return "", fmt.Errorf("invalid task definition ARN: %w", err) + } + + // parsed.Resource == "task-definition/family:revision" + parts := strings.Split(parsed.Resource, "/") + if len(parts) != 2 { + return "", fmt.Errorf("invalid task definition ARN format: %s", taskDefinitionArn) + } + + // Get family:revision and extract just the family name + familyRevision := parts[1] + familyParts := strings.Split(familyRevision, ":") + taskDefinitionName = familyParts[0] + + return taskDefinitionName, nil } // extractEntrypointFromTaskDefinition extracts ssm-parent entrypoint and config from task definition func extractEntrypointFromTaskDefinition(profile, cluster, taskDefinitionName, containerName string) (entrypoint string, configPath string, err error) { - // Use AWS SDK v1 for compatibility with existing code - err = makeSession(profile) - if err != nil { - return "", "", fmt.Errorf("failed to create session: %w", err) - } - - svc := ecsv1.New(localSession) - - describeResult, err := svc.DescribeTaskDefinition(&ecsv1.DescribeTaskDefinitionInput{ - TaskDefinition: aws.String(taskDefinitionName), - }) - if err != nil { - return "", "", fmt.Errorf("failed to describe task definition: %w", err) - } - - // Find the container definition - for _, containerDef := range describeResult.TaskDefinition.ContainerDefinitions { - if aws.StringValue(containerDef.Name) == containerName { - // Check EntryPoint field - if len(containerDef.EntryPoint) > 0 { - // EntryPoint is typically: ["/sbin/ssm-parent", "run", "-e", "-p", "...", "--", "su-exec", "www"] - // We want to extract the ssm-parent path (first element) and config if present - entrypoint = aws.StringValue(containerDef.EntryPoint[0]) - - // Look for -c flag in EntryPoint to find config path - for i, arg := range containerDef.EntryPoint { - if i > 0 && aws.StringValue(arg) == "-c" && i+1 < len(containerDef.EntryPoint) { - configPath = aws.StringValue(containerDef.EntryPoint[i+1]) - break - } - } - - return entrypoint, configPath, nil - } - } - } - - return "", "", fmt.Errorf("container %s not found or has no entrypoint", containerName) + // Use AWS SDK v1 for compatibility with existing code + err = makeSession(profile) + if err != nil { + return "", "", fmt.Errorf("failed to create session: %w", err) + } + + svc := ecsv1.New(localSession) + + describeResult, err := svc.DescribeTaskDefinition(&ecsv1.DescribeTaskDefinitionInput{ + TaskDefinition: aws.String(taskDefinitionName), + }) + if err != nil { + return "", "", fmt.Errorf("failed to describe task definition: %w", err) + } + + // Find the container definition + for _, containerDef := range describeResult.TaskDefinition.ContainerDefinitions { + if aws.StringValue(containerDef.Name) == containerName { + // Check EntryPoint field + if len(containerDef.EntryPoint) > 0 { + // EntryPoint is typically: ["/sbin/ssm-parent", "run", "-e", "-p", "...", "--", "su-exec", "www"] + // We want to extract the ssm-parent path (first element) and config if present + entrypoint = aws.StringValue(containerDef.EntryPoint[0]) + + // Look for -c flag in EntryPoint to find config path + for i, arg := range containerDef.EntryPoint { + if i > 0 && aws.StringValue(arg) == "-c" && i+1 < len(containerDef.EntryPoint) { + configPath = aws.StringValue(containerDef.EntryPoint[i+1]) + break + } + } + + return entrypoint, configPath, nil + } + } + } + + return "", "", fmt.Errorf("container %s not found or has no entrypoint", containerName) } // SSMParentConfig holds the configuration for ssm-parent execution type SSMParentConfig struct { - EntrypointPaths []string - ConfigPaths []string - SupportsCFlag bool + EntrypointPaths []string + ConfigPaths []string + SupportsCFlag bool ExtractionSucceeded bool } @@ -259,7 +259,7 @@ func resolveTaskDefinitionName(cfg ExecConfig) string { if err == nil { taskDefinitionName = extractedTaskDef log.WithFields(log.Fields{ - "task_id": cfg.TaskID, + "task_id": cfg.TaskID, "task_definition": taskDefinitionName, }).Debug("Extracted task definition from task ID") } else { @@ -374,6 +374,8 @@ func trySSMParent(ecstaApp *ecsta.Ecsta, ssmConfig SSMParentConfig, command stri var lastErr error for _, entrypoint := range entrypointPaths { + var entrypointErr error + // First, try with -c flag format if config paths are available if len(configPaths) > 0 { result := trySSMParentWithConfig(ecstaApp, entrypoint, configPaths, command) @@ -387,12 +389,14 @@ func trySSMParent(ecstaApp *ecsta.Ecsta, ssmConfig SSMParentConfig, command stri if result.succeeded { return result } - lastErr = result.err + entrypointErr = result.err // If entrypoint not found, try next entrypoint - if isEntrypointNotFoundError(result.err) { + if result.err != nil && isEntrypointNotFoundError(result.err) { + lastErr = entrypointErr continue } // Other errors: also try next entrypoint + lastErr = entrypointErr continue } // If entrypoint not found, try next entrypoint @@ -400,7 +404,8 @@ func trySSMParent(ecstaApp *ecsta.Ecsta, ssmConfig SSMParentConfig, command stri lastErr = result.err continue } - lastErr = result.err + // Store error for potential use if without -c also fails + entrypointErr = result.err } // Try without -c flag format @@ -408,10 +413,16 @@ func trySSMParent(ecstaApp *ecsta.Ecsta, ssmConfig SSMParentConfig, command stri if result.succeeded { return result } - lastErr = result.err - // If entrypoint not found, try next entrypoint - if result.err != nil && isEntrypointNotFoundError(result.err) { - continue + // Store error for final return if all attempts fail + if result.err != nil { + lastErr = result.err + // If entrypoint not found, try next entrypoint + if isEntrypointNotFoundError(result.err) { + continue + } + } else if entrypointErr != nil { + // Use error from -c attempt if available + lastErr = entrypointErr } } @@ -444,6 +455,9 @@ func tryDirectExecution(ecstaApp *ecsta.Ecsta, command string, ssmTried, ssmSucc } // handleExecutionResults determines the final outcome from ssm-parent and direct execution results +// Note: ecsta.RunExec may return nil even when the command inside the container fails silently. +// We handle this by always attempting direct execution as a fallback, which provides a working +// interactive session even if ssm-parent failed silently. func handleExecutionResults(ssmResult, directResult execResult) error { // Always prefer direct execution result if it succeeded if directResult.succeeded { @@ -456,8 +470,8 @@ func handleExecutionResults(ssmResult, directResult execResult) error { // ssmResult.err == nil and !succeeded means no ssm-parent attempt was made log.Info("Command executed successfully (direct execution)") } - return nil -} + return nil + } // Direct execution failed if ssmResult.err != nil { diff --git a/lib/runFargate.go b/lib/runFargate.go index bb6687a..b3c06a8 100644 --- a/lib/runFargate.go +++ b/lib/runFargate.go @@ -1,179 +1,177 @@ package lib import ( - "fmt" - "github.com/apex/log" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ecs" - "github.com/aws/aws-sdk-go/service/ec2" + "fmt" "strings" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" ) // RunFargate runs the specified one-off task in the cluster using the task definition func RunFargate(profile, cluster, service, taskDefinitionName, imageTag string, imageTags []string, workDir, containerName, awslogGroup, launchType string, securityGroupFilter string, args []string) (exitCode int, err error) { - err = makeSession(profile) - if err != nil { - return 1, err - } - ctx := log.WithFields(log.Fields{"task_definition": taskDefinitionName}) - - - svc := ecs.New(localSession) - svcEC2 := ec2.New(localSession) // Assuming makeSession initializes localSession - - // Fetch subnets and security groups - subnets, err := fetchSubnetsByTag(svcEC2, "Tier", "private") - if err != nil { - log.WithError(err).Error("Failed to fetch subnets by private tag") - return 1, err - } - if len(subnets) == 0 { - subnets, err = fetchSubnetsByTag(svcEC2, "Tier", "public") - - if err != nil { - log.WithError(err).Error("Failed to fetch subnets by public tag") - return 1, err - } -} -securityGroups, err := fetchSecurityGroupsByName(svcEC2, securityGroupFilter) + err = makeSession(profile) + if err != nil { + return 1, err + } + ctx := log.WithFields(log.Fields{"task_definition": taskDefinitionName}) + + svc := ecs.New(localSession) + svcEC2 := ec2.New(localSession) // Assuming makeSession initializes localSession + + // Fetch subnets and security groups + subnets, err := fetchSubnetsByTag(svcEC2, "Tier", "private") if err != nil { - log.WithError(err).Error("Failed to fetch security groups by name") - return 1, err - } - // Set up network configuration - networkConfiguration := &ecs.NetworkConfiguration{ - AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ - Subnets: subnets, - SecurityGroups: securityGroups, - AssignPublicIp: aws.String("ENABLED"), // or "ENABLED" if public IP is needed - }, - } + log.WithError(err).Error("Failed to fetch subnets by private tag") + return 1, err + } + if len(subnets) == 0 { + subnets, err = fetchSubnetsByTag(svcEC2, "Tier", "public") + if err != nil { + log.WithError(err).Error("Failed to fetch subnets by public tag") + return 1, err + } + } + securityGroups, err := fetchSecurityGroupsByName(svcEC2, securityGroupFilter) + if err != nil { + log.WithError(err).Error("Failed to fetch security groups by name") + return 1, err + } + // Set up network configuration + networkConfiguration := &ecs.NetworkConfiguration{ + AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ + Subnets: subnets, + SecurityGroups: securityGroups, + // Currently we always use public IPs for Fargate tasks to ensure internet access. + // This will be changed when IPv6 support is implemented, as IPv6 provides global + // addressing and may eliminate the need for public IPs depending on subnet configuration. + AssignPublicIp: aws.String("ENABLED"), + }, + } ctx.WithFields(log.Fields{ - "Cluster": aws.StringValue(aws.String(cluster)), - "TaskDefinition": aws.StringValue(aws.String(taskDefinitionName)), - "LaunchType": aws.StringValue(aws.String(launchType)), - "Subnets": fmt.Sprint(subnets), - "SecurityGroups": fmt.Sprint(securityGroups), - "AssignPublicIP": aws.StringValue(networkConfiguration.AwsvpcConfiguration.AssignPublicIp), -}).Info("Attempting to launch task") - - describeResult, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ - TaskDefinition: aws.String(taskDefinitionName), - }) - if err != nil { - ctx.WithError(err).Error("Can't get task definition") - return 1, err - } - taskDefinition := describeResult.TaskDefinition - - var foundContainerName bool - if err := modifyContainerDefinitionImages(imageTag, imageTags, workDir, taskDefinition.ContainerDefinitions, ctx); err != nil { - return 1, err - } - for n, containerDefinition := range taskDefinition.ContainerDefinitions { - if aws.StringValue(containerDefinition.Name) == containerName { - foundContainerName = true - // Use shell execution to interpret the command with any arguments - commandLine := strings.Join(args, " ") // Join args into a single command line - containerDefinition.Command = []*string{aws.String("sh"), aws.String("-c"), aws.String(commandLine)} - if awslogGroup != "" { - containerDefinition.LogConfiguration = &ecs.LogConfiguration{ - LogDriver: aws.String("awslogs"), - Options: map[string]*string{ - "awslogs-region": localSession.Config.Region, - "awslogs-group": aws.String(awslogGroup), - "awslogs-stream-prefix": aws.String(cluster), - }, - } - } - taskDefinition.ContainerDefinitions[n] = containerDefinition // Update the container definition - - } -} - if !foundContainerName { - err := fmt.Errorf("Can't find container with specified name in the task definition") - ctx.WithFields(log.Fields{"container_name": containerName}).Error(err.Error()) - return 1, err - } - - registerResult, err := svc.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{ - ContainerDefinitions: taskDefinition.ContainerDefinitions, - Cpu: taskDefinition.Cpu, - ExecutionRoleArn: taskDefinition.ExecutionRoleArn, - Family: taskDefinition.Family, - Memory: taskDefinition.Memory, - NetworkMode: taskDefinition.NetworkMode, - PlacementConstraints: taskDefinition.PlacementConstraints, - RequiresCompatibilities: taskDefinition.Compatibilities, - TaskRoleArn: taskDefinition.TaskRoleArn, - Volumes: taskDefinition.Volumes, - }) - if err != nil { - ctx.WithError(err).Error("Can't register task definition") - return 1, err - } - ctx.WithField("task_definition_arn", aws.StringValue(registerResult.TaskDefinition.TaskDefinitionArn)).Debug("Registered the task definition") - - // Deregister the task definition - defer func() { - _, err = svc.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{ - TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, - }) - if err != nil { - ctx.WithError(err).Error("Can't deregister task definition") - } - }() - - // Run the task with network configuration - runTaskInput := ecs.RunTaskInput{ - Cluster: aws.String(cluster), - TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, - Count: aws.Int64(1), - StartedBy: aws.String("go-deploy"), - LaunchType: aws.String(launchType), - NetworkConfiguration: networkConfiguration, - } - - runResult, err := svc.RunTask(&runTaskInput) - if err != nil { - ctx.WithError(err).Error("Can't run specified task") - return 1, err - } - if len(runResult.Tasks) == 0 { - ctx.Error("No tasks could be run. Please check if the ECS cluster has enough resources") - return 1, err - } - - ctx.Info("Waiting for the task to finish") - var tasks []*string - for _, task := range runResult.Tasks { - tasks = append(tasks, task.TaskArn) - ctx.WithField("task_arn", aws.StringValue(task.TaskArn)).Debug("Started task") - } - tasksInput := &ecs.DescribeTasksInput{ - Cluster: aws.String(cluster), - Tasks: tasks, - } - err = svc.WaitUntilTasksStopped(tasksInput) - if err != nil { - ctx.WithError(err).Error("The waiter has been finished with an error") - exitCode = 3 - return exitCode, err - } - - tasksOutput, err := svc.DescribeTasks(tasksInput) - if err != nil { - ctx.WithError(err).Error("Can't describe stopped tasks") - return 1, err - } - - - - - -for _, task := range tasksOutput.Tasks { + "Cluster": cluster, + "TaskDefinition": taskDefinitionName, + "LaunchType": launchType, + "Subnets": fmt.Sprint(subnets), + "SecurityGroups": fmt.Sprint(securityGroups), + "AssignPublicIP": aws.StringValue(networkConfiguration.AwsvpcConfiguration.AssignPublicIp), + }).Info("Attempting to launch task") + + describeResult, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ + TaskDefinition: aws.String(taskDefinitionName), + }) + if err != nil { + ctx.WithError(err).Error("Can't get task definition") + return 1, err + } + taskDefinition := describeResult.TaskDefinition + + var foundContainerName bool + if err := modifyContainerDefinitionImages(imageTag, imageTags, workDir, taskDefinition.ContainerDefinitions, ctx); err != nil { + return 1, err + } + for n, containerDefinition := range taskDefinition.ContainerDefinitions { + if aws.StringValue(containerDefinition.Name) == containerName { + foundContainerName = true + // Use shell execution to interpret the command with any arguments + commandLine := strings.Join(args, " ") // Join args into a single command line + containerDefinition.Command = []*string{aws.String("sh"), aws.String("-c"), aws.String(commandLine)} + if awslogGroup != "" { + containerDefinition.LogConfiguration = &ecs.LogConfiguration{ + LogDriver: aws.String("awslogs"), + Options: map[string]*string{ + "awslogs-region": localSession.Config.Region, + "awslogs-group": aws.String(awslogGroup), + "awslogs-stream-prefix": aws.String(cluster), + }, + } + } + taskDefinition.ContainerDefinitions[n] = containerDefinition // Update the container definition + + } + } + if !foundContainerName { + err := fmt.Errorf("Can't find container with specified name in the task definition") + ctx.WithFields(log.Fields{"container_name": containerName}).Error(err.Error()) + return 1, err + } + + registerResult, err := svc.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{ + ContainerDefinitions: taskDefinition.ContainerDefinitions, + Cpu: taskDefinition.Cpu, + ExecutionRoleArn: taskDefinition.ExecutionRoleArn, + Family: taskDefinition.Family, + Memory: taskDefinition.Memory, + NetworkMode: taskDefinition.NetworkMode, + PlacementConstraints: taskDefinition.PlacementConstraints, + RequiresCompatibilities: taskDefinition.Compatibilities, + TaskRoleArn: taskDefinition.TaskRoleArn, + Volumes: taskDefinition.Volumes, + }) + if err != nil { + ctx.WithError(err).Error("Can't register task definition") + return 1, err + } + ctx.WithField("task_definition_arn", aws.StringValue(registerResult.TaskDefinition.TaskDefinitionArn)).Debug("Registered the task definition") + + // Deregister the task definition + defer func() { + _, err = svc.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{ + TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, + }) + if err != nil { + ctx.WithError(err).Error("Can't deregister task definition") + } + }() + + // Run the task with network configuration + runTaskInput := ecs.RunTaskInput{ + Cluster: aws.String(cluster), + TaskDefinition: registerResult.TaskDefinition.TaskDefinitionArn, + Count: aws.Int64(1), + StartedBy: aws.String("go-deploy"), + LaunchType: aws.String(launchType), + NetworkConfiguration: networkConfiguration, + } + + runResult, err := svc.RunTask(&runTaskInput) + if err != nil { + ctx.WithError(err).Error("Can't run specified task") + return 1, err + } + if len(runResult.Tasks) == 0 { + ctx.Error("No tasks could be run. Please check if the ECS cluster has enough resources") + return 1, err + } + + ctx.Info("Waiting for the task to finish") + var tasks []*string + for _, task := range runResult.Tasks { + tasks = append(tasks, task.TaskArn) + ctx.WithField("task_arn", aws.StringValue(task.TaskArn)).Debug("Started task") + } + tasksInput := &ecs.DescribeTasksInput{ + Cluster: aws.String(cluster), + Tasks: tasks, + } + err = svc.WaitUntilTasksStopped(tasksInput) + if err != nil { + ctx.WithError(err).Error("The waiter has been finished with an error") + exitCode = 3 + return exitCode, err + } + + tasksOutput, err := svc.DescribeTasks(tasksInput) + if err != nil { + ctx.WithError(err).Error("Can't describe stopped tasks") + return 1, err + } + + for _, task := range tasksOutput.Tasks { for _, container := range task.Containers { ctx := log.WithFields(log.Fields{ "container_name": aws.StringValue(container.Name), @@ -195,7 +193,7 @@ for _, task := range tasksOutput.Tasks { if aws.StringValue(container.Name) == containerName { if len(reason) == 0 { exitCode = int(aws.Int64Value(container.ExitCode)) - + if awslogGroup != "" { // get log output taskUUID, err := parseTaskUUID(container.TaskArn) @@ -215,8 +213,5 @@ for _, task := range tasksOutput.Tasks { } } - return exitCode, nil + return exitCode, nil } - - - diff --git a/lib/util.go b/lib/util.go index ad6a292..b480667 100644 --- a/lib/util.go +++ b/lib/util.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" - "github.com/aws/aws-sdk-go/service/ec2" ) var localSession *session.Session @@ -137,47 +137,45 @@ func modifyContainerDefinitionImages(imageTag string, imageTags []string, workDi // fetchSubnetsByTag fetches subnet IDs by a specific tag name and value func fetchSubnetsByTag(svc *ec2.EC2, tagKey, tagValue string) ([]*string, error) { - input := &ec2.DescribeSubnetsInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String(fmt.Sprintf("tag:%s", tagKey)), - Values: []*string{aws.String(tagValue)}, - }, - }, - } - - result, err := svc.DescribeSubnets(input) - if err != nil { - return nil, fmt.Errorf("error describing subnets: %w", err) - } - - var subnets []*string - for _, subnet := range result.Subnets { - subnets = append(subnets, subnet.SubnetId) - } - - return subnets, nil -} + input := &ec2.DescribeSubnetsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String(fmt.Sprintf("tag:%s", tagKey)), + Values: []*string{aws.String(tagValue)}, + }, + }, + } + result, err := svc.DescribeSubnets(input) + if err != nil { + return nil, fmt.Errorf("error describing subnets: %w", err) + } -func fetchSecurityGroupsByName(svc *ec2.EC2, securityGroupFilter string) ([]*string, error) { - // Describe all security groups - input := &ec2.DescribeSecurityGroupsInput{} - - result, err := svc.DescribeSecurityGroups(input) - if err != nil { - return nil, fmt.Errorf("error describing security groups: %w", err) - } - - var securityGroups []*string - // Loop through the security groups and add those that contain the filter in their name - for _, sg := range result.SecurityGroups { - if strings.Contains(*sg.GroupName, securityGroupFilter) { - securityGroups = append(securityGroups, sg.GroupId) - } - } - - // Return the filtered list of security group IDs - return securityGroups, nil + var subnets []*string + for _, subnet := range result.Subnets { + subnets = append(subnets, subnet.SubnetId) + } + + return subnets, nil } +func fetchSecurityGroupsByName(svc *ec2.EC2, securityGroupFilter string) ([]*string, error) { + // Describe all security groups + input := &ec2.DescribeSecurityGroupsInput{} + + result, err := svc.DescribeSecurityGroups(input) + if err != nil { + return nil, fmt.Errorf("error describing security groups: %w", err) + } + + var securityGroups []*string + // Loop through the security groups and add those that contain the filter in their name + for _, sg := range result.SecurityGroups { + if strings.Contains(*sg.GroupName, securityGroupFilter) { + securityGroups = append(securityGroups, sg.GroupId) + } + } + + // Return the filtered list of security group IDs + return securityGroups, nil +}