Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Gateway to define their own home directories under an XDG base convention.
| `FUNC_E_STATE_HOME` | `~/.local/state/func-e` | `api.StateHome()` |
| `FUNC_E_RUNTIME_DIR` | `/tmp/func-e-${UID}` | `api.RuntimeDir()` |
| `FUNC_E_RUN_ID` | auto-generated | `api.RunID()` |
| `ENVOY_PATH` | | `api.EnvoyPath()` |

| File Type | Purpose | Default Path |
|------------------------|----------------------------------------------|----------------------------------------------------------------------------------|
Expand Down
5 changes: 5 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ choose one, invoke `func-e use 1.38.0`. This installs into
`$FUNC_E_DATA_HOME/envoy-versions/1.38.0`, if not already present. You may
also use minor version, such as `func-e use 1.38`.

`$ENVOY_PATH` runs a custom Envoy binary, skipping version
resolution and download. This is useful for validating pre-release
or feature branch builds.

You may want to override `$ENVOY_VERSIONS_URL` to supply custom builds or
otherwise control the source of Envoy binaries. When overriding, validate
your JSON first: https://archive.tetratelabs.io/release-versions-schema.json
Expand Down Expand Up @@ -49,4 +53,5 @@ such as glibc. This value must be constant within a `$FUNC_E_DATA_HOME`.
| FUNC_E_RUNTIME_DIR | directory for temporary files (used by run command) | /tmp/func-e-${UID} |
| FUNC_E_RUN_ID | custom run identifier for logs/runtime directories (used by run command) | auto-generated timestamp |
| ENVOY_VERSIONS_URL | URL of Envoy versions JSON | https://archive.tetratelabs.io/envoy/envoy-versions.json |
| ENVOY_PATH | path to a custom Envoy binary, bypassing download | |
| FUNC_E_PLATFORM | the host OS and architecture of Envoy binaries. Ex. darwin/arm64 | $GOOS/$GOARCH |
7 changes: 7 additions & 0 deletions api/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ func EnvoyVersion(envoyVersion string) RunOption {
}
}

// EnvoyPath overrides the path to the Envoy binary, bypassing download.
func EnvoyPath(envoyPath string) RunOption {
return func(o *api.RunOpts) {
o.EnvoyPath = envoyPath
}
}

// Out is where status messages are written. Defaults to os.Stdout
func Out(out io.Writer) RunOption {
return func(o *api.RunOpts) {
Expand Down
113 changes: 47 additions & 66 deletions internal/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"os"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -242,85 +243,65 @@ func parseAdminPort(addr string) (int, error) {
return port, nil
}

// extractAdminAddressPath returns the first match before [internalapi.ArgsIgnoreRest].
func extractAdminAddressPath(cmdline []string) (string, error) {
for i := range len(cmdline) {
arg := cmdline[i]
if arg == internalapi.ArgsIgnoreRest {
break
}
switch {
case arg == AddressPathFlag && i+1 < len(cmdline) && cmdline[i+1] != "":
return cmdline[i+1], nil
case strings.HasPrefix(arg, AddressPathFlag+"="):
if value := strings.TrimPrefix(arg, AddressPathFlag+"="); value != "" {
return value, nil
}
}
// flagValue parses a flag from a process command line, e.g.:
//
// ["envoy", "--flag", "value"] → "value"
// ["envoy", "--flag=value"] → "value"
// ["/bin/sh", "-c", "envoy --flag value"] → "value"
//
// afterEnvoyArgs controls which side of `--` to scan,
// so Envoy-native flags and func-e-appended flags are found in the right region.
func flagValue(cmdline []string, flag string, afterEnvoyArgs bool) (string, error) {
value := scanFlag(cmdline, flag, afterEnvoyArgs)

// /bin/sh -c "envoy ..." packs all args into cmdline[2], so re-scan there.
if value == "" && len(cmdline) >= 3 && cmdline[1] == "-c" {
value = scanFlag(strings.Fields(cmdline[2]), flag, afterEnvoyArgs)
}

// Shell wrappers expose the wrapped command as one argv entry. Keep this
// fallback after the argv-preserving scan so direct args can contain spaces.
if len(cmdline) >= 3 && cmdline[1] == "-c" {
fields := strings.Fields(cmdline[2])
for i := range len(fields) {
arg := fields[i]
if arg == internalapi.ArgsIgnoreRest {
break
}
switch {
case arg == AddressPathFlag && i+1 < len(fields) && fields[i+1] != "":
return fields[i+1], nil
case strings.HasPrefix(arg, AddressPathFlag+"="):
if value := strings.TrimPrefix(arg, AddressPathFlag+"="); value != "" {
return value, nil
}
}
}
if value == "" {
return "", fmt.Errorf("%s not found in command line", flag)
}

return "", fmt.Errorf("%s not found in command line", AddressPathFlag)
return value, nil
}

// extractRunID returns the last match, so the func-e-appended value after [internalapi.ArgsIgnoreRest] wins.
func extractRunID(cmdline []string) (string, error) {
var runID string
for i := 0; i < len(cmdline); i++ {
arg := cmdline[i]
switch {
case arg == runIDFlag && i+1 < len(cmdline) && cmdline[i+1] != "":
runID = cmdline[i+1]
i++
case strings.HasPrefix(arg, runIDFlag+"="):
if value := strings.TrimPrefix(arg, runIDFlag+"="); value != "" {
runID = value
}
// scanFlag returns the last match of flag in the region selected by afterEnvoyArgs.
// Last-wins matches TCLAP (Envoy's CLI parser) which silently accepts duplicate flags.
func scanFlag(args []string, flag string, afterEnvoyArgs bool) string {
if i := slices.Index(args, "--"); i >= 0 {
if afterEnvoyArgs {
args = args[i+1:]
} else {
args = args[:i]
}
}
if runID != "" {
return runID, nil
} else if afterEnvoyArgs {
return "" // without the marker, there is no "after" to scan
}

if len(cmdline) >= 3 && cmdline[1] == "-c" {
fields := strings.Fields(cmdline[2])
for i := 0; i < len(fields); i++ {
arg := fields[i]
switch {
case arg == runIDFlag && i+1 < len(fields) && fields[i+1] != "":
runID = fields[i+1]
var result string
for i := 0; i < len(args); i++ {
// --flag value
if args[i] == flag {
if i+1 < len(args) && args[i+1] != "" {
result = args[i+1]
i++
case strings.HasPrefix(arg, runIDFlag+"="):
if value := strings.TrimPrefix(arg, runIDFlag+"="); value != "" {
runID = value
}
}
continue
}
// --flag=value
if v, ok := strings.CutPrefix(args[i], flag+"="); ok && v != "" {
result = v
}
}
if runID != "" {
return runID, nil
}
return result
}

func extractAdminAddressPath(cmdline []string) (string, error) {
return flagValue(cmdline, AddressPathFlag, false)
}

return "", fmt.Errorf("%s not found in command line", runIDFlag)
func extractRunID(cmdline []string) (string, error) {
return flagValue(cmdline, runIDFlag, true)
}

type envoyProcessCandidate struct {
Expand Down
99 changes: 54 additions & 45 deletions internal/admin/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,67 +378,58 @@ func TestAdminClient_NewListenerRequest(t *testing.T) {
}
}

func TestExtractAdminAddressPath(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "admin-address.txt")
pathWithSpaces := filepath.Join(tmpDir, "admin address.txt")

func TestScanFlag(t *testing.T) {
tests := []struct {
name string
cmdline []string
expected string
expectedErr string
name string
args []string
flag string
afterEnvoyArgs bool
expected string
}{
{"reads value form before Envoy ignore-rest", []string{"envoy", AddressPathFlag, tmpDir}, tmpDir, ""},
{"preserves spaces in direct argv value", []string{"envoy", AddressPathFlag, pathWithSpaces}, pathWithSpaces, ""},
{"reads equals form before Envoy ignore-rest", []string{"envoy", AddressPathFlag + "=" + tmpFile}, tmpFile, ""},
{"finds value after other Envoy-owned args", []string{"--config", "/etc/envoy.yaml", AddressPathFlag, tmpDir}, tmpDir, ""},
{"flag not present", []string{"envoy", "--config", "/etc/envoy.yaml"}, "", AddressPathFlag + " not found in command line"},
{"flag present but no value", []string{"envoy", AddressPathFlag}, "", AddressPathFlag + " not found in command line"},
{"empty cmdline", []string{}, "", AddressPathFlag + " not found in command line"},
{"ignores value form hidden behind Envoy ignore-rest", []string{"envoy", "--", AddressPathFlag, tmpDir}, "", AddressPathFlag + " not found in command line"},
{"keeps earlier equals form when later value is hidden", []string{"envoy", AddressPathFlag + "=" + tmpFile, "--", AddressPathFlag, tmpDir}, tmpFile, ""},
{"accepts ignore-rest token as the flag value", []string{"envoy", AddressPathFlag, "--"}, "--", ""},
{"reads value form from shell-wrapped command", []string{"sh", "-c", fmt.Sprintf("sleep 30 && echo %s %s", AddressPathFlag, tmpDir)}, tmpDir, ""},
{"reads value form from shell wrapper with extra args", []string{"sh", "-c", fmt.Sprintf("envoy %s %s --other-flag", AddressPathFlag, tmpDir)}, tmpDir, ""},
{"reads equals form from shell-wrapped command", []string{"sh", "-c", fmt.Sprintf("envoy %s=%s --other-flag", AddressPathFlag, tmpFile)}, tmpFile, ""},
{"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"},
{"keeps shell-wrapped equals form before ignore-rest", []string{"sh", "-c", fmt.Sprintf("envoy %s=%s -- %s %s", AddressPathFlag, tmpFile, AddressPathFlag, tmpDir)}, tmpFile, ""},
{"accepts ignore-rest token as shell-wrapped value", []string{"sh", "-c", fmt.Sprintf("envoy %s --", AddressPathFlag)}, "--", ""},
{"admin address path", []string{"envoy", AddressPathFlag, "/tmp/admin.txt"}, AddressPathFlag, false, "/tmp/admin.txt"},
{"admin address path equals form", []string{"envoy", AddressPathFlag + "=/tmp/admin.txt"}, AddressPathFlag, false, "/tmp/admin.txt"},
{"admin address path with other flags", []string{"envoy", "-c", "envoy.yaml", AddressPathFlag, "/tmp/admin.txt", "--log-level", "info"}, AddressPathFlag, false, "/tmp/admin.txt"},
{"empty value is not matched", []string{"envoy", AddressPathFlag, ""}, AddressPathFlag, false, ""},
{"empty equals value is not matched", []string{"envoy", AddressPathFlag + "="}, AddressPathFlag, false, ""},
{"flag not present", []string{"envoy", "-c", "envoy.yaml"}, AddressPathFlag, false, ""},
{"empty args", []string{}, AddressPathFlag, false, ""},
{"last-wins", []string{"envoy", AddressPathFlag, "/first", AddressPathFlag, "/second"}, AddressPathFlag, false, "/second"},
{"admin path hidden after marker", []string{"envoy", AddressPathFlag, "/tmp/admin.txt", "--", AddressPathFlag, "/hidden"}, AddressPathFlag, false, "/tmp/admin.txt"},
{"no admin path before marker", []string{"envoy", "-c", "envoy.yaml", "--", AddressPathFlag, "/hidden"}, AddressPathFlag, false, ""},
{"run-id after marker", []string{"envoy", "-c", "envoy.yaml", "--", runIDFlag, "run-1"}, runIDFlag, true, "run-1"},
{"run-id before marker is ignored", []string{"envoy", runIDFlag, "before", "--", runIDFlag, "after"}, runIDFlag, true, "after"},
{"no marker scans everything for envoy flags", []string{"envoy", AddressPathFlag, "/tmp/admin.txt"}, AddressPathFlag, false, "/tmp/admin.txt"},
{"no marker means nothing is after it", []string{"envoy", runIDFlag, "run-1"}, runIDFlag, true, ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := extractAdminAddressPath(tt.cmdline)
if tt.expectedErr != "" {
require.EqualError(t, err, tt.expectedErr)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, actual)
}
require.Equal(t, tt.expected, scanFlag(tt.args, tt.flag, tt.afterEnvoyArgs))
})
}
}

func TestExtractRunID(t *testing.T) {
func TestFlagValue(t *testing.T) {
tests := []struct {
name string
cmdline []string
expected string
expectedErr string
name string
cmdline []string
flag string
afterEnvoyArgs bool
expected string
expectedErr string
}{
{"finds func-e marker after Envoy ignore-rest", []string{"envoy", "--", runIDFlag, "run-1"}, "run-1", ""},
{"finds equals-form func-e marker after Envoy ignore-rest", []string{"envoy", "--", runIDFlag + "=run-1"}, "run-1", ""},
{"finds func-e marker in shell-wrapped command", []string{"sh", "-c", "envoy -- --run-id run-1"}, "run-1", ""},
{"finds shell-wrapped equals-form func-e marker", []string{"sh", "-c", "envoy -- --run-id=run-1"}, "run-1", ""},
{"uses appended func-e marker over Envoy-owned value", []string{"envoy", runIDFlag, "ignored", "--", runIDFlag, "run-2"}, "run-2", ""},
{"uses shell-wrapped appended marker over Envoy-owned value", []string{"sh", "-c", "envoy --run-id ignored -- --run-id run-2"}, "run-2", ""},
{"requires func-e marker for process matching", []string{"envoy", "--", "--other", "value"}, "", runIDFlag + " not found in command line"},
{"direct args", []string{"envoy", "--flag", "val"}, "--flag", false, "val", ""},
{"shell-wrapped value form", []string{"sh", "-c", "envoy --flag val"}, "--flag", false, "val", ""},
{"shell-wrapped equals form", []string{"sh", "-c", "envoy --flag=val"}, "--flag", false, "val", ""},
{"shell-wrapped respects sentinel", []string{"sh", "-c", "envoy -- --flag hidden"}, "--flag", false, "", "--flag not found in command line"},
{"shell-wrapped after sentinel", []string{"sh", "-c", "envoy -- --flag val"}, "--flag", true, "val", ""},
{"prefers direct args over shell fallback", []string{"envoy", "--flag", "direct"}, "--flag", false, "direct", ""},
{"not found", []string{"envoy"}, "--flag", false, "", "--flag not found in command line"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := extractRunID(tt.cmdline)
actual, err := flagValue(tt.cmdline, tt.flag, tt.afterEnvoyArgs)
if tt.expectedErr != "" {
require.EqualError(t, err, tt.expectedErr)
} else {
Expand All @@ -449,6 +440,24 @@ func TestExtractRunID(t *testing.T) {
}
}

func TestExtractAdminAddressPath(t *testing.T) {
adminPath, err := extractAdminAddressPath([]string{"envoy", AddressPathFlag, "/tmp/admin.txt", "--", runIDFlag, "run-1"})
require.NoError(t, err)
require.Equal(t, "/tmp/admin.txt", adminPath)

_, err = extractAdminAddressPath([]string{"envoy", "--", AddressPathFlag, "/tmp/admin.txt"})
require.EqualError(t, err, AddressPathFlag+" not found in command line")
}

func TestExtractRunID(t *testing.T) {
id, err := extractRunID([]string{"envoy", "--", runIDFlag, "run-1"})
require.NoError(t, err)
require.Equal(t, "run-1", id)

_, err = extractRunID([]string{"envoy", runIDFlag, "before-sentinel"})
require.EqualError(t, err, runIDFlag+" not found in command line")
}

func TestSelectEnvoyProcess(t *testing.T) {
tests := []struct {
name string
Expand Down
5 changes: 1 addition & 4 deletions internal/api/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import (
"net/http"
)

// ArgsIgnoreRest is Envoy's CLI separator: args after this are not parsed.
const ArgsIgnoreRest = "--"

// HTTPTransport creates the HTTP client transport used during a run.
type HTTPTransport func() http.RoundTripper

Expand All @@ -29,6 +26,6 @@ type RunOpts struct {
EnvoyOut io.Writer
EnvoyErr io.Writer
HTTPTransport http.RoundTripper
EnvoyPath string // Internal: path to the Envoy binary (for tests).
EnvoyPath string // Path to a custom Envoy binary, bypassing download.
StartupHook StartupHook // Experimental: custom startup hook
}
15 changes: 13 additions & 2 deletions internal/cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func NewApp(o *globals.GlobalOpts) *cli.Command {
o.HTTPClient = http.DefaultClient
}

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

app := &cli.Command{
Expand All @@ -37,6 +37,10 @@ choose one, invoke ` + fmt.Sprintf("`func-e use %s`", version.LastKnownEnvoy) +
` + lastKnownEnvoyPath + `, if not already present. You may
also use minor version, such as ` + fmt.Sprintf("`func-e use %s`", version.LastKnownEnvoyMinor) + `.

` + "`$ENVOY_PATH`" + ` runs a custom Envoy binary, skipping version
resolution and download. This is useful for validating pre-release
or feature branch builds.

You may want to override ` + "`$ENVOY_VERSIONS_URL`" + ` to supply custom builds or
otherwise control the source of Envoy binaries. When overriding, validate
your JSON first: ` + globals.DefaultEnvoyVersionsSchemaURL + `
Expand Down Expand Up @@ -113,6 +117,13 @@ such as glibc. This value must be constant within a ` + "`$FUNC_E_DATA_HOME`" +
Local: true,
Sources: cli.EnvVars("ENVOY_VERSIONS_URL"),
},
&cli.StringFlag{
Name: "envoy-path",
Usage: "path to a custom Envoy binary, bypassing download",
Destination: &envoyPath,
Local: true,
Sources: cli.EnvVars("ENVOY_PATH"),
},
&cli.StringFlag{
Name: "platform",
Usage: "the host OS and architecture of Envoy binaries. Ex. darwin/arm64",
Expand All @@ -123,7 +134,7 @@ such as glibc. This value must be constant within a ` + "`$FUNC_E_DATA_HOME`" +
},
},
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
if err := runtime.InitializeGlobalOpts(o, envoyVersionsURL, homeDir, configHome, dataHome, stateHome, runtimeDir, platform, runID); err != nil {
if err := runtime.InitializeGlobalOpts(o, envoyVersionsURL, envoyPath, homeDir, configHome, dataHome, stateHome, runtimeDir, platform, runID); err != nil {
return ctx, NewValidationError(err.Error())
}
return ctx, nil
Expand Down
9 changes: 9 additions & 0 deletions internal/cmd/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,15 @@ func TestPlatformArg(t *testing.T) {
}
}

func TestEnvoyPath(t *testing.T) {
testDirConfig(t, dirConfigTest{
envVar: "ENVOY_PATH",
flag: "--envoy-path",
suffix: "envoy-path",
accessor: func(o *globals.GlobalOpts) string { return o.EnvoyPath },
})
}

func TestEnvoyVersionsURL(t *testing.T) {
type testCase struct {
name string
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ directory (aka $PWD) until func-e is interrupted (ex Ctrl+C, Ctrl+Break).
Envoy's console output writes to "stdout.log" and "stderr.log" in the run directory
(` + fmt.Sprintf("`%s`", globals.DefaultStateHome) + `/envoy-logs/{runID}).`,
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
if o.EnvoyPath != "" { // custom binary, skip version resolution
return ctx, nil
}
if err := runtime.EnsureEnvoyVersion(ctx, o); err != nil {
return ctx, NewValidationError(err.Error())
}
Expand Down
Loading