-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add fakebinary for acommodating testing the child process management (#…
…371) This adds the `internal/test/fakebinary` package allowing us to test running a fake `func-e` process that spawns a fake `envoy`. This allows us to isolate the process control angle and can lead to quicker diagnosis of platform-specific behaviors, as well as show potential for future process abstraction. For example: to check the behavior of `func-e` when we send `SIGKILL` to it (in Linux, we need to make sure the child dies and receive whatever signal we have configured in `SysProcAttr.Pdeathsig`). We changed the setting for `SysProcAttr.Pdeathsig` to `SIGKILL` since when anyone sends `SIGKILL` on Linux to `func-e`, we want the spawned envoy to receive `SIGKILL` as well. Reference: #371 (comment). This also abstracts away the process for building any fake binary that optionally uses some of `func-e` internal packages. For example, in this change, we can isolate the build for fake `func-e` to use `internal/moreos` package (to check on `moreos.ProcessGroupAttr()`) only and no more than that what it requires. Signed-off-by: Dhi Aurrahman <dio@rockybars.com> Co-authored-by: Adrian Cole <adrian@tetrate.io> Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
- Loading branch information
1 parent
238c416
commit 20554ef
Showing
10 changed files
with
309 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// Copyright 2021 Tetrate | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package moreos | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"embed" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"github.com/shirou/gopsutil/v3/process" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/tetratelabs/func-e/internal/test/fakebinary" | ||
"github.com/tetratelabs/func-e/internal/version" | ||
) | ||
|
||
var ( | ||
// fakeFuncESrc is a test source file used to simulate how func-e manages its child process | ||
//go:embed testdata/fake_func-e.go | ||
fakeFuncESrc []byte // Embedding the fakeFuncESrc is easier than file I/O and ensures it doesn't skew coverage | ||
|
||
// Include the source imported by fakeFuncESrc directly and indirectly | ||
// TODO: replace wildcard with {{goos}} after https://github.com/golang/go/issues/48348. | ||
//go:embed moreos.go | ||
//go:embed proc_*.go | ||
moreosSrcDir embed.FS | ||
) | ||
|
||
// TestProcessGroupAttr_Kill tests sending SIGKILL to fake func-e. | ||
// On linux, we propagate SIGKILL to the child process as the configured SysProcAttr.Pdeathsig | ||
// in proc_linux.go. | ||
func TestProcessGroupAttr_Kill(t *testing.T) { | ||
// This works only for linux, sending kill -9 on darwin will not kill the process, we need to kill | ||
// via pgid or kill the child first. | ||
if runtime.GOOS != "linux" { | ||
t.Skip() | ||
} | ||
tempDir := t.TempDir() | ||
|
||
// Build a fake envoy and pass the ENV hint so that fake func-e uses it | ||
fakeEnvoy := filepath.Join(tempDir, "envoy"+Exe) | ||
fakebinary.RequireFakeEnvoy(t, fakeEnvoy) | ||
t.Setenv("ENVOY_PATH", fakeEnvoy) | ||
|
||
fakeFuncE := filepath.Join(tempDir, "func-e"+Exe) | ||
requireFakeFuncE(t, fakeFuncE) | ||
|
||
stderr := new(bytes.Buffer) | ||
stdout := new(bytes.Buffer) | ||
|
||
// With an arg so fakeFuncE runs fakeEnvoy as its child and doesn't exit. | ||
arg := string(version.LastKnownEnvoy) | ||
cmd := exec.Command(fakeFuncE, "run", arg, "-c") | ||
cmd.Stdout = stdout | ||
cmd.Stderr = stderr | ||
|
||
require.NoError(t, cmd.Start()) | ||
|
||
// Block until we reach an expected line or timeout. | ||
reader := bufio.NewReader(stderr) | ||
waitFor := "initializing epoch 0" | ||
require.Eventually(t, func() bool { | ||
b, _ := reader.Peek(512) // the error value is always EOF. | ||
return strings.HasPrefix(string(b), waitFor) | ||
}, 5*time.Second, 100*time.Millisecond, "timeout waiting for stderr to contain %q", waitFor) | ||
|
||
require.Equal(t, Sprintf("starting: %s %s -c\n", fakeEnvoy, arg), stdout.String()) | ||
|
||
fakeFuncEProcess, err := process.NewProcess(int32(cmd.Process.Pid)) | ||
require.NoError(t, err) | ||
|
||
// Get all fake func-e children processes. | ||
children, err := fakeFuncEProcess.Children() | ||
require.NoError(t, err) | ||
require.Equal(t, len(children), 1) // Should have only one child process i.e. the fake envoy process. | ||
fakeEnvoyProcess := &os.Process{Pid: int(children[0].Pid)} | ||
|
||
// Kill the fake func-e process. | ||
require.NoError(t, cmd.Process.Kill()) | ||
|
||
// Wait for the process to die; this could error due to the kill signal. | ||
cmd.Wait() //nolint | ||
|
||
// Wait and check if fake func-e and envoy processes are killed. | ||
requireFindProcessError(t, cmd.Process, process.ErrorProcessNotRunning) | ||
requireFindProcessError(t, fakeEnvoyProcess, process.ErrorProcessNotRunning) | ||
|
||
// Ensure both processes are killed. | ||
require.NoError(t, EnsureProcessDone(cmd.Process)) | ||
require.NoError(t, EnsureProcessDone(fakeEnvoyProcess)) | ||
} | ||
|
||
// requireFakeFuncE builds a func-e binary only depends on fakeFuncESrc and the sources in this package. | ||
// This is used to test integrated use of tools like ProcessGroupAttr without mixing unrelated concerns or dependencies. | ||
func requireFakeFuncE(t *testing.T, path string) { | ||
workDir := t.TempDir() | ||
|
||
// Copy the sources needed for fake func-e, but nothing else | ||
moreosDir := filepath.Join(workDir, "internal", "moreos") | ||
require.NoError(t, os.MkdirAll(moreosDir, 0700)) | ||
moreosSrcs, err := moreosSrcDir.ReadDir(".") | ||
require.NoError(t, err) | ||
for _, src := range moreosSrcs { | ||
data, err := moreosSrcDir.ReadFile(src.Name()) | ||
require.NoError(t, err) | ||
require.NoError(t, os.WriteFile(filepath.Join(moreosDir, src.Name()), data, 0600)) | ||
} | ||
|
||
fakeFuncEBin := fakebinary.RequireBuildFakeBinary(t, workDir, "func-e", fakeFuncESrc) | ||
require.NoError(t, os.WriteFile(path, fakeFuncEBin, 0700)) //nolint:gosec | ||
} | ||
|
||
func requireFindProcessError(t *testing.T, proc *os.Process, expectedErr error) { | ||
// Wait until the operating system removes or adds the scheduled process. | ||
require.Eventually(t, func() bool { | ||
_, err := process.NewProcess(int32(proc.Pid)) // because os.FindProcess is no-op in Linux! | ||
return err == expectedErr | ||
}, 100*time.Millisecond, 5*time.Millisecond, "timeout waiting for expected error %v", expectedErr) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package main | ||
|
||
// only import moreos, as that's what we are testing | ||
import ( | ||
"context" | ||
"os" | ||
"os/exec" | ||
"os/signal" | ||
"strings" | ||
"syscall" | ||
"time" | ||
|
||
"github.com/tetratelabs/func-e/internal/moreos" | ||
) | ||
|
||
// main simulates ../../.../main.go, but only focuses on sub process control style used by envoy.Run. | ||
// This allows us to write unit tests and identify failures more directly than e2e tests. | ||
// | ||
// Notably, this uses a variable ENVOY_PATH instead of envoy.GetHomeVersion, in order to reduce logic. | ||
// | ||
// In the future, some of this process control structure might move to moreos in order to reduce copy/paste between here | ||
// and internal/envoy/run.go (envoy.Run). | ||
func main() { | ||
if len(os.Args) < 2 { | ||
moreos.Fprintf(os.Stderr, "not enough args\n") | ||
os.Exit(1) | ||
} | ||
|
||
if os.Args[1] != "run" { | ||
moreos.Fprintf(os.Stderr, "%s not supported\n", os.Args[1]) | ||
os.Exit(1) | ||
} | ||
|
||
// Like envoy.GetHomeVersion, $FUNC_E_HOME/versions/$(cat $FUNC_E_HOME/version)/bin/envoy$GOEXE. | ||
cmd := exec.Command(os.Getenv("ENVOY_PATH"), os.Args[2:]...) | ||
cmd.SysProcAttr = moreos.ProcessGroupAttr() | ||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
|
||
// Like envoy.Run. | ||
waitCtx, waitCancel := context.WithCancel(context.Background()) | ||
sigCtx, _ := signal.NotifyContext(waitCtx, syscall.SIGINT, syscall.SIGTERM) | ||
defer waitCancel() | ||
|
||
moreos.Fprintf(os.Stdout, "starting: %s\n", strings.Join(cmd.Args, " ")) //nolint | ||
if err := cmd.Start(); err != nil { | ||
moreos.Fprintf(os.Stderr, "error: unable to start Envoy process: %s\n", err) | ||
os.Exit(1) | ||
} | ||
|
||
// Wait in a goroutine. We may need to kill the process if a signal occurs first. | ||
go func() { | ||
defer waitCancel() | ||
_ = cmd.Wait() // Envoy logs like "caught SIGINT" or "caught ENVOY_SIGTERM", so we don't repeat logging here. | ||
}() | ||
|
||
// Block until we receive SIGINT or are canceled because Envoy has died. | ||
<-sigCtx.Done() | ||
|
||
// Block until it exits to ensure file descriptors are closed prior to archival. | ||
// Allow up to 5 seconds for a clean stop, killing if it can't for any reason. | ||
select { | ||
case <-waitCtx.Done(): // cmd.Wait goroutine finished | ||
case <-time.After(5 * time.Second): | ||
_ = moreos.EnsureProcessDone(cmd.Process) | ||
} | ||
|
||
if cmd.ProcessState != nil && cmd.ProcessState.ExitCode() > 0 { | ||
moreos.Fprintf(os.Stderr, "envoy exited with status: %d\n", cmd.ProcessState.ExitCode()) | ||
os.Exit(1) | ||
} | ||
os.Exit(0) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.