Skip to content

Commit cab252c

Browse files
Add AdminClient for Envoy admin API access (#483)
Signed-off-by: Adrian Cole <[email protected]>
1 parent 1ac9967 commit cab252c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1706
-1008
lines changed

api/run.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@ import (
99
"context"
1010
"io"
1111

12-
"github.com/tetratelabs/func-e/internal/opts"
12+
"github.com/tetratelabs/func-e/internal/api"
1313
)
1414

1515
// HomeDir is an absolute path which most importantly contains "versions"
1616
// installed from EnvoyVersionsURL. Defaults to "${HOME}/.func-e"
1717
func HomeDir(homeDir string) RunOption {
18-
return func(o *opts.RunOpts) {
18+
return func(o *api.RunOpts) {
1919
o.HomeDir = homeDir
2020
}
2121
}
2222

2323
// EnvoyVersionsURL is the path to the envoy-versions.json.
2424
// Defaults to "https://archive.tetratelabs.io/envoy/envoy-versions.json"
2525
func EnvoyVersionsURL(envoyVersionsURL string) RunOption {
26-
return func(o *opts.RunOpts) {
26+
return func(o *api.RunOpts) {
2727
o.EnvoyVersionsURL = envoyVersionsURL
2828
}
2929
}
@@ -35,28 +35,28 @@ func EnvoyVersionsURL(envoyVersionsURL string) RunOption {
3535
// EnvoyVersionsURL. Its value can be in full version major.minor.patch format,
3636
// e.g. 1.18.1 or without patch component, major.minor, e.g. 1.18.
3737
func EnvoyVersion(envoyVersion string) RunOption {
38-
return func(o *opts.RunOpts) {
38+
return func(o *api.RunOpts) {
3939
o.EnvoyVersion = envoyVersion
4040
}
4141
}
4242

4343
// Out is where status messages are written. Defaults to os.Stdout
4444
func Out(out io.Writer) RunOption {
45-
return func(o *opts.RunOpts) {
45+
return func(o *api.RunOpts) {
4646
o.Out = out
4747
}
4848
}
4949

5050
// EnvoyOut sets the writer for Envoy stdout
5151
func EnvoyOut(w io.Writer) RunOption {
52-
return func(o *opts.RunOpts) {
52+
return func(o *api.RunOpts) {
5353
o.EnvoyOut = w
5454
}
5555
}
5656

5757
// EnvoyErr sets the writer for Envoy stderr
5858
func EnvoyErr(w io.Writer) RunOption {
59-
return func(o *opts.RunOpts) {
59+
return func(o *api.RunOpts) {
6060
o.EnvoyErr = w
6161
}
6262
}
@@ -65,7 +65,7 @@ func EnvoyErr(w io.Writer) RunOption {
6565
//
6666
// Note: None of these default to values read from OS environment variables.
6767
// If you wish to introduce such behavior, populate them in calling code.
68-
type RunOption func(*opts.RunOpts)
68+
type RunOption func(*api.RunOpts)
6969

7070
// RunFunc downloads Envoy and runs it as a process with the arguments
7171
// passed to it. Use api.RunOption for configuration options.

e2e/func-e_run_test.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,35 @@
44
package e2e
55

66
import (
7-
"context"
87
"testing"
98

109
"github.com/tetratelabs/func-e/internal/test/e2e"
1110
)
1211

1312
func TestRun(t *testing.T) {
14-
e2e.TestRun(context.Background(), t, funcEFactory{})
13+
e2e.TestRun(t.Context(), t, funcEFactory{})
14+
}
15+
16+
func TestRun_AdminAddressPath(t *testing.T) {
17+
e2e.TestRun_AdminAddressPath(t.Context(), t, funcEFactory{})
18+
}
19+
20+
func TestRun_LogWarn(t *testing.T) {
21+
e2e.TestRun_LogWarn(t.Context(), t, funcEFactory{})
1522
}
1623

1724
func TestRun_RunDirectory(t *testing.T) {
18-
e2e.TestRun_RunDirectory(context.Background(), t, funcEFactory{})
25+
e2e.TestRun_RunDirectory(t.Context(), t, funcEFactory{})
1926
}
2027

2128
func TestRun_InvalidConfig(t *testing.T) {
22-
e2e.TestRun_InvalidConfig(context.Background(), t, funcEFactory{})
29+
e2e.TestRun_InvalidConfig(t.Context(), t, funcEFactory{})
2330
}
2431

2532
func TestRun_StaticFile(t *testing.T) {
26-
e2e.TestRun_StaticFile(context.Background(), t, funcEFactory{})
33+
e2e.TestRun_StaticFile(t.Context(), t, funcEFactory{})
2734
}
2835

2936
func TestRun_CtrlCs(t *testing.T) {
30-
e2e.TestRun_CtrlCs(context.Background(), t, funcEFactory{})
37+
e2e.TestRun_CtrlCs(t.Context(), t, funcEFactory{})
3138
}

e2e/func-e_test.go

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import (
1414
"runtime"
1515
"syscall"
1616
"testing"
17+
"time"
1718

18-
"github.com/shirou/gopsutil/v4/process"
19-
19+
"github.com/tetratelabs/func-e/experimental/admin"
2020
"github.com/tetratelabs/func-e/internal/test/build"
2121
"github.com/tetratelabs/func-e/internal/test/e2e"
2222
)
@@ -62,8 +62,8 @@ func readOrBuildFuncEBin() error {
6262
}
6363

6464
// funcEExec is a temporary adapter for e2e tests except run.
65-
func funcEExec(args ...string) (string, string, error) {
66-
cmd := exec.CommandContext(context.Background(), funcEBin, args...)
65+
func funcEExec(ctx context.Context, args ...string) (string, string, error) {
66+
cmd := exec.CommandContext(ctx, funcEBin, args...)
6767
stdout := new(bytes.Buffer)
6868
stderr := new(bytes.Buffer)
6969
cmd.Stdout = io.MultiWriter(os.Stdout, stdout) // we want to see full `func-e` output in the test log
@@ -85,42 +85,19 @@ type funcE struct {
8585
stdout, stderr io.Writer
8686
}
8787

88-
// OnStart inspects the running func-e process tree to find the Envoy process and its run directory.
89-
func (a *funcE) OnStart(ctx context.Context) (runDir string, envoyPid int32, err error) {
88+
// OnStart inspects the running func-e process tree to find the Envoy process and its run directory,
89+
// then waits for Envoy's admin API to be ready.
90+
func (a *funcE) OnStart(ctx context.Context) (admin.AdminClient, error) {
9091
if a.cmd == nil || a.cmd.Process == nil {
91-
return "", 0, fmt.Errorf("no active process")
92-
}
93-
funcEPid := int32(a.cmd.Process.Pid)
94-
funcEProc, err := process.NewProcessWithContext(ctx, funcEPid)
95-
if err != nil {
96-
return "", 0, fmt.Errorf("failed to get func-e process: %w", err)
97-
}
98-
99-
children, err := funcEProc.Children()
100-
if err != nil {
101-
return "", 0, fmt.Errorf("failed to get child processes: %w", err)
102-
}
103-
104-
if len(children) == 0 {
105-
return "", 0, fmt.Errorf("no child processes found")
92+
return nil, fmt.Errorf("no active process")
10693
}
94+
funcEPid := a.cmd.Process.Pid
10795

108-
envoyProc := children[0]
109-
envoyPidValue := envoyProc.Pid
110-
111-
// Get command line args to find run directory
112-
cmdline, err := envoyProc.CmdlineSlice()
113-
if err != nil {
114-
return "", 0, fmt.Errorf("failed to get command line of envoy: %w", err)
115-
}
116-
117-
// Look for the run directory in the command line
118-
for i, arg := range cmdline {
119-
if arg == "--func-e-run-dir" && i+1 < len(cmdline) {
120-
return cmdline[i+1], envoyPidValue, nil
121-
}
96+
adminClient, err := admin.NewAdminClient(ctx, funcEPid)
97+
if err == nil {
98+
err = adminClient.AwaitReady(ctx, 100*time.Millisecond)
12299
}
123-
return "", 0, fmt.Errorf("failed to find run dir in envoy args: %v", cmdline)
100+
return adminClient, err
124101
}
125102

126103
// Run invokes `func-e run args...` and blocks until the process exits.

e2e/func-e_use_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestFuncEUse(t *testing.T) {
2222
homeDir := t.TempDir()
2323

2424
t.Run("not yet installed", func(t *testing.T) {
25-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "use", version.LastKnownEnvoy.String())
25+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "use", version.LastKnownEnvoy.String())
2626
require.NoError(t, err)
2727
require.Regexp(t, `^downloading https:.*tar.*z\r?\n$`, stdout)
2828
require.Empty(t, stderr)
@@ -38,7 +38,7 @@ func TestFuncEUse(t *testing.T) {
3838
})
3939

4040
t.Run("already installed", func(t *testing.T) {
41-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "use", version.LastKnownEnvoy.String())
41+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "use", version.LastKnownEnvoy.String())
4242

4343
require.NoError(t, err)
4444
require.Equal(t, fmt.Sprintf("%s is already downloaded\n", version.LastKnownEnvoy.String()), stdout)
@@ -48,7 +48,7 @@ func TestFuncEUse(t *testing.T) {
4848

4949
func TestFuncEUse_UnknownVersion(t *testing.T) {
5050
v := "1.1.1"
51-
stdout, stderr, err := funcEExec("use", v)
51+
stdout, stderr, err := funcEExec(t.Context(), "use", v)
5252

5353
require.EqualError(t, err, "exit status 1")
5454
require.Empty(t, stdout)
@@ -58,7 +58,7 @@ func TestFuncEUse_UnknownVersion(t *testing.T) {
5858

5959
func TestFuncEUse_UnknownMinorVersion(t *testing.T) {
6060
v := "1.1"
61-
stdout, stderr, err := funcEExec("use", v)
61+
stdout, stderr, err := funcEExec(t.Context(), "use", v)
6262

6363
require.EqualError(t, err, "exit status 1")
6464
require.Regexp(t, `^looking up the latest patch for Envoy version 1.1\r?\n$`, stdout)
@@ -73,15 +73,15 @@ func TestFuncEUse_MinorVersion(t *testing.T) {
7373
// The intended minor version to be installed. This version is known to have darwin and linux binaries.
7474
minorVersion := "1.24"
7575

76-
allVersions, _, err := funcEExec("versions", "-a")
76+
allVersions, _, err := funcEExec(t.Context(), "versions", "-a")
7777
require.NoError(t, err)
7878

7979
baseVersion, upgradedVersion := getVersionsRange(allVersions, minorVersion)
8080

8181
homeDir := t.TempDir()
8282

8383
t.Run("install last known", func(t *testing.T) {
84-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "use", version.LastKnownEnvoy.String())
84+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "use", version.LastKnownEnvoy.String())
8585

8686
require.NoError(t, err)
8787
require.Regexp(t, `^downloading https:.*tar.*z\r?\n$`, stdout)
@@ -98,7 +98,7 @@ func TestFuncEUse_MinorVersion(t *testing.T) {
9898
})
9999

100100
t.Run(fmt.Sprintf("install %s as base version", baseVersion), func(t *testing.T) {
101-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "use", baseVersion)
101+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "use", baseVersion)
102102

103103
require.NoError(t, err)
104104
require.Regexp(t, `^downloading https:.*tar.*z\r?\n$`, stdout)
@@ -115,7 +115,7 @@ func TestFuncEUse_MinorVersion(t *testing.T) {
115115
})
116116

117117
t.Run(fmt.Sprintf("install %s as upgraded version", upgradedVersion), func(t *testing.T) {
118-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "use", minorVersion)
118+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "use", minorVersion)
119119

120120
require.NoError(t, err)
121121
require.Regexp(t, `^looking up the latest patch for Envoy version 1.24\r?\ndownloading https:.*tar.*z\r?\n$`, stdout)
@@ -132,14 +132,14 @@ func TestFuncEUse_MinorVersion(t *testing.T) {
132132
})
133133

134134
t.Run("use upgraded version after downloaded", func(t *testing.T) {
135-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "use", minorVersion)
135+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "use", minorVersion)
136136
require.NoError(t, err)
137137
require.Equal(t, fmt.Sprintf("looking up the latest patch for Envoy version 1.24\n%s is already downloaded\n", upgradedVersion), stdout)
138138
require.Empty(t, stderr)
139139
})
140140

141141
t.Run("which upgraded version", func(t *testing.T) {
142-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "which")
142+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "which")
143143
relativeEnvoyBin := filepath.Join("versions", upgradedVersion, "bin", "envoy"+"")
144144
require.Contains(t, stdout, fmt.Sprintf("%s\n", relativeEnvoyBin))
145145
require.Empty(t, stderr)

e2e/func-e_version_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
func TestFuncEVersion(t *testing.T) {
1313
t.Parallel()
1414

15-
stdout, stderr, err := funcEExec("--version")
15+
stdout, stderr, err := funcEExec(t.Context(), "--version")
1616

1717
require.Regexp(t, `^func-e version ([^\s]+)\r?\n$`, stdout)
1818
require.Empty(t, stderr)

e2e/func-e_versions_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
func TestFuncEVersions_NothingYet(t *testing.T) {
1818
homeDir := t.TempDir()
1919

20-
stdout, stderr, err := funcEExec("--home-dir", homeDir, "versions")
20+
stdout, stderr, err := funcEExec(t.Context(), "--home-dir", homeDir, "versions")
2121

2222
require.NoError(t, err)
2323
require.Empty(t, stdout)
@@ -27,7 +27,7 @@ func TestFuncEVersions_NothingYet(t *testing.T) {
2727
func TestFuncEVersions(t *testing.T) {
2828
t.Parallel()
2929

30-
stdout, stderr, err := funcEExec("versions")
30+
stdout, stderr, err := funcEExec(t.Context(), "versions")
3131

3232
// Depending on ~/func-e/version, what's selected may not be the latest version or even installed at all.
3333
require.Regexp(t, "[ *] [1-9][0-9]*\\.[0-9]+\\.[0-9]+(_debug)? 202[1-9]-[01][0-9]-[0-3][0-9].*\n", stdout)
@@ -38,7 +38,7 @@ func TestFuncEVersions(t *testing.T) {
3838
func TestFuncEVersions_All(t *testing.T) {
3939
t.Parallel()
4040

41-
stdout, stderr, err := funcEExec("versions", "-a")
41+
stdout, stderr, err := funcEExec(t.Context(), "versions", "-a")
4242

4343
require.Regexp(t, fmt.Sprintf("[ *] %s 202[1-9]-[01][0-9]-[0-3][0-9].*\n", version.LastKnownEnvoy), stdout)
4444
require.Empty(t, stderr)
@@ -50,9 +50,9 @@ func TestFuncEVersions_AllIncludesInstalled(t *testing.T) {
5050

5151
// Cheap test that one includes the other. It doesn't actually parse the output, but the above tests prove the
5252
// latest version is in each deviation.
53-
allVersions, _, err := funcEExec("versions", "-a")
53+
allVersions, _, err := funcEExec(t.Context(), "versions", "-a")
5454
require.NoError(t, err)
55-
installedVersions, _, err := funcEExec("versions")
55+
installedVersions, _, err := funcEExec(t.Context(), "versions")
5656
require.NoError(t, err)
5757

5858
require.Greater(t, countLines(allVersions), countLines(installedVersions), "expected more versions available than installed")

e2e/func-e_which_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import (
1818
// lagging on Homebrew maintenance (OS/x), or lag in someone re-releasing on archive-envoy after Homebrew is updated.
1919
func TestFuncEWhich(t *testing.T) { // not parallel as it can end up downloading concurrently
2020
// Explicitly issue "use" for the last known version to ensure when latest is ahead of this, the test doesn't fail.
21-
_, _, err := funcEExec("use", version.LastKnownEnvoy.String())
21+
_, _, err := funcEExec(t.Context(), "use", version.LastKnownEnvoy.String())
2222
require.NoError(t, err)
2323

24-
stdout, stderr, err := funcEExec("which")
24+
stdout, stderr, err := funcEExec(t.Context(), "which")
2525
relativeEnvoyBin := filepath.Join("versions", version.LastKnownEnvoy.String(), "bin", "envoy")
2626
require.Contains(t, stdout, fmt.Sprintf("%s\n", relativeEnvoyBin))
2727
require.Empty(t, stderr)

e2e/main_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package e2e
55

66
import (
7+
"context"
78
"fmt"
89
"io"
910
"net/http"
@@ -28,7 +29,7 @@ func TestMain(m *testing.M) {
2829
}
2930

3031
// pre-flight check the binary is usable
31-
versionLine, _, err := funcEExec("--version")
32+
versionLine, _, err := funcEExec(context.Background(), "--version")
3233
if err != nil {
3334
exitOnInvalidBinary(err)
3435
}

experimental/admin/admin.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright func-e contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package admin
5+
6+
import (
7+
"context"
8+
9+
"github.com/tetratelabs/func-e/api"
10+
"github.com/tetratelabs/func-e/internal/admin"
11+
internalapi "github.com/tetratelabs/func-e/internal/api"
12+
)
13+
14+
// AdminClient provides methods to interact with Envoy's admin API.
15+
//
16+
// This type alias exposes the internal AdminClient interface for experimental use.
17+
type AdminClient = internalapi.AdminClient
18+
19+
// NewAdminClient returns an AdminClient if `funcEPid` has a child envoy process.
20+
func NewAdminClient(ctx context.Context, funcEPid int) (AdminClient, error) {
21+
// Poll for the run directory and admin address path from the Envoy process command line
22+
runDir, adminAddressPath, err := admin.PollAdminAddressPathAndRunDir(ctx, funcEPid)
23+
if err != nil {
24+
return nil, err
25+
}
26+
return admin.NewAdminClient(ctx, runDir, adminAddressPath)
27+
}
28+
29+
// StartupHook runs once the Envoy admin server is ready. Configure this
30+
// via the WithStartupHook api.RunOption.
31+
//
32+
// Note: Startup hooks are considered mandatory and will stop the run with
33+
// error if failed. If your hook is optional, rescue panics and log your own
34+
// errors.
35+
type StartupHook = internalapi.StartupHook
36+
37+
// WithStartupHook returns a RunOption that sets a startup hook.
38+
//
39+
// This is an experimental API that should only be used by CLI entrypoints.
40+
// See package documentation for usage constraints.
41+
//
42+
// If provided, this hook will REPLACE the default config dump hook.
43+
// If you want to preserve default behavior, do not use this option.
44+
func WithStartupHook(hook StartupHook) api.RunOption {
45+
return func(o *internalapi.RunOpts) {
46+
o.StartupHook = hook
47+
}
48+
}

0 commit comments

Comments
 (0)