Skip to content

Commit c1bbfa9

Browse files
Public API Separation (#476)
Signed-off-by: Adrian Cole <[email protected]> Co-authored-by: Adrian Cole <[email protected]>
1 parent 65897ce commit c1bbfa9

File tree

16 files changed

+264
-184
lines changed

16 files changed

+264
-184
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ main_sources := $(wildcard $(filter-out %_test.go $(all_testdata) $(all_testuti
9999
main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,$(f)),./,./$(f))))
100100

101101
build/func-e_%/func-e: $(main_sources)
102-
$(call go-build,$@,$<)
102+
$(call go-build,$@)
103103

104104
dist/func-e_$(VERSION)_%.tar.gz: build/func-e_%/func-e
105105
@printf "$(ansi_format_dark)" tar.gz "tarring $@"
@@ -216,7 +216,7 @@ define go-build
216216
@# $(go:go=) removes the trailing 'go', so we can insert cross-build variables
217217
@$(go:go=) CGO_ENABLED=0 GOOS=$(call go-os,$1) GOARCH=$(call go-arch,$1) go build \
218218
-ldflags "-s -w -X main.version=$(VERSION)" \
219-
-o $1 $2
219+
-o $1 ./cmd/func-e
220220
@printf "$(ansi_format_bright)" build "ok"
221221
endef
222222

api/func-e.go

Lines changed: 0 additions & 131 deletions
This file was deleted.

api/run.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2025 Tetrate
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package api allows Go projects to use func-e as a library, decoupled from how
5+
// the func-e binary reads environment variables or CLI args.
6+
package api
7+
8+
import (
9+
"context"
10+
"io"
11+
12+
"github.com/tetratelabs/func-e/internal/opts"
13+
)
14+
15+
// HomeDir is an absolute path which most importantly contains "versions"
16+
// installed from EnvoyVersionsURL. Defaults to "${HOME}/.func-e"
17+
func HomeDir(homeDir string) RunOption {
18+
return func(o *opts.RunOpts) {
19+
o.HomeDir = homeDir
20+
}
21+
}
22+
23+
// EnvoyVersionsURL is the path to the envoy-versions.json.
24+
// Defaults to "https://archive.tetratelabs.io/envoy/envoy-versions.json"
25+
func EnvoyVersionsURL(envoyVersionsURL string) RunOption {
26+
return func(o *opts.RunOpts) {
27+
o.EnvoyVersionsURL = envoyVersionsURL
28+
}
29+
}
30+
31+
// EnvoyVersion overrides the version of Envoy to run. Defaults to the
32+
// contents of "$HomeDir/versions/version".
33+
//
34+
// When that file is missing, it is generated from ".latestVersion" from the
35+
// EnvoyVersionsURL. Its value can be in full version major.minor.patch format,
36+
// e.g. 1.18.1 or without patch component, major.minor, e.g. 1.18.
37+
func EnvoyVersion(envoyVersion string) RunOption {
38+
return func(o *opts.RunOpts) {
39+
o.EnvoyVersion = envoyVersion
40+
}
41+
}
42+
43+
// Out is where status messages are written. Defaults to os.Stdout
44+
func Out(out io.Writer) RunOption {
45+
return func(o *opts.RunOpts) {
46+
o.Out = out
47+
}
48+
}
49+
50+
// EnvoyOut sets the writer for Envoy stdout
51+
func EnvoyOut(w io.Writer) RunOption {
52+
return func(o *opts.RunOpts) {
53+
o.EnvoyOut = w
54+
}
55+
}
56+
57+
// EnvoyErr sets the writer for Envoy stderr
58+
func EnvoyErr(w io.Writer) RunOption {
59+
return func(o *opts.RunOpts) {
60+
o.EnvoyErr = w
61+
}
62+
}
63+
64+
// RunOption is a configuration for RunFunc.
65+
//
66+
// Note: None of these default to values read from OS environment variables.
67+
// If you wish to introduce such behavior, populate them in calling code.
68+
type RunOption func(*opts.RunOpts)
69+
70+
// RunFunc downloads Envoy and runs it as a process with the arguments
71+
// passed to it. Use api.RunOption for configuration options.
72+
//
73+
// On success, this blocks and returns nil when either `ctx` is done, or the
74+
// process exits with status zero.
75+
//
76+
// The default implementation of RunFunc is func_e.Run.
77+
type RunFunc func(ctx context.Context, args []string, options ...RunOption) error
File renamed without changes.
File renamed without changes.

e2e/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This directory holds the end-to-end tests for `func-e`.
44

5-
By default, end-to-end (e2e) tests verify a `func-e` binary built from [main.go](../main.go).
5+
By default, end-to-end (e2e) tests verify a `func-e` binary built from [main.go](../cmd/func-e/main.go).
66

77
## Using native go commands:
88
End-to-end tests default to look for `func-e`, in the project root (current directory).

e2e/func-e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func readOrBuildFuncEBin() error {
4747
return fmt.Errorf("failed to create build directory %s: %w", buildDir, err)
4848
}
4949
var err error
50-
if funcEBin, err = build.GoBuild(filepath.Join(projectRoot, "main.go"), buildDir); err != nil {
50+
if funcEBin, err = build.GoBuild(filepath.Join(projectRoot, "cmd/func-e/main.go"), buildDir); err != nil {
5151
return err
5252
}
5353
}

internal/envoy/run.go

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,37 +53,43 @@ func (r *Runtime) Run(ctx context.Context, args []string) error {
5353
// Warn, but don't fail if we can't write the pid file for some reason
5454
r.maybeWarn(os.WriteFile(filepath.Join(r.o.RunDir, "envoy.pid"), []byte(strconv.Itoa(cmd.Process.Pid)), 0o600))
5555

56-
errCh := make(chan error, 1)
56+
hookErrCh := make(chan error, 1)
5757

5858
// Process stderr in a goroutine
59-
go r.processStderr(ctx, stderrPipe, errCh)
59+
go r.processStderr(ctx, stderrPipe, hookErrCh)
6060

61-
// Wait for the process to exit
62-
waitErr := cmd.Wait()
61+
// Wait for the process, and any stderr processing, to complete
62+
exitErr := cmd.Wait()
63+
hookErr := <-hookErrCh
6364

64-
// After process exit, check for any stderr processing error
65-
stderrErr := <-errCh
66-
if stderrErr != nil {
67-
return stderrErr
65+
// First, check for startup hook errors
66+
if hookErr != nil {
67+
return hookErr
6868
}
6969

70-
if waitErr == nil || errors.Is(ctx.Err(), context.Canceled) {
71-
return nil // don't treat context cancel (graceful shutdown) as an error
70+
// Next, handle process exit errors
71+
if exitErr != nil {
72+
// Only ignore exit errors on cancellation if there was no stderr error
73+
if errors.Is(ctx.Err(), context.Canceled) {
74+
return nil
75+
}
76+
return exitErr
7277
}
73-
return waitErr
78+
79+
return nil // Clean exit
7480
}
7581

7682
// processStderr scans stderr output and triggers the startup hook when Envoy is ready.
77-
func (r *Runtime) processStderr(ctx context.Context, stderrPipe io.Reader, errCh chan<- error) {
78-
var procErr error
83+
func (r *Runtime) processStderr(ctx context.Context, stderrPipe io.Reader, hookErrCh chan<- error) {
84+
var hookErr error
7985
defer func() {
8086
if p := recover(); p != nil {
81-
if procErr == nil {
82-
procErr = fmt.Errorf("processStderr panicked: %v", p)
87+
if hookErr == nil {
88+
hookErr = fmt.Errorf("processStderr panicked: %v", p)
8389
}
8490
r.logf("processStderr panicked: %v", p)
8591
}
86-
errCh <- procErr
92+
hookErrCh <- hookErr
8793
}()
8894

8995
scanner := bufio.NewScanner(stderrPipe)
@@ -99,30 +105,20 @@ func (r *Runtime) processStderr(ctx context.Context, stderrPipe io.Reader, errCh
99105
hookTriggered = true
100106
adminAddrBytes, err := os.ReadFile(r.adminAddressPath)
101107
if err != nil {
102-
procErr = fmt.Errorf("failed to read admin address from %s: %w", r.adminAddressPath, err)
103-
r.logf(procErr.Error())
108+
hookErr = fmt.Errorf("failed to read admin address from %s: %w", r.adminAddressPath, err)
109+
r.logf(hookErr.Error())
104110
break
105111
}
106112
adminAddress := strings.TrimSpace(string(adminAddrBytes))
107113
r.adminAddress = adminAddress
108114

109115
// Call startup hook
110116
if err := r.startupHook(ctx, r.o.RunDir, adminAddress); err != nil {
111-
procErr = err
117+
hookErr = err
112118
r.logf(err.Error())
113119
break
114120
}
115121
}
116122
}
117-
118-
// Log and propagate unexpected scanner errors, ignoring EOF, closed pipe, or context cancellation.
119-
if err := scanner.Err(); err != nil && ctx.Err() == nil {
120-
// Skip expected errors that indicate normal stream closure
121-
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
122-
r.logf("error scanning stderr: %v", err)
123-
if procErr == nil {
124-
procErr = fmt.Errorf("error scanning stderr: %w", err)
125-
}
126-
}
127-
}
123+
// ignore scanner errors as we are only concerned in hook errors
128124
}

internal/opts/opts.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright func-e contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package opts holds shared configuration types for func-e options.
5+
// This is internal and not intended for direct use by external packages.
6+
package opts
7+
8+
import "io"
9+
10+
// RunOpts holds the configuration set by RunOptions.
11+
type RunOpts struct {
12+
HomeDir string
13+
EnvoyVersion string
14+
EnvoyVersionsURL string
15+
Out io.Writer
16+
EnvoyOut io.Writer
17+
EnvoyErr io.Writer
18+
EnvoyPath string // Internal: path to the Envoy binary (for tests).
19+
}

api/func-e_run_test.go renamed to internal/run/func-e_run_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright func-e contributors
22
// SPDX-License-Identifier: Apache-2.0
33

4-
package api
4+
package run
55

66
import (
77
"context"

0 commit comments

Comments
 (0)