containerd/cio/io_unix_test.go
Hsing-Yu (David) Chen 687a5f51a8 fix: allow attaching to any combination of stdin/stdout/stderr
Before this PR, if a stdin/stdout/stderr stream is nil,
and the corresponding FIFO is not an empty string,
a panic will occur when Read/Write of the nil stream is invoked in io.CopyBuffer.

Signed-off-by: Hsing-Yu (David) Chen <davidhsingyuchen@gmail.com>
2023-08-01 09:56:04 -07:00

306 lines
7.4 KiB
Go

//go:build !windows
/*
Copyright The containerd Authors.
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 cio
import (
"bytes"
"context"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/containerd/fifo"
"github.com/stretchr/testify/assert"
)
func TestOpenFifos(t *testing.T) {
scenarios := []*FIFOSet{
{
Config: Config{
Stdin: "",
Stdout: filepath.Join("This/does/not/exist", "test-stdout"),
Stderr: filepath.Join("This/does/not/exist", "test-stderr"),
},
},
{
Config: Config{
Stdin: filepath.Join("This/does/not/exist", "test-stdin"),
Stdout: "",
Stderr: filepath.Join("This/does/not/exist", "test-stderr"),
},
},
{
Config: Config{
Stdin: "",
Stdout: "",
Stderr: filepath.Join("This/does/not/exist", "test-stderr"),
},
},
}
for _, scenario := range scenarios {
_, err := openFifos(context.Background(), scenario)
assert.Error(t, err, scenario)
}
}
// TestOpenFifosWithTerminal tests openFifos should not open stderr if terminal
// is set.
func TestOpenFifosWithTerminal(t *testing.T) {
var ctx, cancel = context.WithCancel(context.Background())
defer cancel()
ioFifoDir := t.TempDir()
cfg := Config{
Stdout: filepath.Join(ioFifoDir, "test-stdout"),
Stderr: filepath.Join(ioFifoDir, "test-stderr"),
}
// Without terminal, pipes.Stderr should not be nil
{
p, err := openFifos(ctx, NewFIFOSet(cfg, nil))
if err != nil {
t.Fatalf("unexpected error during openFifos: %v", err)
}
if p.Stderr == nil {
t.Fatalf("unexpected empty stderr pipe")
}
}
// With terminal, pipes.Stderr should be nil
{
cfg.Terminal = true
p, err := openFifos(ctx, NewFIFOSet(cfg, nil))
if err != nil {
t.Fatalf("unexpected error during openFifos: %v", err)
}
if p.Stderr != nil {
t.Fatalf("unexpected stderr pipe")
}
}
}
func assertHasPrefix(t *testing.T, s, prefix string) {
t.Helper()
if !strings.HasPrefix(s, prefix) {
t.Fatalf("expected %s to start with %s", s, prefix)
}
}
func TestNewFIFOSetInDir(t *testing.T) {
root := t.TempDir()
fifos, err := NewFIFOSetInDir(root, "theid", true)
assert.NoError(t, err)
dir := filepath.Dir(fifos.Stdin)
assertHasPrefix(t, dir, root)
expected := &FIFOSet{
Config: Config{
Stdin: filepath.Join(dir, "theid-stdin"),
Stdout: filepath.Join(dir, "theid-stdout"),
Stderr: filepath.Join(dir, "theid-stderr"),
Terminal: true,
},
}
assert.Equal(t, fifos.Config, expected.Config)
files, err := os.ReadDir(root)
assert.NoError(t, err)
assert.Len(t, files, 1)
assert.Nil(t, fifos.Close())
files, err = os.ReadDir(root)
assert.NoError(t, err)
assert.Len(t, files, 0)
}
func TestNewAttach(t *testing.T) {
testCases := []struct {
name string
expectedStdin, expectedStdout, expectedStderr string
}{
{
name: "attach to all streams (stdin, stdout, and stderr)",
expectedStdin: "this is the stdin",
expectedStdout: "this is the stdout",
expectedStderr: "this is the stderr",
},
{
name: "don't attach to stdin",
expectedStdout: "this is the stdout",
expectedStderr: "this is the stderr",
},
{
name: "don't attach to stdout",
expectedStdin: "this is the stdin",
expectedStderr: "this is the stderr",
},
{
name: "don't attach to stderr",
expectedStdin: "this is the stdin",
expectedStdout: "this is the stdout",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var (
stdin = bytes.NewBufferString(tc.expectedStdin)
stdout = new(bytes.Buffer)
stderr = new(bytes.Buffer)
// The variables below have to be of the interface type (i.e., io.Reader/io.Writer)
// instead of the concrete type (i.e., *bytes.Buffer) *before* being passed to NewAttach.
// Otherwise, in NewAttach, the interface value won't be nil
// (it's just that the concrete value inside the interface itself is nil. [1]),
// which means that the corresponding FIFO path won't be set to be an empty string,
// and that's not what we want.
//
// [1] https://go.dev/tour/methods/12
stdinArg io.Reader
stdoutArg, stderrArg io.Writer
)
if tc.expectedStdin != "" {
stdinArg = stdin
}
if tc.expectedStdout != "" {
stdoutArg = stdout
}
if tc.expectedStderr != "" {
stderrArg = stderr
}
attacher := NewAttach(WithStreams(stdinArg, stdoutArg, stderrArg))
fifos, err := NewFIFOSetInDir("", "theid", false)
assert.NoError(t, err)
attachedFifos, err := attacher(fifos)
assert.NoError(t, err)
defer attachedFifos.Close()
producers := setupFIFOProducers(t, attachedFifos.Config())
initProducers(t, producers, tc.expectedStdout, tc.expectedStderr)
var actualStdin []byte
if producers.Stdin != nil {
actualStdin, err = io.ReadAll(producers.Stdin)
assert.NoError(t, err)
}
attachedFifos.Wait()
attachedFifos.Cancel()
assert.Nil(t, attachedFifos.Close())
assert.Equal(t, tc.expectedStdout, stdout.String())
assert.Equal(t, tc.expectedStderr, stderr.String())
assert.Equal(t, tc.expectedStdin, string(actualStdin))
})
}
}
type producers struct {
Stdin io.ReadCloser
Stdout io.WriteCloser
Stderr io.WriteCloser
}
func setupFIFOProducers(t *testing.T, fifos Config) producers {
var (
err error
pipes producers
ctx = context.Background()
)
if fifos.Stdin != "" {
pipes.Stdin, err = fifo.OpenFifo(ctx, fifos.Stdin, syscall.O_RDONLY, 0)
assert.NoError(t, err)
}
if fifos.Stdout != "" {
pipes.Stdout, err = fifo.OpenFifo(ctx, fifos.Stdout, syscall.O_WRONLY, 0)
assert.NoError(t, err)
}
if fifos.Stderr != "" {
pipes.Stderr, err = fifo.OpenFifo(ctx, fifos.Stderr, syscall.O_WRONLY, 0)
assert.NoError(t, err)
}
return pipes
}
func initProducers(t *testing.T, producers producers, stdout, stderr string) {
if producers.Stdout != nil {
_, err := producers.Stdout.Write([]byte(stdout))
assert.NoError(t, err)
assert.Nil(t, producers.Stdout.Close())
}
if producers.Stderr != nil {
_, err := producers.Stderr.Write([]byte(stderr))
assert.NoError(t, err)
assert.Nil(t, producers.Stderr.Close())
}
}
func TestLogURIGenerator(t *testing.T) {
baseTestLogURIGenerator(t, []LogURIGeneratorTestCase{
{
scheme: "fifo",
path: "/full/path/pipe.fifo",
expected: "fifo:///full/path/pipe.fifo",
},
{
scheme: "file",
path: "/full/path/file.txt",
args: map[string]string{
"maxSize": "100MB",
},
expected: "file:///full/path/file.txt?maxSize=100MB",
},
{
scheme: "binary",
path: "/full/path/bin",
args: map[string]string{
"id": "testing",
},
expected: "binary:///full/path/bin?id=testing",
},
{
scheme: "unknown",
path: "nowhere",
err: "must be absolute",
},
{
scheme: "binary",
path: "C:\\path\\to\\binary",
// NOTE: Windows paths should not be parse-able outside of Windows:
err: "must be absolute",
},
})
}