Skip to content

Commit cf9478b

Browse files
Adds api.EnvoyPath to run a custom Envoy binary (#506)
Signed-off-by: Adrian Cole <adrian@tetrate.io> Co-authored-by: Anuraag (Rag) Agrawal <anuraaga@gmail.com>
1 parent bced608 commit cf9478b

20 files changed

Lines changed: 187 additions & 138 deletions

File tree

CONFIG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Gateway to define their own home directories under an XDG base convention.
2020
| `FUNC_E_STATE_HOME` | `~/.local/state/func-e` | `api.StateHome()` |
2121
| `FUNC_E_RUNTIME_DIR` | `/tmp/func-e-${UID}` | `api.RuntimeDir()` |
2222
| `FUNC_E_RUN_ID` | auto-generated | `api.RunID()` |
23+
| `ENVOY_PATH` | | `api.EnvoyPath()` |
2324

2425
| File Type | Purpose | Default Path |
2526
|------------------------|----------------------------------------------|----------------------------------------------------------------------------------|

USAGE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ choose one, invoke `func-e use 1.38.0`. This installs into
77
`$FUNC_E_DATA_HOME/envoy-versions/1.38.0`, if not already present. You may
88
also use minor version, such as `func-e use 1.38`.
99

10+
`$ENVOY_PATH` runs a custom Envoy binary, skipping version
11+
resolution and download. This is useful for validating pre-release
12+
or feature branch builds.
13+
1014
You may want to override `$ENVOY_VERSIONS_URL` to supply custom builds or
1115
otherwise control the source of Envoy binaries. When overriding, validate
1216
your JSON first: https://archive.tetratelabs.io/release-versions-schema.json
@@ -49,4 +53,5 @@ such as glibc. This value must be constant within a `$FUNC_E_DATA_HOME`.
4953
| FUNC_E_RUNTIME_DIR | directory for temporary files (used by run command) | /tmp/func-e-${UID} |
5054
| FUNC_E_RUN_ID | custom run identifier for logs/runtime directories (used by run command) | auto-generated timestamp |
5155
| ENVOY_VERSIONS_URL | URL of Envoy versions JSON | https://archive.tetratelabs.io/envoy/envoy-versions.json |
56+
| ENVOY_PATH | path to a custom Envoy binary, bypassing download | |
5257
| FUNC_E_PLATFORM | the host OS and architecture of Envoy binaries. Ex. darwin/arm64 | $GOOS/$GOARCH |

api/run.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ func EnvoyVersion(envoyVersion string) RunOption {
105105
}
106106
}
107107

108+
// EnvoyPath overrides the path to the Envoy binary, bypassing download.
109+
func EnvoyPath(envoyPath string) RunOption {
110+
return func(o *api.RunOpts) {
111+
o.EnvoyPath = envoyPath
112+
}
113+
}
114+
108115
// Out is where status messages are written. Defaults to os.Stdout
109116
func Out(out io.Writer) RunOption {
110117
return func(o *api.RunOpts) {

internal/admin/admin.go

Lines changed: 47 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"net/http"
1414
"net/url"
1515
"os"
16+
"slices"
1617
"strconv"
1718
"strings"
1819
"time"
@@ -242,85 +243,65 @@ func parseAdminPort(addr string) (int, error) {
242243
return port, nil
243244
}
244245

245-
// extractAdminAddressPath returns the first match before [internalapi.ArgsIgnoreRest].
246-
func extractAdminAddressPath(cmdline []string) (string, error) {
247-
for i := range len(cmdline) {
248-
arg := cmdline[i]
249-
if arg == internalapi.ArgsIgnoreRest {
250-
break
251-
}
252-
switch {
253-
case arg == AddressPathFlag && i+1 < len(cmdline) && cmdline[i+1] != "":
254-
return cmdline[i+1], nil
255-
case strings.HasPrefix(arg, AddressPathFlag+"="):
256-
if value := strings.TrimPrefix(arg, AddressPathFlag+"="); value != "" {
257-
return value, nil
258-
}
259-
}
246+
// flagValue parses a flag from a process command line, e.g.:
247+
//
248+
// ["envoy", "--flag", "value"] → "value"
249+
// ["envoy", "--flag=value"] → "value"
250+
// ["/bin/sh", "-c", "envoy --flag value"] → "value"
251+
//
252+
// afterEnvoyArgs controls which side of `--` to scan,
253+
// so Envoy-native flags and func-e-appended flags are found in the right region.
254+
func flagValue(cmdline []string, flag string, afterEnvoyArgs bool) (string, error) {
255+
value := scanFlag(cmdline, flag, afterEnvoyArgs)
256+
257+
// /bin/sh -c "envoy ..." packs all args into cmdline[2], so re-scan there.
258+
if value == "" && len(cmdline) >= 3 && cmdline[1] == "-c" {
259+
value = scanFlag(strings.Fields(cmdline[2]), flag, afterEnvoyArgs)
260260
}
261261

262-
// Shell wrappers expose the wrapped command as one argv entry. Keep this
263-
// fallback after the argv-preserving scan so direct args can contain spaces.
264-
if len(cmdline) >= 3 && cmdline[1] == "-c" {
265-
fields := strings.Fields(cmdline[2])
266-
for i := range len(fields) {
267-
arg := fields[i]
268-
if arg == internalapi.ArgsIgnoreRest {
269-
break
270-
}
271-
switch {
272-
case arg == AddressPathFlag && i+1 < len(fields) && fields[i+1] != "":
273-
return fields[i+1], nil
274-
case strings.HasPrefix(arg, AddressPathFlag+"="):
275-
if value := strings.TrimPrefix(arg, AddressPathFlag+"="); value != "" {
276-
return value, nil
277-
}
278-
}
279-
}
262+
if value == "" {
263+
return "", fmt.Errorf("%s not found in command line", flag)
280264
}
281-
282-
return "", fmt.Errorf("%s not found in command line", AddressPathFlag)
265+
return value, nil
283266
}
284267

285-
// extractRunID returns the last match, so the func-e-appended value after [internalapi.ArgsIgnoreRest] wins.
286-
func extractRunID(cmdline []string) (string, error) {
287-
var runID string
288-
for i := 0; i < len(cmdline); i++ {
289-
arg := cmdline[i]
290-
switch {
291-
case arg == runIDFlag && i+1 < len(cmdline) && cmdline[i+1] != "":
292-
runID = cmdline[i+1]
293-
i++
294-
case strings.HasPrefix(arg, runIDFlag+"="):
295-
if value := strings.TrimPrefix(arg, runIDFlag+"="); value != "" {
296-
runID = value
297-
}
268+
// scanFlag returns the last match of flag in the region selected by afterEnvoyArgs.
269+
// Last-wins matches TCLAP (Envoy's CLI parser) which silently accepts duplicate flags.
270+
func scanFlag(args []string, flag string, afterEnvoyArgs bool) string {
271+
if i := slices.Index(args, "--"); i >= 0 {
272+
if afterEnvoyArgs {
273+
args = args[i+1:]
274+
} else {
275+
args = args[:i]
298276
}
299-
}
300-
if runID != "" {
301-
return runID, nil
277+
} else if afterEnvoyArgs {
278+
return "" // without the marker, there is no "after" to scan
302279
}
303280

304-
if len(cmdline) >= 3 && cmdline[1] == "-c" {
305-
fields := strings.Fields(cmdline[2])
306-
for i := 0; i < len(fields); i++ {
307-
arg := fields[i]
308-
switch {
309-
case arg == runIDFlag && i+1 < len(fields) && fields[i+1] != "":
310-
runID = fields[i+1]
281+
var result string
282+
for i := 0; i < len(args); i++ {
283+
// --flag value
284+
if args[i] == flag {
285+
if i+1 < len(args) && args[i+1] != "" {
286+
result = args[i+1]
311287
i++
312-
case strings.HasPrefix(arg, runIDFlag+"="):
313-
if value := strings.TrimPrefix(arg, runIDFlag+"="); value != "" {
314-
runID = value
315-
}
316288
}
289+
continue
290+
}
291+
// --flag=value
292+
if v, ok := strings.CutPrefix(args[i], flag+"="); ok && v != "" {
293+
result = v
317294
}
318295
}
319-
if runID != "" {
320-
return runID, nil
321-
}
296+
return result
297+
}
298+
299+
func extractAdminAddressPath(cmdline []string) (string, error) {
300+
return flagValue(cmdline, AddressPathFlag, false)
301+
}
322302

323-
return "", fmt.Errorf("%s not found in command line", runIDFlag)
303+
func extractRunID(cmdline []string) (string, error) {
304+
return flagValue(cmdline, runIDFlag, true)
324305
}
325306

326307
type envoyProcessCandidate struct {

internal/admin/admin_test.go

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -378,67 +378,58 @@ func TestAdminClient_NewListenerRequest(t *testing.T) {
378378
}
379379
}
380380

381-
func TestExtractAdminAddressPath(t *testing.T) {
382-
tmpDir := t.TempDir()
383-
tmpFile := filepath.Join(tmpDir, "admin-address.txt")
384-
pathWithSpaces := filepath.Join(tmpDir, "admin address.txt")
385-
381+
func TestScanFlag(t *testing.T) {
386382
tests := []struct {
387-
name string
388-
cmdline []string
389-
expected string
390-
expectedErr string
383+
name string
384+
args []string
385+
flag string
386+
afterEnvoyArgs bool
387+
expected string
391388
}{
392-
{"reads value form before Envoy ignore-rest", []string{"envoy", AddressPathFlag, tmpDir}, tmpDir, ""},
393-
{"preserves spaces in direct argv value", []string{"envoy", AddressPathFlag, pathWithSpaces}, pathWithSpaces, ""},
394-
{"reads equals form before Envoy ignore-rest", []string{"envoy", AddressPathFlag + "=" + tmpFile}, tmpFile, ""},
395-
{"finds value after other Envoy-owned args", []string{"--config", "/etc/envoy.yaml", AddressPathFlag, tmpDir}, tmpDir, ""},
396-
{"flag not present", []string{"envoy", "--config", "/etc/envoy.yaml"}, "", AddressPathFlag + " not found in command line"},
397-
{"flag present but no value", []string{"envoy", AddressPathFlag}, "", AddressPathFlag + " not found in command line"},
398-
{"empty cmdline", []string{}, "", AddressPathFlag + " not found in command line"},
399-
{"ignores value form hidden behind Envoy ignore-rest", []string{"envoy", "--", AddressPathFlag, tmpDir}, "", AddressPathFlag + " not found in command line"},
400-
{"keeps earlier equals form when later value is hidden", []string{"envoy", AddressPathFlag + "=" + tmpFile, "--", AddressPathFlag, tmpDir}, tmpFile, ""},
401-
{"accepts ignore-rest token as the flag value", []string{"envoy", AddressPathFlag, "--"}, "--", ""},
402-
{"reads value form from shell-wrapped command", []string{"sh", "-c", fmt.Sprintf("sleep 30 && echo %s %s", AddressPathFlag, tmpDir)}, tmpDir, ""},
403-
{"reads value form from shell wrapper with extra args", []string{"sh", "-c", fmt.Sprintf("envoy %s %s --other-flag", AddressPathFlag, tmpDir)}, tmpDir, ""},
404-
{"reads equals form from shell-wrapped command", []string{"sh", "-c", fmt.Sprintf("envoy %s=%s --other-flag", AddressPathFlag, tmpFile)}, tmpFile, ""},
405-
{"ignores shell-wrapped value hidden behind Envoy ignore-rest", []string{"sh", "-c", fmt.Sprintf("envoy -- %s %s", AddressPathFlag, tmpDir)}, "", AddressPathFlag + " not found in command line"},
406-
{"keeps shell-wrapped equals form before ignore-rest", []string{"sh", "-c", fmt.Sprintf("envoy %s=%s -- %s %s", AddressPathFlag, tmpFile, AddressPathFlag, tmpDir)}, tmpFile, ""},
407-
{"accepts ignore-rest token as shell-wrapped value", []string{"sh", "-c", fmt.Sprintf("envoy %s --", AddressPathFlag)}, "--", ""},
389+
{"admin address path", []string{"envoy", AddressPathFlag, "/tmp/admin.txt"}, AddressPathFlag, false, "/tmp/admin.txt"},
390+
{"admin address path equals form", []string{"envoy", AddressPathFlag + "=/tmp/admin.txt"}, AddressPathFlag, false, "/tmp/admin.txt"},
391+
{"admin address path with other flags", []string{"envoy", "-c", "envoy.yaml", AddressPathFlag, "/tmp/admin.txt", "--log-level", "info"}, AddressPathFlag, false, "/tmp/admin.txt"},
392+
{"empty value is not matched", []string{"envoy", AddressPathFlag, ""}, AddressPathFlag, false, ""},
393+
{"empty equals value is not matched", []string{"envoy", AddressPathFlag + "="}, AddressPathFlag, false, ""},
394+
{"flag not present", []string{"envoy", "-c", "envoy.yaml"}, AddressPathFlag, false, ""},
395+
{"empty args", []string{}, AddressPathFlag, false, ""},
396+
{"last-wins", []string{"envoy", AddressPathFlag, "/first", AddressPathFlag, "/second"}, AddressPathFlag, false, "/second"},
397+
{"admin path hidden after marker", []string{"envoy", AddressPathFlag, "/tmp/admin.txt", "--", AddressPathFlag, "/hidden"}, AddressPathFlag, false, "/tmp/admin.txt"},
398+
{"no admin path before marker", []string{"envoy", "-c", "envoy.yaml", "--", AddressPathFlag, "/hidden"}, AddressPathFlag, false, ""},
399+
{"run-id after marker", []string{"envoy", "-c", "envoy.yaml", "--", runIDFlag, "run-1"}, runIDFlag, true, "run-1"},
400+
{"run-id before marker is ignored", []string{"envoy", runIDFlag, "before", "--", runIDFlag, "after"}, runIDFlag, true, "after"},
401+
{"no marker scans everything for envoy flags", []string{"envoy", AddressPathFlag, "/tmp/admin.txt"}, AddressPathFlag, false, "/tmp/admin.txt"},
402+
{"no marker means nothing is after it", []string{"envoy", runIDFlag, "run-1"}, runIDFlag, true, ""},
408403
}
409404

410405
for _, tt := range tests {
411406
t.Run(tt.name, func(t *testing.T) {
412-
actual, err := extractAdminAddressPath(tt.cmdline)
413-
if tt.expectedErr != "" {
414-
require.EqualError(t, err, tt.expectedErr)
415-
} else {
416-
require.NoError(t, err)
417-
require.Equal(t, tt.expected, actual)
418-
}
407+
require.Equal(t, tt.expected, scanFlag(tt.args, tt.flag, tt.afterEnvoyArgs))
419408
})
420409
}
421410
}
422411

423-
func TestExtractRunID(t *testing.T) {
412+
func TestFlagValue(t *testing.T) {
424413
tests := []struct {
425-
name string
426-
cmdline []string
427-
expected string
428-
expectedErr string
414+
name string
415+
cmdline []string
416+
flag string
417+
afterEnvoyArgs bool
418+
expected string
419+
expectedErr string
429420
}{
430-
{"finds func-e marker after Envoy ignore-rest", []string{"envoy", "--", runIDFlag, "run-1"}, "run-1", ""},
431-
{"finds equals-form func-e marker after Envoy ignore-rest", []string{"envoy", "--", runIDFlag + "=run-1"}, "run-1", ""},
432-
{"finds func-e marker in shell-wrapped command", []string{"sh", "-c", "envoy -- --run-id run-1"}, "run-1", ""},
433-
{"finds shell-wrapped equals-form func-e marker", []string{"sh", "-c", "envoy -- --run-id=run-1"}, "run-1", ""},
434-
{"uses appended func-e marker over Envoy-owned value", []string{"envoy", runIDFlag, "ignored", "--", runIDFlag, "run-2"}, "run-2", ""},
435-
{"uses shell-wrapped appended marker over Envoy-owned value", []string{"sh", "-c", "envoy --run-id ignored -- --run-id run-2"}, "run-2", ""},
436-
{"requires func-e marker for process matching", []string{"envoy", "--", "--other", "value"}, "", runIDFlag + " not found in command line"},
421+
{"direct args", []string{"envoy", "--flag", "val"}, "--flag", false, "val", ""},
422+
{"shell-wrapped value form", []string{"sh", "-c", "envoy --flag val"}, "--flag", false, "val", ""},
423+
{"shell-wrapped equals form", []string{"sh", "-c", "envoy --flag=val"}, "--flag", false, "val", ""},
424+
{"shell-wrapped respects sentinel", []string{"sh", "-c", "envoy -- --flag hidden"}, "--flag", false, "", "--flag not found in command line"},
425+
{"shell-wrapped after sentinel", []string{"sh", "-c", "envoy -- --flag val"}, "--flag", true, "val", ""},
426+
{"prefers direct args over shell fallback", []string{"envoy", "--flag", "direct"}, "--flag", false, "direct", ""},
427+
{"not found", []string{"envoy"}, "--flag", false, "", "--flag not found in command line"},
437428
}
438429

439430
for _, tt := range tests {
440431
t.Run(tt.name, func(t *testing.T) {
441-
actual, err := extractRunID(tt.cmdline)
432+
actual, err := flagValue(tt.cmdline, tt.flag, tt.afterEnvoyArgs)
442433
if tt.expectedErr != "" {
443434
require.EqualError(t, err, tt.expectedErr)
444435
} else {
@@ -449,6 +440,24 @@ func TestExtractRunID(t *testing.T) {
449440
}
450441
}
451442

443+
func TestExtractAdminAddressPath(t *testing.T) {
444+
adminPath, err := extractAdminAddressPath([]string{"envoy", AddressPathFlag, "/tmp/admin.txt", "--", runIDFlag, "run-1"})
445+
require.NoError(t, err)
446+
require.Equal(t, "/tmp/admin.txt", adminPath)
447+
448+
_, err = extractAdminAddressPath([]string{"envoy", "--", AddressPathFlag, "/tmp/admin.txt"})
449+
require.EqualError(t, err, AddressPathFlag+" not found in command line")
450+
}
451+
452+
func TestExtractRunID(t *testing.T) {
453+
id, err := extractRunID([]string{"envoy", "--", runIDFlag, "run-1"})
454+
require.NoError(t, err)
455+
require.Equal(t, "run-1", id)
456+
457+
_, err = extractRunID([]string{"envoy", runIDFlag, "before-sentinel"})
458+
require.EqualError(t, err, runIDFlag+" not found in command line")
459+
}
460+
452461
func TestSelectEnvoyProcess(t *testing.T) {
453462
tests := []struct {
454463
name string

internal/api/opts.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import (
1010
"net/http"
1111
)
1212

13-
// ArgsIgnoreRest is Envoy's CLI separator: args after this are not parsed.
14-
const ArgsIgnoreRest = "--"
15-
1613
// HTTPTransport creates the HTTP client transport used during a run.
1714
type HTTPTransport func() http.RoundTripper
1815

@@ -29,6 +26,6 @@ type RunOpts struct {
2926
EnvoyOut io.Writer
3027
EnvoyErr io.Writer
3128
HTTPTransport http.RoundTripper
32-
EnvoyPath string // Internal: path to the Envoy binary (for tests).
29+
EnvoyPath string // Path to a custom Envoy binary, bypassing download.
3330
StartupHook StartupHook // Experimental: custom startup hook
3431
}

internal/cmd/app.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func NewApp(o *globals.GlobalOpts) *cli.Command {
2222
o.HTTPClient = http.DefaultClient
2323
}
2424

25-
var envoyVersionsURL, homeDir, configHome, dataHome, stateHome, runtimeDir, platform, runID string
25+
var envoyVersionsURL, envoyPath, homeDir, configHome, dataHome, stateHome, runtimeDir, platform, runID string
2626
lastKnownEnvoyPath := fmt.Sprintf("`$FUNC_E_DATA_HOME/envoy-versions/%s`", version.LastKnownEnvoy)
2727

2828
app := &cli.Command{
@@ -37,6 +37,10 @@ choose one, invoke ` + fmt.Sprintf("`func-e use %s`", version.LastKnownEnvoy) +
3737
` + lastKnownEnvoyPath + `, if not already present. You may
3838
also use minor version, such as ` + fmt.Sprintf("`func-e use %s`", version.LastKnownEnvoyMinor) + `.
3939
40+
` + "`$ENVOY_PATH`" + ` runs a custom Envoy binary, skipping version
41+
resolution and download. This is useful for validating pre-release
42+
or feature branch builds.
43+
4044
You may want to override ` + "`$ENVOY_VERSIONS_URL`" + ` to supply custom builds or
4145
otherwise control the source of Envoy binaries. When overriding, validate
4246
your JSON first: ` + globals.DefaultEnvoyVersionsSchemaURL + `
@@ -113,6 +117,13 @@ such as glibc. This value must be constant within a ` + "`$FUNC_E_DATA_HOME`" +
113117
Local: true,
114118
Sources: cli.EnvVars("ENVOY_VERSIONS_URL"),
115119
},
120+
&cli.StringFlag{
121+
Name: "envoy-path",
122+
Usage: "path to a custom Envoy binary, bypassing download",
123+
Destination: &envoyPath,
124+
Local: true,
125+
Sources: cli.EnvVars("ENVOY_PATH"),
126+
},
116127
&cli.StringFlag{
117128
Name: "platform",
118129
Usage: "the host OS and architecture of Envoy binaries. Ex. darwin/arm64",
@@ -123,7 +134,7 @@ such as glibc. This value must be constant within a ` + "`$FUNC_E_DATA_HOME`" +
123134
},
124135
},
125136
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
126-
if err := runtime.InitializeGlobalOpts(o, envoyVersionsURL, homeDir, configHome, dataHome, stateHome, runtimeDir, platform, runID); err != nil {
137+
if err := runtime.InitializeGlobalOpts(o, envoyVersionsURL, envoyPath, homeDir, configHome, dataHome, stateHome, runtimeDir, platform, runID); err != nil {
127138
return ctx, NewValidationError(err.Error())
128139
}
129140
return ctx, nil

internal/cmd/app_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,15 @@ func TestPlatformArg(t *testing.T) {
297297
}
298298
}
299299

300+
func TestEnvoyPath(t *testing.T) {
301+
testDirConfig(t, dirConfigTest{
302+
envVar: "ENVOY_PATH",
303+
flag: "--envoy-path",
304+
suffix: "envoy-path",
305+
accessor: func(o *globals.GlobalOpts) string { return o.EnvoyPath },
306+
})
307+
}
308+
300309
func TestEnvoyVersionsURL(t *testing.T) {
301310
type testCase struct {
302311
name string

internal/cmd/run.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ directory (aka $PWD) until func-e is interrupted (ex Ctrl+C, Ctrl+Break).
3535
Envoy's console output writes to "stdout.log" and "stderr.log" in the run directory
3636
(` + fmt.Sprintf("`%s`", globals.DefaultStateHome) + `/envoy-logs/{runID}).`,
3737
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
38+
if o.EnvoyPath != "" { // custom binary, skip version resolution
39+
return ctx, nil
40+
}
3841
if err := runtime.EnsureEnvoyVersion(ctx, o); err != nil {
3942
return ctx, NewValidationError(err.Error())
4043
}

0 commit comments

Comments
 (0)