conmon-rs/pkg/client/client_test.go

906 lines
26 KiB
Go

package client_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/containers/common/pkg/resize"
"github.com/containers/conmon-rs/pkg/client"
"github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/unshare"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencontainers/runtime-tools/generate"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
)
var _ = Describe("ConmonClient", func() {
var tr *testRunner
var sut *client.ConmonClient
JustAfterEach(func() {
if sut != nil {
pid := sut.PID()
Expect(pid).To(BeNumerically(">", 0))
rss := vmRSSGivenPID(pid)
// use Println because GinkgoWriter only writes on failure,
// and it's interesting to see this value for successful runs too.
GinkgoWriter.Println("VmRSS for server is", rss)
Expect(rss).To(BeNumerically("<", maxRSSKB))
}
})
AfterEach(func() {
Expect(tr.rr.RunCommand("delete", "-f", tr.ctrID)).To(Succeed())
if sut != nil {
Expect(sut.Shutdown()).To(Succeed())
}
Expect(os.RemoveAll(tr.tmpDir)).To(Succeed())
})
Describe("New", func() {
It("should restore from running server", func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
sut = tr.configGivenEnv()
sut2 := tr.configGivenEnv()
Expect(sut2.PID()).To(Equal(sut.PID()))
})
})
Describe("Version", func() {
for _, verbose := range []bool{false, true} {
name := "should succeed"
if verbose {
name += " with verbose output"
}
It(name, func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
sut = tr.configGivenEnv()
version, err := sut.Version(
context.Background(),
&client.VersionConfig{Verbose: verbose},
)
Expect(err).To(Succeed())
Expect(version.ProcessID).NotTo(BeZero())
Expect(version.Version).NotTo(BeEmpty())
Expect(version.Commit).NotTo(BeEmpty())
Expect(version.BuildDate).NotTo(BeEmpty())
Expect(version.Target).NotTo(BeEmpty())
Expect(version.RustVersion).NotTo(BeEmpty())
Expect(version.CargoVersion).NotTo(BeEmpty())
if verbose {
Expect(version.CargoTree).NotTo(BeEmpty())
} else {
Expect(version.CargoTree).To(BeEmpty())
}
})
}
})
Describe("CreateContainer", func() {
for _, terminal := range []bool{true, false} {
It(testName("should create a simple container", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfig(terminal)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
})
It(testName("should write exit file", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfig(terminal)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
Expect(fileContents(tr.exitPath())).To(Equal("0"))
})
It(testName("should kill created children if being killed", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfig(terminal)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
Expect(sut.Shutdown()).To(Succeed())
sut = nil
Eventually(func() error {
return tr.rr.RunCommandCheckOutput("stopped", "list")
}, time.Second*20).Should(Succeed())
})
It(testName("should execute cleanup command when container exits", terminal), func() {
tr = newTestRunner()
filepath := fmt.Sprintf("%s/conmon-client-test%s", os.TempDir(), tr.ctrID)
tr.createRuntimeConfig(terminal)
sut = tr.configGivenEnv()
tr.createContainerWithConfig(sut, &client.CreateContainerConfig{
ID: tr.ctrID,
BundlePath: tr.tmpDir,
Terminal: terminal,
ExitPaths: []string{tr.exitPath()},
OOMExitPaths: []string{tr.oomExitPath()},
LogDrivers: []client.ContainerLogDriver{{
Type: client.LogDriverTypeContainerRuntimeInterface,
Path: tr.logPath(),
}},
CleanupCmd: []string{"touch", filepath},
})
tr.startContainer(sut)
Expect(fileContents(filepath)).To(BeEmpty())
})
It(testName("should return error if invalid command", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"invalid"}, nil)
sut = tr.configGivenEnv()
_, err := sut.CreateContainer(context.Background(), &client.CreateContainerConfig{
ID: tr.ctrID,
BundlePath: tr.tmpDir,
Terminal: terminal,
LogDrivers: []client.ContainerLogDriver{{
Type: client.LogDriverTypeContainerRuntimeInterface,
Path: tr.logPath(),
}},
})
Expect(err).NotTo(Succeed())
Expect(err.Error()).To(ContainSubstring(`executable file not found in $PATH"`))
})
It(testName("should handle long run dir", terminal), func() {
tr = newTestRunner()
tr.tmpDir = MustDirInTempDir(
tr.tmpDir,
"thisisareallylongdirithasmanycharactersinthepathsosuperduperlongannoyinglylong",
)
tr.createRuntimeConfig(terminal)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
})
It(testName("should succeed/error to set the window size", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfig(terminal)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
err := sut.SetWindowSizeContainer(
context.Background(),
&client.SetWindowSizeContainerConfig{
ID: tr.ctrID,
Size: &resize.TerminalSize{
Width: 10,
Height: 20,
},
},
)
if terminal {
Expect(err).To(Succeed())
} else {
Expect(err).NotTo(Succeed())
}
})
It(testName("should catch out of memory (oom) events", terminal), func() {
if unshare.IsRootless() {
Skip("does not run rootless")
}
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(
terminal,
[]string{"/busybox", "tail", "/dev/zero"},
func(g generate.Generator) {
g.SetLinuxResourcesMemoryLimit(1024 * 1024)
},
)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
for range 10 {
if _, err := os.Stat(tr.oomExitPath()); err == nil {
break
}
GinkgoWriter.Println("Waiting for OOM exit path to exist")
time.Sleep(time.Second)
}
Expect(fileContents(tr.oomExitPath())).To(BeEmpty())
})
It(testName("should reopen logs based on max size", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(
terminal,
[]string{"/busybox", "sh", "-c", "echo hello && echo world"},
nil,
)
sut = tr.configGivenEnv()
cfg := tr.defaultConfig(terminal)
cfg.LogDrivers[0].MaxSize = 50
tr.createContainerWithConfig(sut, cfg)
tr.startContainer(sut)
logs := fileContents(tr.logPath())
Expect(logs).NotTo(ContainSubstring("hello"))
})
It(testName("should respect global args", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(
terminal,
[]string{"/busybox", "sh", "-c", "echo hello && echo world"},
nil,
)
logFile := MustFile(filepath.Join(tr.tmpDir, "runtime-log"))
sut = tr.configGivenEnv()
cfg := tr.defaultConfig(terminal)
cfg.GlobalArgs = []string{"--log", logFile, "--debug"}
tr.createContainerWithConfig(sut, cfg)
tr.startContainer(sut)
logs := fileContents(logFile)
Expect(logs).NotTo(BeEmpty())
})
}
})
Describe("ExecSync Stress", func() {
for _, terminal := range []bool{true, false} {
It(testName("should handle many requests", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "30"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
var wg sync.WaitGroup
for i := range 10 {
wg.Add(1)
go func(i int) {
defer GinkgoRecover()
defer wg.Done()
result, err := sut.ExecSyncContainer(context.Background(), &client.ExecSyncConfig{
ID: tr.ctrID,
Command: []string{"/busybox", "echo", "-n", "hello", "world", strconv.Itoa(i)},
Terminal: terminal,
Timeout: timeoutUnlimited,
})
Expect(err).To(Succeed())
Expect(result).NotTo(BeNil())
Expect(string(result.Stdout)).To(Equal(fmt.Sprintf("hello world %d", i)))
GinkgoWriter.Println("done with", i, string(result.Stdout))
}(i)
}
wg.Wait()
})
It(testName("should not leak memory", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "600"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
pid := sut.PID()
rssBefore := vmRSSGivenPID(pid)
GinkgoWriter.Printf("VmRSS before: %d\n", rssBefore)
for i := range 25 {
result, err := sut.ExecSyncContainer(context.Background(), &client.ExecSyncConfig{
ID: tr.ctrID,
Command: []string{"/busybox", "echo", "-n", "hello", "world", strconv.Itoa(i)},
Terminal: terminal,
Timeout: timeoutUnlimited,
})
Expect(err).To(Succeed())
Expect(result).NotTo(BeNil())
Expect(string(result.Stdout)).To(Equal(fmt.Sprintf("hello world %d", i)))
GinkgoWriter.Println("done with", i, string(result.Stdout))
}
rssAfter := vmRSSGivenPID(pid)
GinkgoWriter.Printf("VmRSS after: %d\n", rssAfter)
GinkgoWriter.Printf("VmRSS diff: %d\n", rssAfter-rssBefore)
Expect(rssAfter - rssBefore).To(BeNumerically("<", 2000))
})
}
})
Describe("ExecSyncContainer", func() {
for _, terminal := range []bool{true, false} {
It(testName("should succeed without timeout", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "10"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
result, err := sut.ExecSyncContainer(context.Background(), &client.ExecSyncConfig{
ID: tr.ctrID,
Command: []string{"/busybox", "echo", "-n", "hello", "world"},
Timeout: timeoutUnlimited,
Terminal: terminal,
})
Expect(err).To(Succeed())
Expect(result.ExitCode).To(BeEquivalentTo(0))
Expect(result.Stdout).To(BeEquivalentTo("hello world"))
Expect(result.Stderr).To(BeEmpty())
err = sut.ReopenLogContainer(context.Background(), &client.ReopenLogContainerConfig{
ID: tr.ctrID,
})
Expect(err).To(Succeed())
logs := fileContents(tr.logPath())
Expect(logs).To(BeEmpty())
})
It(testName("should succeed with timeout", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "10"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
result, err := sut.ExecSyncContainer(context.Background(), &client.ExecSyncConfig{
ID: tr.ctrID,
Command: []string{"/busybox", "echo", "-n", "hello", "world"},
Timeout: 10,
Terminal: terminal,
})
Expect(err).To(Succeed())
Expect(result.ExitCode).To(BeEquivalentTo(0))
Expect(result.Stdout).To(BeEquivalentTo("hello world"))
Expect(result.Stderr).To(BeEmpty())
})
It(testName("should set the correct exit code", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "10"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
result, err := sut.ExecSyncContainer(context.Background(), &client.ExecSyncConfig{
ID: tr.ctrID,
Command: []string{"/busybox", "invalid"},
Timeout: timeoutUnlimited,
Terminal: terminal,
})
Expect(err).To(Succeed())
Expect(result.ExitCode).To(BeEquivalentTo(127))
expectedStr := "invalid: applet not found"
if terminal {
expectedStr += "\r"
}
expectedStr += "\n"
if terminal {
Expect(result.Stdout).To(BeEquivalentTo(expectedStr))
Expect(result.Stderr).To(BeEmpty())
} else {
Expect(result.Stdout).To(BeEmpty())
Expect(result.Stderr).To(BeEquivalentTo(expectedStr))
}
})
It(testName("should timeout", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "20"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
result, err := sut.ExecSyncContainer(context.Background(), &client.ExecSyncConfig{
ID: tr.ctrID,
Command: []string{"/busybox", "sleep", "5"},
Timeout: 3,
Terminal: terminal,
})
Expect(err).To(Succeed())
Expect(result).NotTo(BeNil())
Expect(result.TimedOut).To(BeTrue())
})
}
})
Describe("Attach", func() {
matrix := []struct {
terminal bool
pipe string
}{
{
terminal: true,
pipe: "stdout",
},
{
terminal: false,
pipe: "stdout",
},
{
terminal: false,
pipe: "stderr",
},
}
for _, test := range matrix {
terminal := test.terminal
pipe := test.pipe
It(testName("should succeed with "+test.pipe, test.terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sh"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
stdinReader, stdinWriter := io.Pipe()
stdReader, stdWriter := io.Pipe()
cfg := &client.AttachConfig{
ID: tr.ctrID,
SocketPath: filepath.Join(tr.tmpDir, "attach"),
StopAfterStdinEOF: true,
Streams: client.AttachStreams{
Stdin: &client.In{stdinReader},
},
}
if pipe == "stdout" {
cfg.Streams.Stdout = &client.Out{stdWriter}
} else {
cfg.Streams.Stderr = &client.Out{stdWriter}
}
testAttach(sut, cfg, stdinWriter, stdReader, pipe, pipe == "stderr", terminal)
})
}
})
Describe("Tracing", func() {
const contribTracingPath = "../../contrib/tracing/"
BeforeEach(func() {
cmd := exec.Command(contribTracingPath + "start")
fmt.Fprintln(os.Stdout)
cmd.Stdout = os.Stdout
Expect(cmd.Run()).To(Succeed())
})
hasTraces := func() bool {
resp, err := http.Get("http://localhost:16686/api/traces?service=conmonrs")
Expect(err).NotTo(HaveOccurred())
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
Expect(err).NotTo(HaveOccurred())
type traceData struct {
Data []interface{} `json:"data"`
}
traces := &traceData{}
Expect(json.Unmarshal(body, traces)).NotTo(HaveOccurred())
return len(traces.Data) > 0
}
It("should succeed", func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
tr.enableTracing = true
sut = tr.configGivenEnv()
tr.createContainer(sut, false)
tr.startContainer(sut)
for range 100 {
if hasTraces() {
break
}
time.Sleep(time.Second)
}
})
AfterEach(func() {
cmd := exec.Command(contribTracingPath + "stop")
cmd.Stdout = os.Stdout
Expect(cmd.Run()).To(Succeed())
})
})
Describe("CreateNamespaces", func() {
It("should succeed without namespaces", func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
sut = tr.configGivenEnv()
podID := uuid.New().String()
response, err := sut.CreateNamespaces(
context.Background(),
&client.CreateNamespacesConfig{
PodID: podID,
},
)
Expect(err).To(Succeed())
Expect(response).NotTo(BeNil())
})
It("should fail without pod ID", func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
sut = tr.configGivenEnv()
response, err := sut.CreateNamespaces(
context.Background(),
&client.CreateNamespacesConfig{},
)
Expect(err).NotTo(Succeed())
Expect(response).To(BeNil())
})
It("should succeed without user namespace", func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
sut = tr.configGivenEnv()
podID := uuid.New().String()
response, err := sut.CreateNamespaces(
context.Background(),
&client.CreateNamespacesConfig{
PodID: podID,
Namespaces: []client.Namespace{
client.NamespaceIPC,
client.NamespaceNet,
client.NamespacePID,
client.NamespaceUTS,
},
},
)
Expect(err).To(Succeed())
Expect(response).NotTo(BeNil())
Expect(len(response.Namespaces)).To(BeEquivalentTo(5))
Expect(response.Namespaces[0].Type).To(Equal(client.NamespaceIPC))
Expect(response.Namespaces[1].Type).To(Equal(client.NamespacePID))
Expect(response.Namespaces[2].Type).To(Equal(client.NamespaceNet))
Expect(response.Namespaces[3].Type).To(Equal(client.NamespaceUser))
Expect(response.Namespaces[4].Type).To(Equal(client.NamespaceUTS))
for i, ns := range response.Namespaces {
stat, err := os.Lstat(ns.Path)
Expect(err).To(Succeed())
Expect(stat.IsDir()).To(BeFalse())
Expect(stat.Size()).To(BeZero())
Expect(stat.Mode()).To(Equal(fs.FileMode(0o444)))
Expect(filepath.Base(ns.Path)).To(Equal(podID))
Expect(err).To(Succeed())
const basePath = "/var/run/"
switch i {
case 0:
Expect(ns.Path).To(ContainSubstring(basePath + "ipcns/"))
case 1:
Expect(ns.Path).To(ContainSubstring(basePath + "pidns/"))
case 2:
Expect(ns.Path).To(ContainSubstring(basePath + "netns/"))
case 3:
Expect(ns.Path).To(ContainSubstring(basePath + "userns/"))
case 4:
Expect(ns.Path).To(ContainSubstring(basePath + "utsns/"))
}
}
})
It("should succeed with user namespace and custom base path", func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
sut = tr.configGivenEnv()
basePath := MustTempDir("ns-test-")
defer os.RemoveAll(basePath)
podID := uuid.New().String()
uids := []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}}
gids := []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}}
response, err := sut.CreateNamespaces(
context.Background(),
&client.CreateNamespacesConfig{
Namespaces: []client.Namespace{
client.NamespaceIPC,
client.NamespaceNet,
client.NamespacePID,
client.NamespaceUTS,
client.NamespaceUser,
},
IDMappings: idtools.NewIDMappingsFromMaps(uids, gids),
BasePath: basePath,
PodID: podID,
},
)
Expect(err).To(Succeed())
Expect(response).NotTo(BeNil())
Expect(len(response.Namespaces)).To(BeEquivalentTo(5))
Expect(response.Namespaces[0].Type).To(Equal(client.NamespaceIPC))
Expect(response.Namespaces[1].Type).To(Equal(client.NamespacePID))
Expect(response.Namespaces[2].Type).To(Equal(client.NamespaceNet))
Expect(response.Namespaces[3].Type).To(Equal(client.NamespaceUser))
Expect(response.Namespaces[4].Type).To(Equal(client.NamespaceUTS))
for _, ns := range response.Namespaces {
stat, err := os.Lstat(ns.Path)
Expect(err).To(Succeed())
Expect(stat.IsDir()).To(BeFalse())
Expect(stat.Size()).To(BeZero())
}
})
It("should fail with user namespace without mappings", func() {
tr = newTestRunner()
tr.createRuntimeConfig(false)
sut = tr.configGivenEnv()
response, err := sut.CreateNamespaces(
context.Background(),
&client.CreateNamespacesConfig{
Namespaces: []client.Namespace{
client.NamespaceUser,
},
},
)
Expect(err).NotTo(Succeed())
Expect(errors.Is(err, client.ErrMissingIDMappings)).To(BeTrue())
Expect(response).To(BeNil())
})
})
})
var _ = Describe("JSONLogger", func() {
var tr *testRunner
var sut *client.ConmonClient
AfterEach(func() {
Expect(os.RemoveAll(tr.tmpDir)).To(Succeed())
if sut != nil {
Expect(sut.Shutdown()).To(Succeed())
}
})
Describe("Logging", func() {
for _, terminal := range []bool{true, false} {
It(testName("should log in JSON format", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(
terminal,
[]string{"/busybox", "sh", "-c", "echo hello && echo world"},
nil,
)
sut = tr.configGivenEnv()
_, err := sut.CreateContainer(context.Background(), &client.CreateContainerConfig{
ID: tr.ctrID,
BundlePath: tr.tmpDir,
Terminal: terminal,
LogDrivers: []client.ContainerLogDriver{{
Type: client.LogDriverTypeJSONLogger,
Path: tr.logPath(),
}},
})
Expect(err).NotTo(HaveOccurred())
tr.startContainer(sut)
logContent, err := os.ReadFile(tr.logPath())
Expect(err).NotTo(HaveOccurred())
// Check if the log content is in JSON format
// This is a basic check assuming that a valid JSON starts and ends with braces '{' and '}'
Expect(strings.TrimSpace(string(logContent))).To(SatisfyAll(
Not(BeEmpty()),
HavePrefix("{"),
HaveSuffix("}"),
))
})
}
})
})
var _ = Describe("JournaldLogger", func() {
var tr *testRunner
var sut *client.ConmonClient
AfterEach(func() {
if sut != nil {
Expect(sut.Shutdown()).To(Succeed())
}
})
Describe("Logging", func() {
for _, terminal := range []bool{true, false} {
It(testName("should log to journald", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(
terminal,
[]string{"/busybox", "sh", "-c", "echo hello world && echo foo bar"},
nil,
)
sut = tr.configGivenEnv()
_, err := sut.CreateContainer(context.Background(), &client.CreateContainerConfig{
ID: tr.ctrID,
BundlePath: tr.tmpDir,
Terminal: terminal,
LogDrivers: []client.ContainerLogDriver{{Type: client.LogDriverTypeJournald}},
})
Expect(err).NotTo(HaveOccurred())
tr.startContainer(sut)
// Verify the journal logs
cmd := exec.Command("journalctl", "-n2", "_COMM=conmonrs")
stdout := strings.Builder{}
cmd.Stdout = &stdout
Expect(cmd.Run()).NotTo(HaveOccurred())
res := stdout.String()
Expect(res).To(ContainSubstring("hello world"))
Expect(res).To(ContainSubstring("foo bar"))
})
}
})
})
var _ = Describe("StreamingServer", func() {
var (
tr *testRunner
sut *client.ConmonClient
)
AfterEach(func() {
Expect(tr.rr.RunCommand("delete", "-f", tr.ctrID)).To(Succeed())
if sut != nil {
Expect(sut.Shutdown()).To(Succeed())
}
Expect(os.RemoveAll(tr.tmpDir)).To(Succeed())
})
Describe("ServeExecContainer", func() {
for _, terminal := range []bool{true, false} {
It(testName("should succeed", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "10"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
result, err := sut.ServeExecContainer(context.Background(), &client.ServeExecContainerConfig{
ID: tr.ctrID,
Command: []string{"/busybox", "sh", "-c", "echo -n stdout && echo -n stderr >&2"},
Tty: terminal,
Stdout: true,
Stderr: true,
})
Expect(err).To(Succeed())
Expect(result.URL).NotTo(BeEmpty())
config := &rest.Config{TLSClientConfig: rest.TLSClientConfig{Insecure: true}}
executor, err := remotecommand.NewWebSocketExecutor(config, "GET", result.URL)
Expect(err).NotTo(HaveOccurred())
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
streamOptions := remotecommand.StreamOptions{
Stdout: stdout,
Stderr: stderr,
Tty: terminal,
}
err = executor.StreamWithContext(context.Background(), streamOptions)
Expect(err).NotTo(HaveOccurred())
if terminal {
Expect(stdout.String()).To(Equal("stdoutstderr"))
Expect(stderr.Len()).To(BeZero())
} else {
Expect(stdout.String()).To(Equal("stdout"))
Expect(stderr.String()).To(Equal("stderr"))
}
})
It(testName("should fail if container does not exist", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "10"}, nil)
sut = tr.configGivenEnv()
result, err := sut.ServeExecContainer(context.Background(), &client.ServeExecContainerConfig{
ID: "wrong",
Command: []string{"/busybox", "sh", "-c", "echo -n stdout && echo -n stderr >&2"},
Tty: terminal,
Stdout: true,
Stderr: true,
})
Expect(err).To(HaveOccurred())
Expect(result).To(BeNil())
})
}
})
Describe("ServeAttachContainer", func() {
const terminal = true
It(testName("should succeed", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "watch", "-n0.5", "echo", "test"}, nil)
sut = tr.configGivenEnv()
tr.createContainer(sut, terminal)
tr.startContainer(sut)
result, err := sut.ServeAttachContainer(context.Background(), &client.ServeAttachContainerConfig{
ID: tr.ctrID,
Stdin: true,
Stdout: true,
Stderr: true,
})
Expect(err).To(Succeed())
Expect(result.URL).NotTo(BeEmpty())
config := &rest.Config{TLSClientConfig: rest.TLSClientConfig{Insecure: true}}
executor, err := remotecommand.NewWebSocketExecutor(config, "GET", result.URL)
Expect(err).NotTo(HaveOccurred())
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
streamOptions := remotecommand.StreamOptions{
Stdout: stdout,
Stderr: stderr,
Tty: terminal,
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err = executor.StreamWithContext(ctx, streamOptions)
Expect(err).To(HaveOccurred())
Expect(tr.rr.RunCommand("delete", "-f", tr.ctrID)).To(Succeed())
Expect(stdout.String()).To(ContainSubstring("echo test"))
Expect(stderr.String()).To(BeEmpty())
})
It(testName("should fail if container does not exist", terminal), func() {
tr = newTestRunner()
tr.createRuntimeConfigWithProcessArgs(terminal, []string{"/busybox", "sleep", "10"}, nil)
sut = tr.configGivenEnv()
result, err := sut.ServeAttachContainer(context.Background(), &client.ServeAttachContainerConfig{
ID: "wrong",
})
Expect(err).To(HaveOccurred())
Expect(result).To(BeNil())
})
})
})