Skip to content

Commit bced608

Browse files
Integrates RunID and HTTPTransport to NewAdminClient (#505)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
1 parent 0a25b40 commit bced608

13 files changed

Lines changed: 530 additions & 151 deletions

File tree

e2e/func-e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (a *funcE) OnStart(ctx context.Context) (admin.AdminClient, error) {
104104
funcEPid := a.cmd.Process.Pid
105105

106106
// Poll for the admin address path from the Envoy process command line
107-
envoyPid, adminAddressPath, err := internaladmin.PollEnvoyPidAndAdminAddressPath(ctx, funcEPid)
107+
envoyPid, adminAddressPath, err := internaladmin.PollEnvoyPidAndAdminAddressPath(ctx, funcEPid, "")
108108
if err != nil {
109109
return nil, err
110110
}

experimental/admin/admin.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,26 @@ import (
1818
type AdminClient = internalapi.AdminClient
1919

2020
// NewAdminClient returns an AdminClient if `funcEPid` has a child envoy process.
21-
func NewAdminClient(ctx context.Context, funcEPid int) (AdminClient, error) {
22-
// Poll for the admin address path from the Envoy process command line
23-
_, adminAddressPath, err := admin.PollEnvoyPidAndAdminAddressPath(ctx, funcEPid)
21+
//
22+
// Supported api.RunOption values:
23+
// - api.RunID - use when funcEPid launched multiple envoys
24+
// - api.HTTPTransport - use in testing or observability.
25+
func NewAdminClient(ctx context.Context, funcEPid int, options ...api.RunOption) (AdminClient, error) {
26+
var opts internalapi.RunOpts
27+
for _, o := range options {
28+
o(&opts)
29+
}
30+
31+
_, adminAddressPath, err := admin.PollEnvoyPidAndAdminAddressPath(ctx, funcEPid, opts.RunID)
2432
if err != nil {
2533
return nil, err
2634
}
27-
return admin.NewAdminClient(ctx, http.DefaultClient, adminAddressPath)
35+
36+
transport := opts.HTTPTransport
37+
if transport == nil {
38+
transport = http.DefaultTransport
39+
}
40+
return admin.NewAdminClient(ctx, &http.Client{Transport: transport}, adminAddressPath)
2841
}
2942

3043
// StartupHook runs once the Envoy admin server is ready. Configure this

internal/admin/admin.go

Lines changed: 152 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"errors"
1010
"fmt"
1111
"io"
12+
"net"
1213
"net/http"
1314
"net/url"
1415
"os"
@@ -24,9 +25,13 @@ import (
2425
const (
2526
ServerAddr = "127.0.0.1:9901"
2627
AddressPathFlag = "--admin-address-path"
28+
runIDFlag = "--run-id"
2729
live = "live"
30+
pollInterval = 50 * time.Millisecond
2831
)
2932

33+
var errMultipleEnvoyProcesses = fmt.Errorf("multiple Envoy processes found; set %s to disambiguate", runIDFlag)
34+
3035
// NewAdminClient creates an AdminClient by polling for the admin port at
3136
// adminAddressPath.
3237
func NewAdminClient(ctx context.Context, client *http.Client, adminAddressPath string) (internalapi.AdminClient, error) {
@@ -192,7 +197,7 @@ func (c *adminClient) Get(ctx context.Context, path string) ([]byte, error) {
192197
// pollAdminAddressPathForPort polls for the admin-address.txt file.
193198
// It returns the admin port number or an error if the timeout is reached.
194199
func pollAdminAddressPathForPort(ctx context.Context, adminAddressPath string) (int, error) {
195-
ticker := time.NewTicker(50 * time.Millisecond)
200+
ticker := time.NewTicker(pollInterval)
196201
defer ticker.Stop()
197202

198203
var adminAddr string
@@ -221,65 +226,166 @@ LOOP:
221226
}
222227
}
223228

224-
// Parse as a URL so hostnames, IPv4, and bracketed IPv6 addresses share
225-
// one port extraction path.
226-
u, err := url.Parse("http://" + adminAddr)
229+
return parseAdminPort(adminAddr)
230+
}
231+
232+
func parseAdminPort(addr string) (int, error) {
233+
_, portStr, err := net.SplitHostPort(addr)
227234
if err != nil {
228235
return 0, fmt.Errorf("failed to parse Envoy's admin address: %w", err)
229236
}
230237

231-
port, err := strconv.Atoi(u.Port())
238+
port, err := strconv.Atoi(portStr)
232239
if err != nil {
233240
return 0, fmt.Errorf("failed to parse Envoy's admin port: %w", err)
234241
}
235-
236242
return port, nil
237243
}
238244

239-
// extractFlagValue parses a flag value from command line arguments.
240-
func extractFlagValue(flag string, cmdline []string) (string, error) {
241-
for i, arg := range cmdline {
242-
if arg == flag && i+1 < len(cmdline) && cmdline[i+1] != "" {
243-
return cmdline[i+1], nil
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
244251
}
245-
if value, ok := strings.CutPrefix(arg, flag+"="); ok && value != "" {
246-
return value, nil
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+
}
247259
}
248260
}
249261

250262
// Shell wrappers expose the wrapped command as one argv entry. Keep this
251263
// fallback after the argv-preserving scan so direct args can contain spaces.
252264
if len(cmdline) >= 3 && cmdline[1] == "-c" {
253265
fields := strings.Fields(cmdline[2])
254-
for i, arg := range fields {
255-
if arg == flag && i+1 < len(fields) && fields[i+1] != "" {
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] != "":
256273
return fields[i+1], nil
274+
case strings.HasPrefix(arg, AddressPathFlag+"="):
275+
if value := strings.TrimPrefix(arg, AddressPathFlag+"="); value != "" {
276+
return value, nil
277+
}
257278
}
258-
if value, ok := strings.CutPrefix(arg, flag+"="); ok && value != "" {
259-
return value, nil
279+
}
280+
}
281+
282+
return "", fmt.Errorf("%s not found in command line", AddressPathFlag)
283+
}
284+
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+
}
298+
}
299+
}
300+
if runID != "" {
301+
return runID, nil
302+
}
303+
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]
311+
i++
312+
case strings.HasPrefix(arg, runIDFlag+"="):
313+
if value := strings.TrimPrefix(arg, runIDFlag+"="); value != "" {
314+
runID = value
315+
}
260316
}
261317
}
262318
}
319+
if runID != "" {
320+
return runID, nil
321+
}
263322

264-
return "", fmt.Errorf("%s not found in command line", flag)
323+
return "", fmt.Errorf("%s not found in command line", runIDFlag)
265324
}
266325

267-
// PollEnvoyPidAndAdminAddressPath polls for the Envoy child process and
268-
// extracts its pid and admin address path from its command line.
326+
type envoyProcessCandidate struct {
327+
pid int
328+
cmdline []string
329+
}
330+
331+
func selectEnvoyProcess(candidates []envoyProcessCandidate, runID string) (envoyPid int, adminAddressPath string, err error) {
332+
if runID == "" {
333+
// Without an explicit runID, skip children that lack the func-e marker.
334+
foundRunID := false
335+
for _, candidate := range candidates {
336+
if _, err := extractRunID(candidate.cmdline); err != nil {
337+
continue
338+
}
339+
foundRunID = true
340+
341+
path, err := extractAdminAddressPath(candidate.cmdline)
342+
if err != nil {
343+
continue
344+
}
345+
// Keep scanning past the first match to detect ambiguity.
346+
if adminAddressPath != "" {
347+
return 0, "", errMultipleEnvoyProcesses
348+
}
349+
envoyPid = candidate.pid
350+
adminAddressPath = path
351+
}
352+
if adminAddressPath != "" {
353+
return envoyPid, adminAddressPath, nil
354+
}
355+
if !foundRunID {
356+
return 0, "", fmt.Errorf("no child with %s", runIDFlag)
357+
}
358+
return 0, "", fmt.Errorf("no child with %s", AddressPathFlag)
359+
}
360+
361+
for _, candidate := range candidates {
362+
id, err := extractRunID(candidate.cmdline)
363+
if err != nil || id != runID {
364+
continue
365+
}
366+
adminAddressPath, err := extractAdminAddressPath(candidate.cmdline)
367+
if err != nil {
368+
return 0, "", err
369+
}
370+
return candidate.pid, adminAddressPath, nil
371+
}
372+
return 0, "", fmt.Errorf("no child with %s %s", runIDFlag, runID)
373+
}
374+
375+
// PollEnvoyPidAndAdminAddressPath polls for a child process tagged with
376+
// runID and extracts its pid and admin address path from its command line.
269377
//
270-
// This polls as the goroutine may be called prior to the Envoy subprocess.
271-
func PollEnvoyPidAndAdminAddressPath(ctx context.Context, funcEPid int) (envoyPid int, adminAddressPath string, err error) {
378+
// This polls because the child may not exist yet when the caller starts.
379+
func PollEnvoyPidAndAdminAddressPath(ctx context.Context, funcEPid int, runID string) (envoyPid int, adminAddressPath string, err error) {
272380
funcEProc, err := process.NewProcessWithContext(ctx, int32(funcEPid)) //nolint:gosec // funcEPid never overflows int32
273381
if err != nil {
274382
return 0, "", fmt.Errorf("failed to get func-e process: %w", err)
275383
}
276384

277-
ticker := time.NewTicker(50 * time.Millisecond)
385+
ticker := time.NewTicker(pollInterval)
278386
defer ticker.Stop()
279387

280-
var envoyProc *process.Process
281388
var lastErr error
282-
LOOP:
283389
for {
284390
select {
285391
case <-ctx.Done():
@@ -299,22 +405,27 @@ LOOP:
299405
continue
300406
}
301407

302-
// func-e starts one Envoy child process.
303-
envoyProc = children[0]
304-
envoyPid = int(envoyProc.Pid)
305-
break LOOP
306-
}
307-
}
308-
309-
envoyCmdline, err := envoyProc.CmdlineSliceWithContext(ctx)
310-
if err != nil {
311-
return 0, "", fmt.Errorf("failed to get command line of Envoy: %w", err)
312-
}
408+
candidates := make([]envoyProcessCandidate, 0, len(children))
409+
for _, child := range children {
410+
cmdline, err := child.CmdlineSliceWithContext(ctx)
411+
if err != nil {
412+
continue
413+
}
414+
candidates = append(candidates, envoyProcessCandidate{
415+
pid: int(child.Pid),
416+
cmdline: cmdline,
417+
})
418+
}
313419

314-
adminAddressPath, err = extractFlagValue(AddressPathFlag, envoyCmdline)
315-
if err != nil {
316-
return 0, "", err
420+
envoyPid, adminAddressPath, err = selectEnvoyProcess(candidates, runID)
421+
if err == nil {
422+
return envoyPid, adminAddressPath, nil
423+
}
424+
// Ambiguity won't resolve with more polling; the caller needs a runID.
425+
if errors.Is(err, errMultipleEnvoyProcesses) {
426+
return 0, "", err
427+
}
428+
lastErr = err
429+
}
317430
}
318-
319-
return envoyPid, adminAddressPath, nil
320431
}

0 commit comments

Comments
 (0)