Files
certmanager/test/integration/invocation_linux_test.go

181 lines
4.8 KiB
Go

//go:build integration && linux
package integration
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"golang.org/x/sys/unix"
)
// Under generate() the exit code is the only failure signal and every byte of
// stdout is fed to parsejson(). A bare invocation may therefore print help and
// exit zero on a terminal and nowhere else. /dev/null and a closed fd 1 are the
// traps: both leave the process holding a character device, so a stat of the
// file mode calls them terminals.
func TestBareInvocationHelpsOnlyATerminal(t *testing.T) {
dir := t.TempDir()
bin := buildBinary(t, dir, filepath.Join(dir, "absent.yaml"))
t.Run("pipe", func(t *testing.T) {
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
defer func() { _ = r.Close() }()
code := runBare(t, bin, w)
_ = w.Close()
out, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read: %v", err)
}
assertMachineCaller(t, code, out)
})
t.Run("regular file", func(t *testing.T) {
path := filepath.Join(t.TempDir(), "stdout")
f, err := os.Create(path)
if err != nil {
t.Fatalf("create: %v", err)
}
defer func() { _ = f.Close() }()
code := runBare(t, bin, f)
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
assertMachineCaller(t, code, out)
})
t.Run("unix socket", func(t *testing.T) {
sock, peer := socketPair(t)
code := runBare(t, bin, sock)
// The child's copy went with it; dropping ours ends the peer's read.
_ = sock.Close()
out, err := io.ReadAll(peer)
if err != nil {
t.Fatalf("read: %v", err)
}
assertMachineCaller(t, code, out)
})
// Nothing written to /dev/null is observable, so the exit code carries the
// whole contract here -- which is exactly the situation under generate().
t.Run("dev null", func(t *testing.T) {
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
t.Fatalf("open %s: %v", os.DevNull, err)
}
defer func() { _ = devNull.Close() }()
if code := runBare(t, bin, devNull); code == 0 {
t.Errorf("exit code = 0, want non-zero")
}
})
// A nil file closes fd 1 across the exec. The Go runtime reopens it onto
// /dev/null before main() runs, so the process sees a character device it
// never chose.
t.Run("closed fd 1", func(t *testing.T) {
if code := runBare(t, bin, nil); code == 0 {
t.Errorf("exit code = 0, want non-zero")
}
})
t.Run("pty", func(t *testing.T) {
master, slave := openPTY(t)
drained := make(chan []byte, 1)
go func() {
out, _ := io.ReadAll(master)
drained <- out
}()
code := runBare(t, bin, slave)
// The child's copy is gone; dropping ours makes the master read EIO.
_ = slave.Close()
out := <-drained
if code != 0 {
t.Errorf("exit code = %d, want 0", code)
}
if !strings.Contains(string(out), "Usage:") {
t.Errorf("stdout = %q, want the help text", out)
}
})
}
func assertMachineCaller(t *testing.T, code int, stdout []byte) {
t.Helper()
if code == 0 {
t.Errorf("exit code = 0, want non-zero (stdout %q)", stdout)
}
if len(stdout) != 0 {
t.Errorf("stdout = %q, want empty", stdout)
}
}
// runBare runs the binary with no arguments and stdout as fd 1. A nil stdout
// leaves fd 1 closed at exec.
func runBare(t *testing.T, bin string, stdout *os.File) int {
t.Helper()
proc, err := os.StartProcess(bin, []string{bin}, &os.ProcAttr{
Env: hermeticEnv(t),
Files: []*os.File{nil, stdout, nil},
})
if err != nil {
t.Fatalf("start %s: %v", bin, err)
}
state, err := proc.Wait()
if err != nil {
t.Fatalf("wait: %v", err)
}
return state.ExitCode()
}
// socketPair returns a connected unix socket to hand the child as stdout, and
// the peer end to read back whatever reached it.
func socketPair(t *testing.T) (sock, peer *os.File) {
t.Helper()
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
if err != nil {
t.Fatalf("socketpair: %v", err)
}
sock = os.NewFile(uintptr(fds[0]), "stdout-socket")
peer = os.NewFile(uintptr(fds[1]), "stdout-socket-peer")
t.Cleanup(func() { _ = sock.Close(); _ = peer.Close() })
return sock, peer
}
// openPTY allocates a pty pair. CI containers have no controlling terminal, so
// this is the only way to exercise the branch that prints help and exits zero.
func openPTY(t *testing.T) (master, slave *os.File) {
t.Helper()
m, err := os.OpenFile("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
t.Fatalf("open /dev/ptmx: %v", err)
}
t.Cleanup(func() { _ = m.Close() })
if err := unix.IoctlSetPointerInt(int(m.Fd()), unix.TIOCSPTLCK, 0); err != nil {
t.Fatalf("unlock pty: %v", err)
}
n, err := unix.IoctlGetInt(int(m.Fd()), unix.TIOCGPTN)
if err != nil {
t.Fatalf("pty number: %v", err)
}
s, err := os.OpenFile(fmt.Sprintf("/dev/pts/%d", n), syscall.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
t.Fatalf("open pty slave: %v", err)
}
t.Cleanup(func() { _ = s.Close() })
return m, s
}