open-consul/command/connect/envoy/exec_unix.go

169 lines
4.1 KiB
Go

// +build linux darwin
package envoy
import (
"errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"golang.org/x/sys/unix"
)
func isHotRestartOption(s string) bool {
restartOpts := []string{
"--restart-epoch",
"--hot-restart-version",
"--drain-time-s",
"--parent-shutdown-time-s",
}
for _, opt := range restartOpts {
if s == opt {
return true
}
if strings.HasPrefix(s, opt+"=") {
return true
}
}
return false
}
func hasHotRestartOption(argSets ...[]string) bool {
for _, args := range argSets {
for _, opt := range args {
if isHotRestartOption(opt) {
return true
}
}
}
return false
}
func execEnvoy(binary string, prefixArgs, suffixArgs []string, bootstrapJson []byte) error {
// Write the Envoy bootstrap config file out to disk in a pocket universe
// visible only to the current process (and exec'd future selves).
fd, err := writeEphemeralEnvoyTempFile(bootstrapJson)
if err != nil {
return errors.New("Could not write envoy bootstrap config to a temp file: " + err.Error())
}
// On unix systems after exec the file descriptors that we should see:
//
// 0: stdin
// 1: stdout
// 2: stderr
// ... any open file descriptors from the parent without CLOEXEC set
//
// Above we explicitly disabled CLOEXEC for our temp file, so assuming
// FD numbers survive across execs, it should just be the value of
// `fd`. This is accessible as a file itself (trippy!) under
// /dev/fd/$FDNUMBER.
magicPath := filepath.Join("/dev/fd", strconv.Itoa(int(fd)))
// We default to disabling hot restart because it makes it easier to run
// multiple envoys locally for testing without them trying to share memory and
// unix sockets and complain about being different IDs. But if user is
// actually configuring hot-restart explicitly with the --restart-epoch option
// then don't disable it!
disableHotRestart := !hasHotRestartOption(prefixArgs, suffixArgs)
// First argument needs to be the executable name.
envoyArgs := []string{binary}
envoyArgs = append(envoyArgs, prefixArgs...)
envoyArgs = append(envoyArgs, "--v2-config-only",
"--config-path",
magicPath,
)
if disableHotRestart {
envoyArgs = append(envoyArgs, "--disable-hot-restart")
}
envoyArgs = append(envoyArgs, suffixArgs...)
// Exec
if err = unix.Exec(binary, envoyArgs, os.Environ()); err != nil {
return errors.New("Failed to exec envoy: " + err.Error())
}
return nil
}
func writeEphemeralEnvoyTempFile(b []byte) (uintptr, error) {
f, err := ioutil.TempFile("", "envoy-ephemeral-config")
if err != nil {
return 0, err
}
errFn := func(err error) (uintptr, error) {
_ = f.Close()
return 0, err
}
// TempFile already does this, but it's cheap to reinforce that we
// WANT the default behavior.
if err := f.Chmod(0600); err != nil {
return errFn(err)
}
// Immediately unlink the file as we are going to just pass the
// file descriptor, not the path.
if err = os.Remove(f.Name()); err != nil {
return errFn(err)
}
if _, err = f.Write(b); err != nil {
return errFn(err)
}
// Rewind the file descriptor so Envoy can read it.
if _, err = f.Seek(0, io.SeekStart); err != nil {
return errFn(err)
}
// Disable CLOEXEC so that this file descriptor is available
// to the exec'd Envoy.
if err := setCloseOnExec(f.Fd(), false); err != nil {
return errFn(err)
}
return f.Fd(), nil
}
// isCloseOnExec checks the provided file descriptor to see if the CLOEXEC flag
// is set.
func isCloseOnExec(fd uintptr) (bool, error) {
flags, err := getFdFlags(fd)
if err != nil {
return false, err
}
return flags&unix.FD_CLOEXEC != 0, nil
}
// setCloseOnExec sets or unsets the CLOEXEC flag on the provided file descriptor
// depending upon the value of the enabled arg.
func setCloseOnExec(fd uintptr, enabled bool) error {
flags, err := getFdFlags(fd)
if err != nil {
return err
}
newFlags := flags
if enabled {
newFlags |= unix.FD_CLOEXEC
} else {
newFlags &= ^unix.FD_CLOEXEC
}
if newFlags == flags {
return nil // noop
}
_, err = unix.FcntlInt(fd, unix.F_SETFD, newFlags)
return err
}
func getFdFlags(fd uintptr) (int, error) {
return unix.FcntlInt(fd, unix.F_GETFD, 0)
}