-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add fakebinary for acommodating testing the child process management #371
Changes from 20 commits
3c034e4
4f0dd5e
c512d41
c8366a8
8a0a37a
5de58eb
e4cf00b
ee11cb2
9d49d37
9307900
13f0074
f83baa4
e82285e
910ed12
2f9f767
735f613
538054f
7ff6f0e
9e74fcc
5fac78e
1e9c003
77d37d2
40748ad
7f3ff54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// 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" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"github.com/shirou/gopsutil/v3/process" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/tetratelabs/func-e/internal/test/fakebinary" | ||
"github.com/tetratelabs/func-e/internal/version" | ||
) | ||
|
||
// TestProcessGroupAttr_Kill tests sending SIGKILL to fake func-e and makes sure fake envoy receives | ||
// SIGTERM (as defined in Pdeathsig). | ||
func TestProcessGroupAttr_Kill(t *testing.T) { | ||
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" | ||
if !assert.Eventually(t, func() bool { | ||
b, e := reader.Peek(512) | ||
return e != nil && strings.Contains(string(b), waitFor) | ||
}, 5*time.Second, 100*time.Millisecond) { | ||
require.FailNowf(t, "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)} | ||
requireFindProcessNoError(t, fakeEnvoyProcess) | ||
dio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Kill the fake func-e process. | ||
// This works only for linux, sending kill -9 on darwin will not kill the process, we need to kill | ||
dio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// via pgid or kill the child first. | ||
require.NoError(t, cmd.Process.Kill()) | ||
|
||
// The child process is expected to receive ENVOY_SIGTERM. | ||
dio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
require.Contains(t, "caught ENVOY_SIGTERM\nexiting\n", stderr.String()) | ||
|
||
// 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)) | ||
} | ||
|
||
func requireFindProcessError(t *testing.T, proc *os.Process, expectedErr error) { | ||
// Wait until the operating system removes or adds the scheduled process. | ||
if !assert.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) { | ||
if expectedErr == nil { | ||
require.FailNow(t, "timeout waiting for finding process with no error") | ||
} | ||
require.FailNow(t, "timeout waiting for expected error %v", expectedErr) | ||
} | ||
} | ||
|
||
func requireFindProcessNoError(t *testing.T, proc *os.Process) { | ||
requireFindProcessError(t, proc, nil) // expect to find the process with no error. | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// 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 ( | ||
"bytes" | ||
"embed" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/tetratelabs/func-e/internal/test/fakebinary" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
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 | ||
//go:embed moreos.go | ||
dio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
//go:embed proc_*.go | ||
moreosSrcDir embed.FS | ||
) | ||
|
||
// Test_ImmediateExit tests the case when fake func-e fails on running fake envoy. | ||
func Test_ImmediateExit(t *testing.T) { | ||
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) | ||
|
||
stdout := new(bytes.Buffer) | ||
stderr := new(bytes.Buffer) | ||
|
||
// intentionally no args, as we expect it to not block and also fail | ||
cmd := exec.Command(fakeFuncE, "run") | ||
cmd.Stdout = stdout | ||
cmd.Stderr = stderr | ||
|
||
require.Error(t, cmd.Run()) | ||
require.Equal(t, Sprintf("starting: %s\n", fakeEnvoy), stdout.String()) | ||
require.Contains(t, stderr.String(), Sprintf("envoy exited with status: 1\n")) | ||
} | ||
|
||
// 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package main | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit I would like to name this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a mixed feeling about this since |
||
|
||
// 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) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to use a condition on
runtime.GOOS
vs_linux
because myself and others might refactor something and it will break compilation only on CI.So, basically handle this in a
t.Skip()
instead of build condition please? We can do this because there are no linux-specific code called here. When doing the skip, do note why it won't work in darwin or windows in case we can follow-up.