diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index a686d0d..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.15.x - - - name: cache modules - uses: actions/cache@v2 + go-version: 1.21.x + - 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 10cd397..fc26338 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,14 +10,14 @@ 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.17.x + go-version: 1.21.x - name: run GoReleaser uses: goreleaser/goreleaser-action@v1 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/exec.go b/cmd/exec.go new file mode 100644 index 0000000..c354436 --- /dev/null +++ b/cmd/exec.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "os" + "strings" + + "github.com/apex/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/springload/ecs-tool/lib" +) + +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.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + viper.SetDefault("run.launch_type", "FARGATE") + 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, " ") + + 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: containerName, + }) + if err != nil { + 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/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..1803979 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -18,6 +18,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 == "" { @@ -52,9 +53,7 @@ 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") + //viper.BindPFlag("task_definition", runCmd.PersistentFlags().Lookup("task_definition")) } diff --git a/cmd/runFargate.go b/cmd/runFargate.go new file mode 100644 index 0000000..36676ba --- /dev/null +++ b/cmd/runFargate.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "os" + + "github.com/apex/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/springload/ecs-tool/lib" +) + +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.`, + 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 == "" { + 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(runFargateCmd) +} diff --git a/go.mod b/go.mod index bd7cea6..53e4878 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,69 @@ 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.55.5 + github.com/aws/aws-sdk-go v1.43.24 + 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 - 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/davecgh/go-spew v1.1.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/aws/protocol/eventstream v1.5.4 // 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/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/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/fujiwara/tracer v1.0.2 // 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/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/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 7cb0202..76e468b 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,59 @@ 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/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.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +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,30 +62,60 @@ 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/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/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= 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= 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= +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/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= 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,25 +133,67 @@ 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/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.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= 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.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/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +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/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-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.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.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= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +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/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/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 new file mode 100644 index 0000000..1080a7a --- /dev/null +++ b/lib/exec.go @@ -0,0 +1,513 @@ +package lib + +import ( + "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" + "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 + +// 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 +} + +// 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 +} + +// 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) +} + +// 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 { + var entrypointErr error + + // 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 + } + entrypointErr = result.err + // If entrypoint not found, try next entrypoint + 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 + if result.err != nil && isEntrypointNotFoundError(result.err) { + lastErr = result.err + continue + } + // Store error for potential use if without -c also fails + entrypointErr = result.err + } + + // Try without -c flag format + result := trySSMParentWithoutConfig(ecstaApp, entrypoint, command) + if result.succeeded { + return result + } + // 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 + } + } + + // 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 +// 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 { + // 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(cfg ExecConfig) error { + // Setup + ecstaApp, err := createEcstaApp(cfg) + if err != nil { + return err + } + + taskDefName := resolveTaskDefinitionName(cfg) + ssmConfig := determineSSMParentConfig(cfg.Profile, cfg.Cluster, taskDefName, cfg.ContainerName) + + // Execute + ssmResult := trySSMParent(ecstaApp, ssmConfig, cfg.Command) + ssmTried := len(ssmConfig.EntrypointPaths) > 0 + ssmSucceeded := ssmResult.succeeded + directResult := tryDirectExecution(ecstaApp, cfg.Command, ssmTried, ssmSucceeded) + + // Handle results + return handleExecutionResults(ssmResult, directResult) +} 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 new file mode 100644 index 0000000..b3c06a8 --- /dev/null +++ b/lib/runFargate.go @@ -0,0 +1,217 @@ +package lib + +import ( + "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) + 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": 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), + }) + 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 exitCode, nil +} diff --git a/lib/util.go b/lib/util.go index 032d468..b480667 100644 --- a/lib/util.go +++ b/lib/util.go @@ -9,6 +9,7 @@ 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" ) @@ -133,3 +134,48 @@ 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 +}