Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add app-entrypoint command #48

Merged
merged 3 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"

cmdController "github.com/hashicorp/consul-ecs/subcommand/acl-controller"
cmdAppEntrypoint "github.com/hashicorp/consul-ecs/subcommand/app-entrypoint"
cmdEnvoyEntrypoint "github.com/hashicorp/consul-ecs/subcommand/envoy-entrypoint"
cmdHealthSync "github.com/hashicorp/consul-ecs/subcommand/health-sync"
cmdMeshInit "github.com/hashicorp/consul-ecs/subcommand/mesh-init"
Expand Down Expand Up @@ -34,6 +35,9 @@ func init() {
"envoy-entrypoint": func() (cli.Command, error) {
return &cmdEnvoyEntrypoint.Command{UI: ui}, nil
},
"app-entrypoint": func() (cli.Command, error) {
return &cmdAppEntrypoint.Command{UI: ui}, nil
},
}
}

Expand Down
22 changes: 13 additions & 9 deletions subcommand/envoy-entrypoint/envoy.go → entrypoint/cmd.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//go:build !windows
// +build !windows

package envoyentrypoint
package entrypoint

import (
"os"
Expand All @@ -11,15 +11,19 @@ import (
"github.com/hashicorp/go-hclog"
)

type EnvoyCmd struct {
// Cmd runs a command in a subprocess (asynchronously).
// Call `go cmd.Run()` to run the command asynchronously.
// Use the Started() channel to wait for the command to start.
// Use the Done() channel to wait for the command to complete.
type Cmd struct {
*exec.Cmd

log hclog.Logger
doneCh chan struct{}
startedCh chan struct{}
}

func NewEnvoyCmd(log hclog.Logger, args []string) *EnvoyCmd {
func NewCmd(log hclog.Logger, args []string) *Cmd {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
Expand All @@ -29,7 +33,7 @@ func NewEnvoyCmd(log hclog.Logger, args []string) *EnvoyCmd {
Setpgid: true,
}

return &EnvoyCmd{
return &Cmd{
Cmd: cmd,
log: log,
doneCh: make(chan struct{}, 1),
Expand All @@ -39,12 +43,12 @@ func NewEnvoyCmd(log hclog.Logger, args []string) *EnvoyCmd {

// Run the command. The Started() and Done() functions can be used
// to wait for the process to start and exit, respectively.
func (e *EnvoyCmd) Run() {
func (e *Cmd) Run() {
defer close(e.doneCh)
defer close(e.startedCh)

if err := e.Cmd.Start(); err != nil {
e.log.Error("starting Envoy process", "error", err.Error())
e.log.Error("starting process", "error", err.Error())
// Closed channels (in defers) indicate the command failed to start.
return
}
Expand All @@ -53,16 +57,16 @@ func (e *EnvoyCmd) Run() {
if err := e.Cmd.Wait(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
// Do not log if it is only a non-zero exit code.
e.log.Error("waiting for Envoy process to finish", "error", err.Error())
e.log.Error("waiting for process to finish", "error", err.Error())
}
}
e.doneCh <- struct{}{}
}

func (e *EnvoyCmd) Started() chan struct{} {
func (e *Cmd) Started() chan struct{} {
return e.startedCh
}

func (e *EnvoyCmd) Done() chan struct{} {
func (e *Cmd) Done() chan struct{} {
return e.doneCh
}
9 changes: 9 additions & 0 deletions subcommand/app-entrypoint/command_common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package appentrypoint

func (c *Command) Help() string {
return ""
}

func (c *Command) Synopsis() string {
return "Entrypoint for running a command in ECS"
}
153 changes: 153 additions & 0 deletions subcommand/app-entrypoint/command_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//go:build !windows
// +build !windows

package appentrypoint

import (
"flag"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"

"github.com/hashicorp/consul-ecs/entrypoint"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/cli"
)

const (
flagShutdownDelay = "shutdown-delay"
)

type Command struct {
UI cli.Ui
log hclog.Logger
once sync.Once
flagSet *flag.FlagSet

sigs chan os.Signal
appCmd *entrypoint.Cmd
shutdownDelay time.Duration
}

func (c *Command) init() {
c.log = hclog.New(&hclog.LoggerOptions{Name: "consul-ecs"})
c.flagSet = flag.NewFlagSet("", flag.ContinueOnError)
c.flagSet.DurationVar(&c.shutdownDelay, flagShutdownDelay, 0,
`Continue running for this long after receiving SIGTERM. Must be a duration (e.g. "10s").`)
c.log = hclog.New(nil)

}

func (c *Command) Run(args []string) int {
c.once.Do(c.init)

// Flag parsing stops just before the first non-flag argument ("-" is a non-flag argument)
// or after the terminator "--"
err := c.flagSet.Parse(args)
if err != nil {
c.UI.Error(fmt.Sprint(err))
return 1
}

// Remaining args for the application command, after parsing our flags
args = c.flagSet.Args()

if len(args) == 0 {
c.UI.Error("command is required")
return 1
}

c.sigs = make(chan os.Signal, 1)
c.appCmd = entrypoint.NewCmd(c.log, args)

return c.realRun()
}

func (c *Command) realRun() int {
signal.Notify(c.sigs)
defer c.cleanup()

go c.appCmd.Run()
if _, ok := <-c.appCmd.Started(); !ok {
return 1
}

if exitCode, exited := c.waitForSigterm(); exited {
return exitCode
}
if c.shutdownDelay > 0 {
c.log.Info(fmt.Sprintf("consul-ecs: received sigterm. waiting %s before terminating application.", c.shutdownDelay))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we include the datetime here or use hclog? Might be useful in any debugging situaton

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is using hclog, so we get a timestamp:

2021-11-30T10:49:05.150-0600 [INFO]  consul-ecs: received sigterm. waiting 5s before terminating application.

(Also, CloudWatch has its own timestamps)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah nice, then can we use k/v logging instead of fmt.Sprintf?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess for this log fmt.Sprintf reads better

if exitCode, exited := c.waitForShutdownDelay(); exited {
return exitCode
}
}
// We've signaled for the process to exit, so wait until it does.
c.waitForAppExit()
return c.appCmd.ProcessState.ExitCode()
}

// waitForSigterm waits until c.appCmd has exited, or until a sigterm is received.
// It returns (exitCode, exited), where if exited=true, then c.appCmd has exited.
func (c *Command) waitForSigterm() (int, bool) {
for {
select {
case <-c.appCmd.Done():
return c.appCmd.ProcessState.ExitCode(), true
case sig := <-c.sigs:
if sig == syscall.SIGTERM {
return -1, false
}
c.forwardSignal(sig)
}
}
}

// waitForShutdownDelay waits for c.appCmd to exit for `delay` seconds.
// After the delay has passed, it sends a sigterm to c.appCmd.
// It returns (exitCode, exited), where if exited=true, then c.appCmd has exited.
func (c *Command) waitForShutdownDelay() (int, bool) {
timer := time.After(c.shutdownDelay)
for {
select {
case <-c.appCmd.Done():
return c.appCmd.ProcessState.ExitCode(), true
case sig := <-c.sigs:
c.forwardSignal(sig)
case <-timer:
if err := syscall.Kill(-c.appCmd.Process.Pid, syscall.SIGTERM); err != nil {
c.log.Warn("error sending sigterm to application", "error", err.Error())
}
}
}

}

func (c *Command) waitForAppExit() {
for {
select {
case <-c.appCmd.Done():
return
case sig := <-c.sigs:
c.forwardSignal(sig)
}
}
}

func (c *Command) forwardSignal(sig os.Signal) {
switch sig {
case syscall.SIGCHLD, syscall.SIGURG:
return
default:
if err := c.appCmd.Process.Signal(sig); err != nil {
c.log.Warn("forwarding signal", "err", err.Error())
}
}
}

func (c *Command) cleanup() {
signal.Stop(c.sigs)
<-c.appCmd.Done()
}
Loading