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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ main_sources := $(wildcard $(filter-out %_test.go $(all_testdata) $(all_testuti
main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,$(f)),./,./$(f))))

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

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

Expand Down
131 changes: 0 additions & 131 deletions api/func-e.go

This file was deleted.

77 changes: 77 additions & 0 deletions api/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2025 Tetrate
// SPDX-License-Identifier: Apache-2.0

// Package api allows Go projects to use func-e as a library, decoupled from how
// the func-e binary reads environment variables or CLI args.
package api

import (
"context"
"io"

"github.com/tetratelabs/func-e/internal/opts"
)

// HomeDir is an absolute path which most importantly contains "versions"
// installed from EnvoyVersionsURL. Defaults to "${HOME}/.func-e"
func HomeDir(homeDir string) RunOption {
return func(o *opts.RunOpts) {
o.HomeDir = homeDir
}
}

// EnvoyVersionsURL is the path to the envoy-versions.json.
// Defaults to "https://archive.tetratelabs.io/envoy/envoy-versions.json"
func EnvoyVersionsURL(envoyVersionsURL string) RunOption {
return func(o *opts.RunOpts) {
o.EnvoyVersionsURL = envoyVersionsURL
}
}

// EnvoyVersion overrides the version of Envoy to run. Defaults to the
// contents of "$HomeDir/versions/version".
//
// When that file is missing, it is generated from ".latestVersion" from the
// EnvoyVersionsURL. Its value can be in full version major.minor.patch format,
// e.g. 1.18.1 or without patch component, major.minor, e.g. 1.18.
func EnvoyVersion(envoyVersion string) RunOption {
return func(o *opts.RunOpts) {
o.EnvoyVersion = envoyVersion
}
}

// Out is where status messages are written. Defaults to os.Stdout
func Out(out io.Writer) RunOption {
return func(o *opts.RunOpts) {
o.Out = out
}
}

// EnvoyOut sets the writer for Envoy stdout
func EnvoyOut(w io.Writer) RunOption {
return func(o *opts.RunOpts) {
o.EnvoyOut = w
}
}

// EnvoyErr sets the writer for Envoy stderr
func EnvoyErr(w io.Writer) RunOption {
return func(o *opts.RunOpts) {
o.EnvoyErr = w
}
}

// RunOption is a configuration for RunFunc.
//
// Note: None of these default to values read from OS environment variables.
// If you wish to introduce such behavior, populate them in calling code.
type RunOption func(*opts.RunOpts)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this uses an internal type which is a trick to effectively hide the exported fields


// RunFunc downloads Envoy and runs it as a process with the arguments
// passed to it. Use api.RunOption for configuration options.
//
// On success, this blocks and returns nil when either `ctx` is done, or the
// process exits with status zero.
//
// The default implementation of RunFunc is func_e.Run.
type RunFunc func(ctx context.Context, args []string, options ...RunOption) error
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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

## Using native go commands:
End-to-end tests default to look for `func-e`, in the project root (current directory).
Expand Down
2 changes: 1 addition & 1 deletion e2e/func-e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func readOrBuildFuncEBin() error {
return fmt.Errorf("failed to create build directory %s: %w", buildDir, err)
}
var err error
if funcEBin, err = build.GoBuild(filepath.Join(projectRoot, "main.go"), buildDir); err != nil {
if funcEBin, err = build.GoBuild(filepath.Join(projectRoot, "cmd/func-e/main.go"), buildDir); err != nil {
return err
}
}
Expand Down
56 changes: 26 additions & 30 deletions internal/envoy/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,37 +53,43 @@ func (r *Runtime) Run(ctx context.Context, args []string) error {
// Warn, but don't fail if we can't write the pid file for some reason
r.maybeWarn(os.WriteFile(filepath.Join(r.o.RunDir, "envoy.pid"), []byte(strconv.Itoa(cmd.Process.Pid)), 0o600))

errCh := make(chan error, 1)
hookErrCh := make(chan error, 1)

// Process stderr in a goroutine
go r.processStderr(ctx, stderrPipe, errCh)
go r.processStderr(ctx, stderrPipe, hookErrCh)

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

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

if waitErr == nil || errors.Is(ctx.Err(), context.Canceled) {
return nil // don't treat context cancel (graceful shutdown) as an error
// Next, handle process exit errors
if exitErr != nil {
// Only ignore exit errors on cancellation if there was no stderr error
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return exitErr
}
return waitErr

return nil // Clean exit
}

// processStderr scans stderr output and triggers the startup hook when Envoy is ready.
func (r *Runtime) processStderr(ctx context.Context, stderrPipe io.Reader, errCh chan<- error) {
var procErr error
func (r *Runtime) processStderr(ctx context.Context, stderrPipe io.Reader, hookErrCh chan<- error) {
var hookErr error
defer func() {
if p := recover(); p != nil {
if procErr == nil {
procErr = fmt.Errorf("processStderr panicked: %v", p)
if hookErr == nil {
hookErr = fmt.Errorf("processStderr panicked: %v", p)
}
r.logf("processStderr panicked: %v", p)
}
errCh <- procErr
hookErrCh <- hookErr
}()

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

// Call startup hook
if err := r.startupHook(ctx, r.o.RunDir, adminAddress); err != nil {
procErr = err
hookErr = err
r.logf(err.Error())
break
}
}
}

// Log and propagate unexpected scanner errors, ignoring EOF, closed pipe, or context cancellation.
if err := scanner.Err(); err != nil && ctx.Err() == nil {
// Skip expected errors that indicate normal stream closure
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
r.logf("error scanning stderr: %v", err)
if procErr == nil {
procErr = fmt.Errorf("error scanning stderr: %w", err)
}
}
}
// ignore scanner errors as we are only concerned in hook errors
}
19 changes: 19 additions & 0 deletions internal/opts/opts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright func-e contributors
// SPDX-License-Identifier: Apache-2.0

// Package opts holds shared configuration types for func-e options.
// This is internal and not intended for direct use by external packages.
package opts

import "io"

// RunOpts holds the configuration set by RunOptions.
type RunOpts struct {
HomeDir string
EnvoyVersion string
EnvoyVersionsURL string
Out io.Writer
EnvoyOut io.Writer
EnvoyErr io.Writer
EnvoyPath string // Internal: path to the Envoy binary (for tests).
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright func-e contributors
// SPDX-License-Identifier: Apache-2.0

package api
package run

import (
"context"
Expand Down
Loading
Loading