1// +build linux darwin
2
3package envoy
4
5import (
6	"errors"
7	"fmt"
8	"os"
9	"os/exec"
10	"path/filepath"
11	"strings"
12	"syscall"
13	"time"
14
15	"golang.org/x/sys/unix"
16)
17
18// testSelfExecOverride is a way for the tests to no fork-bomb themselves by
19// self-executing the whole test suite for each case recursively. It's gross but
20// the least gross option I could think of.
21var testSelfExecOverride string
22
23func isHotRestartOption(s string) bool {
24	restartOpts := []string{
25		"--restart-epoch",
26		"--hot-restart-version",
27		"--drain-time-s",
28		"--parent-shutdown-time-s",
29	}
30	for _, opt := range restartOpts {
31		if s == opt {
32			return true
33		}
34		if strings.HasPrefix(s, opt+"=") {
35			return true
36		}
37	}
38	return false
39}
40
41func hasHotRestartOption(argSets ...[]string) bool {
42	for _, args := range argSets {
43		for _, opt := range args {
44			if isHotRestartOption(opt) {
45				return true
46			}
47		}
48	}
49	return false
50}
51
52func hasMaxObjNameLenOption(argSets ...[]string) bool {
53	for _, args := range argSets {
54		for _, opt := range args {
55			if opt == "--max-obj-name-len" {
56				return true
57			}
58		}
59	}
60	return false
61}
62
63func makeBootstrapPipe(bootstrapJSON []byte) (string, error) {
64	pipeFile := filepath.Join(os.TempDir(),
65		fmt.Sprintf("envoy-%x-bootstrap.json", time.Now().UnixNano()+int64(os.Getpid())))
66
67	err := syscall.Mkfifo(pipeFile, 0600)
68	if err != nil {
69		return pipeFile, err
70	}
71
72	// Get our own executable path.
73	execPath, err := os.Executable()
74	if err != nil {
75		return pipeFile, err
76	}
77
78	if testSelfExecOverride != "" {
79		execPath = testSelfExecOverride
80	} else if strings.HasSuffix(execPath, "/envoy.test") {
81		return pipeFile, fmt.Errorf("I seem to be running in a test binary without " +
82			"overriding the self-executable. Not doing that - it will make you sad. " +
83			"See testSelfExecOverride.")
84	}
85
86	// Exec the pipe-bootstrap internal sub-command which will write the bootstrap
87	// from STDIN to the named pipe (once Envoy opens it) and then clean up the
88	// file for us.
89	cmd := exec.Command(execPath, "connect", "envoy", "pipe-bootstrap", pipeFile)
90	stdin, err := cmd.StdinPipe()
91	if err != nil {
92		return pipeFile, err
93	}
94
95	// Write the config
96	n, err := stdin.Write(bootstrapJSON)
97	// Close STDIN whether it was successful or not
98	stdin.Close()
99	if err != nil {
100		return pipeFile, err
101	}
102	if n < len(bootstrapJSON) {
103		return pipeFile, fmt.Errorf("failed writing boostrap to child STDIN: %s", err)
104	}
105
106	err = cmd.Start()
107	if err != nil {
108		return pipeFile, err
109	}
110
111	// We can't wait for the process since we need to exec into Envoy before it
112	// will be able to complete so it will be remain as a zombie until Envoy is
113	// killed then will be reaped by the init process (pid 0). This is all a bit
114	// gross but the cleanest workaround I can think of for Envoy 1.10 not
115	// supporting /dev/fd/<fd> config paths any more. So we are done and leaving
116	// the child to run it's course without reaping it.
117	return pipeFile, nil
118}
119
120func execEnvoy(binary string, prefixArgs, suffixArgs []string, bootstrapJSON []byte) error {
121	pipeFile, err := makeBootstrapPipe(bootstrapJSON)
122	if err != nil {
123		os.RemoveAll(pipeFile)
124		return err
125	}
126	// We don't defer a cleanup since we are about to Exec into Envoy which means
127	// defer will never fire. The child process cleans up for us in the happy
128	// path.
129
130	// We default to disabling hot restart because it makes it easier to run
131	// multiple envoys locally for testing without them trying to share memory and
132	// unix sockets and complain about being different IDs. But if user is
133	// actually configuring hot-restart explicitly with the --restart-epoch option
134	// then don't disable it!
135	disableHotRestart := !hasHotRestartOption(prefixArgs, suffixArgs)
136
137	// First argument needs to be the executable name.
138	envoyArgs := []string{binary}
139	envoyArgs = append(envoyArgs, prefixArgs...)
140	envoyArgs = append(envoyArgs, "--config-path", pipeFile)
141	if disableHotRestart {
142		envoyArgs = append(envoyArgs, "--disable-hot-restart")
143	}
144	if !hasMaxObjNameLenOption(prefixArgs, suffixArgs) {
145		envoyArgs = append(envoyArgs, "--max-obj-name-len", "256")
146	}
147	envoyArgs = append(envoyArgs, suffixArgs...)
148
149	// Exec
150	if err = unix.Exec(binary, envoyArgs, os.Environ()); err != nil {
151		return errors.New("Failed to exec envoy: " + err.Error())
152	}
153
154	return nil
155}
156