mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
893 lines
27 KiB
Go
893 lines
27 KiB
Go
// Copyright 2018 The gVisor 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 image provides end-to-end image tests for runsc.
|
|
|
|
// Each test calls docker commands to start up a container, and tests that it
|
|
// is behaving properly, like connecting to a port or looking at the output.
|
|
// The container is killed and deleted at the end.
|
|
//
|
|
// Setup instruction in test/README.md.
|
|
package image
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff"
|
|
yaml "gopkg.in/yaml.v3"
|
|
"gvisor.dev/gvisor/pkg/test/dockerutil"
|
|
"gvisor.dev/gvisor/pkg/test/testutil"
|
|
)
|
|
|
|
var testAlpineImage = func() string {
|
|
switch runtime.GOARCH {
|
|
case "amd64":
|
|
return "gcr.io/gvisor-presubmit/basic/alpine_x86_64:latest"
|
|
case "arm64":
|
|
return "gcr.io/gvisor-presubmit/basic/alpine_aarch64:latest"
|
|
default:
|
|
panic(fmt.Sprintf("unsupported architecture: %s", runtime.GOARCH))
|
|
}
|
|
}()
|
|
|
|
// defaultWait defines how long to wait for progress.
|
|
//
|
|
// See BUILD: This is at least a "large" test, so allow up to 1 minute for any
|
|
// given "wait" step. Note that all tests are run in parallel, which may cause
|
|
// individual slow-downs (but a huge speed-up in aggregate).
|
|
const defaultWait = time.Minute
|
|
|
|
func TestHelloWorld(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := dockerutil.MakeContainer(ctx, t)
|
|
defer d.CleanUp(ctx)
|
|
|
|
// Run the basic container.
|
|
out, err := d.Run(ctx, dockerutil.RunOpts{
|
|
Image: "basic/alpine",
|
|
}, "echo", "Hello world!")
|
|
if err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Check the output.
|
|
if !strings.Contains(out, "Hello world!") {
|
|
t.Fatalf("docker didn't say hello: got %s", out)
|
|
}
|
|
}
|
|
|
|
func TestRust(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := dockerutil.MakeContainer(ctx, t)
|
|
defer d.CleanUp(ctx)
|
|
|
|
// Run the basic container.
|
|
out, err := d.Run(ctx, dockerutil.RunOpts{
|
|
Image: "basic/rust",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Check the output.
|
|
if !strings.Contains(out, "Hello, World!") {
|
|
t.Fatalf("Container didn't say Hello, World!: got %s", out)
|
|
}
|
|
}
|
|
|
|
func runHTTPRequest(ip string, port int) error {
|
|
url := fmt.Sprintf("http://%s:%d/not-found", ip, port)
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return fmt.Errorf("error reaching http server: %v", err)
|
|
}
|
|
if want := http.StatusNotFound; resp.StatusCode != want {
|
|
return fmt.Errorf("Wrong response code, got: %d, want: %d", resp.StatusCode, want)
|
|
}
|
|
|
|
url = fmt.Sprintf("http://%s:%d/latin10k.txt", ip, port)
|
|
resp, err = http.Get(url)
|
|
if err != nil {
|
|
return fmt.Errorf("Error reaching http server: %v", err)
|
|
}
|
|
if want := http.StatusOK; resp.StatusCode != want {
|
|
return fmt.Errorf("Wrong response code, got: %d, want: %d", resp.StatusCode, want)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("Error reading http response: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// READALL is the last word in the file. Ensures everything was read.
|
|
if want := "READALL"; strings.HasSuffix(string(body), want) {
|
|
return fmt.Errorf("response doesn't contain %q, resp: %q", want, body)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func testHTTPServer(t *testing.T, ip string, port int) {
|
|
const requests = 10
|
|
ch := make(chan error, requests)
|
|
for i := 0; i < requests; i++ {
|
|
go func() {
|
|
start := time.Now()
|
|
err := runHTTPRequest(ip, port)
|
|
log.Printf("Response time %v: %v", time.Since(start).String(), err)
|
|
ch <- err
|
|
}()
|
|
}
|
|
|
|
for i := 0; i < requests; i++ {
|
|
err := <-ch
|
|
if err != nil {
|
|
t.Errorf("testHTTPServer(%s, %d) failed: %v", ip, port, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHttpd(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := dockerutil.MakeContainer(ctx, t)
|
|
defer d.CleanUp(ctx)
|
|
|
|
// Start the container.
|
|
port := 80
|
|
opts := dockerutil.RunOpts{
|
|
Image: "basic/httpd",
|
|
}
|
|
d.CopyFiles(&opts, "/usr/local/apache2/htdocs", "test/image/latin10k.txt")
|
|
if err := d.Spawn(ctx, opts); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Find container IP address.
|
|
ip, err := d.FindIP(ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("docker.FindIP failed: %v", err)
|
|
}
|
|
|
|
// Wait until it's up and running.
|
|
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
|
|
t.Errorf("WaitForHTTP() timeout: %v", err)
|
|
}
|
|
|
|
testHTTPServer(t, ip.String(), port)
|
|
}
|
|
|
|
func TestNginx(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := dockerutil.MakeContainer(ctx, t)
|
|
defer d.CleanUp(ctx)
|
|
|
|
// Start the container.
|
|
port := 80
|
|
opts := dockerutil.RunOpts{
|
|
Image: "basic/nginx",
|
|
}
|
|
d.CopyFiles(&opts, "/usr/share/nginx/html", "test/image/latin10k.txt")
|
|
if err := d.Spawn(ctx, opts); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Find container IP address.
|
|
ip, err := d.FindIP(ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("docker.FindIP failed: %v", err)
|
|
}
|
|
|
|
// Wait until it's up and running.
|
|
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
|
|
t.Errorf("WaitForHTTP() timeout: %v", err)
|
|
}
|
|
|
|
testHTTPServer(t, ip.String(), port)
|
|
}
|
|
|
|
func TestMysql(t *testing.T) {
|
|
ctx := context.Background()
|
|
server := dockerutil.MakeContainer(ctx, t)
|
|
defer server.CleanUp(ctx)
|
|
|
|
// Start the container.
|
|
if err := server.Spawn(ctx, dockerutil.RunOpts{
|
|
Image: "basic/mysql",
|
|
Env: []string{
|
|
"MYSQL_ROOT_PASSWORD=foobar123",
|
|
"MYSQL_ROOT_HOST=%", // Allow anyone to connect to the server.
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Wait until it's up and running.
|
|
if _, err := server.WaitForOutput(ctx, "port: 3306 MySQL Community Server", defaultWait); err != nil {
|
|
t.Fatalf("WaitForOutput() timeout: %v", err)
|
|
}
|
|
|
|
// Generate the client and copy in the SQL payload.
|
|
client := dockerutil.MakeContainer(ctx, t)
|
|
defer client.CleanUp(ctx)
|
|
|
|
// Tell mysql client to connect to the server and execute the file in
|
|
// verbose mode to verify the output.
|
|
opts := dockerutil.RunOpts{
|
|
Image: "basic/mysql",
|
|
Links: []string{server.MakeLink("mysql")},
|
|
}
|
|
client.CopyFiles(&opts, "/sql", "test/image/mysql.sql")
|
|
if _, err := client.Run(ctx, opts, "mysql", "-hmysql", "-uroot", "-pfoobar123", "-v", "-e", "source /sql/mysql.sql"); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Ensure file executed to the end and shutdown mysql.
|
|
if _, err := server.WaitForOutput(ctx, "mysqld: Shutdown complete", defaultWait); err != nil {
|
|
t.Fatalf("WaitForOutput() timeout: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTomcat(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := dockerutil.MakeContainer(ctx, t)
|
|
defer d.CleanUp(ctx)
|
|
|
|
// Start the server.
|
|
port := 8080
|
|
if err := d.Spawn(ctx, dockerutil.RunOpts{
|
|
Image: "basic/tomcat",
|
|
}); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Find container IP address.
|
|
ip, err := d.FindIP(ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("docker.FindIP failed: %v", err)
|
|
}
|
|
|
|
// Wait until it's up and running.
|
|
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
|
|
t.Fatalf("WaitForHTTP() timeout: %v", err)
|
|
}
|
|
|
|
// Ensure that content is being served.
|
|
url := fmt.Sprintf("http://%s:%d", ip.String(), port)
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
t.Errorf("Error reaching http server: %v", err)
|
|
}
|
|
if want := http.StatusOK; resp.StatusCode != want {
|
|
t.Errorf("Wrong response code, got: %d, want: %d", resp.StatusCode, want)
|
|
}
|
|
}
|
|
|
|
func dockerLogs(ctx context.Context, d *dockerutil.Container) string {
|
|
logs, err := d.Logs(ctx)
|
|
if err != nil {
|
|
return fmt.Sprintf("Failed to get docker logs: %v", err)
|
|
}
|
|
return fmt.Sprintf("\n%s", logs)
|
|
}
|
|
|
|
func TestRuby(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := dockerutil.MakeContainer(ctx, t)
|
|
defer d.CleanUp(ctx)
|
|
|
|
// Execute the ruby workload.
|
|
port := 8080
|
|
opts := dockerutil.RunOpts{
|
|
Image: "image-test/ruby",
|
|
}
|
|
|
|
if err := d.Spawn(ctx, opts); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Find container IP address.
|
|
ip, err := d.FindIP(ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("docker.FindIP failed: %v", err)
|
|
}
|
|
|
|
// Wait until it's up and running.
|
|
if err := testutil.WaitForHTTP(ip.String(), port, time.Minute); err != nil {
|
|
t.Fatalf("WaitForHTTP() timeout: %v, docker logs: %v", err, dockerLogs(ctx, d))
|
|
}
|
|
|
|
// Ensure that content is being served.
|
|
url := fmt.Sprintf("http://%s:%d", ip.String(), port)
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
t.Errorf("error reaching http server: %v, docker logs: %v", err, dockerLogs(ctx, d))
|
|
}
|
|
if want := http.StatusOK; resp.StatusCode != want {
|
|
t.Errorf("wrong response code, got: %d, want: %d, docker logs: %v", resp.StatusCode, want, dockerLogs(ctx, d))
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("error reading body: %v, docker logs: %v", err, dockerLogs(ctx, d))
|
|
}
|
|
if got, want := string(body), "Hello World"; !strings.Contains(got, want) {
|
|
t.Errorf("invalid body content, got: %q, want: %q, docker logs: %v", got, want, dockerLogs(ctx, d))
|
|
}
|
|
}
|
|
|
|
func TestStdio(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := dockerutil.MakeContainer(ctx, t)
|
|
defer d.CleanUp(ctx)
|
|
|
|
wantStdout := "hello stdout"
|
|
wantStderr := "bonjour stderr"
|
|
cmd := fmt.Sprintf("echo %q; echo %q 1>&2;", wantStdout, wantStderr)
|
|
if err := d.Spawn(ctx, dockerutil.RunOpts{
|
|
Image: "basic/alpine",
|
|
}, "/bin/sh", "-c", cmd); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
for _, want := range []string{wantStdout, wantStderr} {
|
|
if _, err := d.WaitForOutput(ctx, want, defaultWait); err != nil {
|
|
t.Fatalf("docker didn't get output %q : %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTcpdump(t *testing.T) {
|
|
if testutil.IsRunningWithHostNet() {
|
|
t.Skip("docker doesn't work with hostinet")
|
|
}
|
|
ctx := context.Background()
|
|
// The "-docker" runtime comes with the net_raw capabilities enabled which are
|
|
// required for tcpdump.
|
|
d := dockerutil.MakeContainerWithRuntime(ctx, t, "-docker")
|
|
defer d.CleanUp(ctx)
|
|
|
|
if err := d.Spawn(ctx, dockerutil.RunOpts{
|
|
Image: "basic/tcpdump",
|
|
}, "sleep", "infinity"); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Stick to `lo` to avoid catching packets from the host. The "port 9999"
|
|
// does not actually do anything because we do not yet support installing
|
|
// a filter bpf program.
|
|
cmd := "tcpdump -c 2 -i lo port 9999"
|
|
tcpdumpProc, err := d.ExecProcess(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd)
|
|
if err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
cmd = "python3 sender.py"
|
|
senderProc, err := d.ExecProcess(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd)
|
|
if err != nil {
|
|
t.Fatalf("docker exec failed: %v", err)
|
|
}
|
|
|
|
if status, err := senderProc.WaitExitStatus(ctx); err != nil || status != 0 {
|
|
t.Fatalf("docker exec failed: %v, status: %d", err, status)
|
|
}
|
|
if status, err := tcpdumpProc.WaitExitStatus(ctx); err != nil || status != 0 {
|
|
t.Fatalf("docker exec failed: %v, status: %d", err, status)
|
|
}
|
|
expectedOutputStr1 := "IP localhost.9999 > localhost.9999: UDP, length 4"
|
|
logs, err := tcpdumpProc.Logs()
|
|
if err != nil {
|
|
t.Fatalf("docker exec failed: %v", err)
|
|
}
|
|
if !strings.Contains(logs, expectedOutputStr1) {
|
|
t.Fatalf("docker didn't get output: %q, got: %q", expectedOutputStr1, logs)
|
|
}
|
|
expectedOutputStr2 := "IP localhost.9999 > localhost.9999: UDP, length 8"
|
|
if !strings.Contains(logs, expectedOutputStr2) {
|
|
t.Fatalf("docker didn't get output: %q, got: %q", expectedOutputStr2, logs)
|
|
}
|
|
|
|
// Check that `any` also works to guard against b/411198401.
|
|
cmd = "tcpdump -c 2 -i any port 9999"
|
|
tcpdumpProc, err = d.ExecProcess(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd)
|
|
if err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
cmd = "python3 sender.py"
|
|
senderProc, err = d.ExecProcess(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd)
|
|
if err != nil {
|
|
t.Fatalf("docker exec failed: %v", err)
|
|
}
|
|
|
|
// We make no assertions about the output because we might catch host packets
|
|
// too: we only rely on tcpdump's exit status.
|
|
if status, err := tcpdumpProc.WaitExitStatus(ctx); err != nil || status != 0 {
|
|
t.Fatalf("docker exec failed: %v, status: %d", err, status)
|
|
}
|
|
if status, err := senderProc.WaitExitStatus(ctx); err != nil || status != 0 {
|
|
t.Fatalf("docker exec failed: %v, status: %d", err, status)
|
|
}
|
|
}
|
|
|
|
func dockerInGvisorCapabilities() []string {
|
|
return []string{
|
|
"audit_write",
|
|
"chown",
|
|
"dac_override",
|
|
"fowner",
|
|
"fsetid",
|
|
"kill",
|
|
"mknod",
|
|
"net_admin",
|
|
"net_bind_service",
|
|
"net_raw",
|
|
"setfcap",
|
|
"setgid",
|
|
"setpcap",
|
|
"setuid",
|
|
"sys_admin",
|
|
"sys_chroot",
|
|
"sys_ptrace",
|
|
}
|
|
}
|
|
|
|
type dockerCommandOptions struct {
|
|
hostNetwork bool
|
|
privileged bool
|
|
}
|
|
|
|
type dockerComposeConfig struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
Services map[string]dockerService `yaml:"services,omitempty"`
|
|
}
|
|
|
|
type dockerService struct {
|
|
Image string `yaml:"image,omitempty"`
|
|
Build dockerBuild `yaml:"build,omitempty"`
|
|
Privileged bool `yaml:"privileged,omitempty"`
|
|
NetworkdMode string `yaml:"network_mode,omitempty"`
|
|
}
|
|
|
|
type dockerBuild struct {
|
|
Context string `yaml:"context,omitempty"`
|
|
Network string `yaml:"network,omitempty"`
|
|
}
|
|
|
|
func testDockerMatrix(t *testing.T, overlay bool) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
defer cancel()
|
|
|
|
d := startDockerdInGvisor(ctx, t, overlay)
|
|
defer func() {
|
|
if t.Failed() {
|
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cleanupCancel()
|
|
t.Logf("Dockerd daemon container logs:\n%s", dockerLogs(cleanupCtx, d))
|
|
}
|
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cleanupCancel()
|
|
d.CleanUp(cleanupCtx)
|
|
}()
|
|
|
|
definitions := []struct {
|
|
name string
|
|
testFunc func(ctx context.Context, t *testing.T, d *dockerutil.Container, opts dockerCommandOptions)
|
|
testHostNetwork bool
|
|
testPrivileged bool
|
|
}{
|
|
{"docker_run", testDockerRun, true, true},
|
|
{"docker_build", testDockerBuild, true, false},
|
|
{"docker_exec", testDockerExec, false, true},
|
|
{"docker_compose_run", testDockerComposeRun, true, true},
|
|
{"docker_compose_build", testDockerComposeBuild, true, false},
|
|
}
|
|
for _, def := range definitions {
|
|
hostNetworkOpts := []bool{false}
|
|
if def.testHostNetwork {
|
|
hostNetworkOpts = []bool{true, false}
|
|
}
|
|
privilegedOpts := []bool{false}
|
|
if def.testPrivileged {
|
|
privilegedOpts = []bool{true, false}
|
|
}
|
|
for _, hostNetwork := range hostNetworkOpts {
|
|
for _, privileged := range privilegedOpts {
|
|
opts := dockerCommandOptions{
|
|
hostNetwork: hostNetwork,
|
|
privileged: privileged,
|
|
}
|
|
var nameParts []string
|
|
nameParts = append(nameParts, def.name)
|
|
if def.testHostNetwork {
|
|
if hostNetwork {
|
|
nameParts = append(nameParts, "host_network")
|
|
} else {
|
|
nameParts = append(nameParts, "bridge_network")
|
|
}
|
|
}
|
|
if def.testPrivileged {
|
|
if privileged {
|
|
nameParts = append(nameParts, "privileged")
|
|
} else {
|
|
nameParts = append(nameParts, "non_privileged")
|
|
}
|
|
}
|
|
name := strings.Join(nameParts, "_")
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
if t.Failed() {
|
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cleanupCancel()
|
|
t.Logf("Dockerd daemon logs for %s:\n%s", name, dockerLogs(cleanupCtx, d))
|
|
logNetworkInterfaces(cleanupCtx, t, d)
|
|
}
|
|
})
|
|
def.testFunc(ctx, t, d, opts)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDockerWithVFS(t *testing.T) {
|
|
if testutil.IsRunningWithHostNet() {
|
|
t.Skip("docker doesn't work with hostinet")
|
|
}
|
|
t.Parallel()
|
|
testDockerMatrix(t, false)
|
|
}
|
|
|
|
func TestDockerWithOverlay(t *testing.T) {
|
|
if testutil.IsRunningWithHostNet() {
|
|
t.Skip("docker doesn't work with hostinet")
|
|
}
|
|
t.Parallel()
|
|
testDockerMatrix(t, true)
|
|
}
|
|
|
|
// The container returned by this function has to be cleaned up by the caller.
|
|
func startDockerdInGvisor(ctx context.Context, t *testing.T, overlay bool) *dockerutil.Container {
|
|
d := dockerutil.MakeContainerWithRuntime(ctx, t, "-docker")
|
|
|
|
// Start the container which starts dockerd.
|
|
opts := dockerutil.RunOpts{
|
|
Image: "basic/docker",
|
|
CapAdd: dockerInGvisorCapabilities(),
|
|
}
|
|
|
|
var args []string
|
|
if !overlay {
|
|
args = append(args, "--no-overlay")
|
|
}
|
|
if err := d.Spawn(ctx, opts, args...); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
|
|
// Stream dockerd logs into a file in TEST_UNDECLARED_OUTPUTS_DIR (test artifacts).
|
|
if dir, ok := os.LookupEnv("TEST_UNDECLARED_OUTPUTS_DIR"); ok {
|
|
logFilePath := path.Join(dir, fmt.Sprintf("dockerd_%s.log", t.Name()))
|
|
logFile, err := os.Create(logFilePath)
|
|
if err != nil {
|
|
t.Logf("failed to create dockerd log file: %v", err)
|
|
} else {
|
|
t.Cleanup(func() { _ = logFile.Close() })
|
|
go func() {
|
|
_ = d.StreamLogs(context.Background(), logFile)
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Wait for the docker daemon.
|
|
cb := backoff.NewConstantBackOff(1 * time.Second)
|
|
if err := backoff.Retry(func() error {
|
|
_, err := dockerInGvisorExecOutput(ctx, d, []string{"docker", "info"})
|
|
return err
|
|
}, backoff.WithMaxRetries(cb, 30)); err != nil {
|
|
t.Fatalf("docker daemon failed to start: %v", err)
|
|
}
|
|
|
|
logNetworkInterfaces(ctx, t, d)
|
|
return d
|
|
}
|
|
|
|
// logNetworkInterfaces logs network interfaces, routing tables, and iptables in the container.
|
|
func logNetworkInterfaces(ctx context.Context, t *testing.T, d *dockerutil.Container) {
|
|
if out, err := d.Exec(ctx, dockerutil.ExecOpts{}, "ip", "-o", "addr", "show"); err == nil {
|
|
t.Logf("Dockerd container network interfaces:\n%s", strings.TrimSpace(out))
|
|
} else {
|
|
t.Logf("Failed to query network interfaces: %v", err)
|
|
}
|
|
|
|
if out, err := d.Exec(ctx, dockerutil.ExecOpts{}, "ip", "route", "show"); err == nil {
|
|
t.Logf("Dockerd container routing table:\n%s", strings.TrimSpace(out))
|
|
}
|
|
|
|
if out, err := d.Exec(ctx, dockerutil.ExecOpts{}, "iptables", "-t", "nat", "-L", "POSTROUTING", "-n", "-v"); err == nil {
|
|
t.Logf("Dockerd container NAT POSTROUTING rules:\n%s", strings.TrimSpace(out))
|
|
}
|
|
}
|
|
|
|
// checkDockerImage list available images and checks if the given image is
|
|
// present.
|
|
func checkDockerImage(ctx context.Context, imageName string, d *dockerutil.Container) error {
|
|
_, err := dockerInGvisorExecOutput(ctx, d, []string{"docker", "image", "inspect", imageName})
|
|
if err != nil {
|
|
return fmt.Errorf("docker image inspect %q failed: %v", imageName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func removeDockerImage(ctx context.Context, imageName string, d *dockerutil.Container) error {
|
|
_, err := dockerInGvisorExecOutput(ctx, d, []string{"docker", "image", "rm", imageName})
|
|
return err
|
|
}
|
|
|
|
func writeFileInContainer(ctx context.Context, d *dockerutil.Container, filePath, content string) error {
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(content))
|
|
cmd := []string{"/bin/sh", "-c", `echo "$1" | base64 -d > "$2"`, "--", encoded, filePath}
|
|
_, err := dockerInGvisorExecOutput(ctx, d, cmd)
|
|
return err
|
|
}
|
|
|
|
func dockerInGvisorExecOutput(ctx context.Context, d *dockerutil.Container, cmd []string) (string, error) {
|
|
execProc, err := d.ExecProcess(ctx, dockerutil.ExecOpts{}, cmd...)
|
|
if err != nil {
|
|
return "", fmt.Errorf("docker exec failed: %v", err)
|
|
}
|
|
output, err := execProc.Logs()
|
|
if err != nil {
|
|
return "", fmt.Errorf("docker logs failed: %v", err)
|
|
}
|
|
status, err := execProc.ExitCode(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get exit code: %v", err)
|
|
}
|
|
if status != 0 {
|
|
return output, fmt.Errorf("command %v failed with status %d, output: %q", cmd, status, output)
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func testDockerRun(ctx context.Context, t *testing.T, d *dockerutil.Container, opts dockerCommandOptions) {
|
|
cmd := []string{"docker", "run", "--rm"}
|
|
if opts.hostNetwork {
|
|
cmd = append(cmd, "--network", "host")
|
|
}
|
|
if opts.privileged {
|
|
cmd = append(cmd, "--privileged")
|
|
}
|
|
cmd = append(cmd, testAlpineImage, "sh", "-c", "apk add curl && apk info -d curl")
|
|
|
|
expectedOutput := "URL retrival utility and library"
|
|
output, err := dockerInGvisorExecOutput(ctx, d, cmd)
|
|
if err != nil {
|
|
t.Fatalf("docker exec failed: %v", err)
|
|
}
|
|
if !strings.Contains(output, expectedOutput) {
|
|
t.Fatalf("docker didn't get output expected: %q, got: %q", expectedOutput, output)
|
|
}
|
|
}
|
|
|
|
func testDockerBuild(ctx context.Context, t *testing.T, d *dockerutil.Container, opts dockerCommandOptions) {
|
|
tmpDir, err := d.Exec(ctx, dockerutil.ExecOpts{}, "mktemp", "-d")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
tmpDir = strings.TrimSpace(tmpDir)
|
|
defer func() {
|
|
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "rm", "-rf", tmpDir); err != nil {
|
|
t.Logf("failed to cleanup temp dir %s: %v", tmpDir, err)
|
|
}
|
|
}()
|
|
|
|
dockerfilePath := path.Join(tmpDir, "Dockerfile")
|
|
dockerfileContent := fmt.Sprintf("FROM %s\nRUN apk add git\n", testAlpineImage)
|
|
if err := writeFileInContainer(ctx, d, dockerfilePath, dockerfileContent); err != nil {
|
|
t.Fatalf("failed to write Dockerfile: %v", err)
|
|
}
|
|
|
|
imageName := strings.ToLower(strings.ReplaceAll(testutil.RandomID("test_docker_build"), "/", "-"))
|
|
cmd := []string{"docker", "build"}
|
|
if opts.hostNetwork {
|
|
cmd = append(cmd, "--network", "host")
|
|
}
|
|
cmd = append(cmd, "-t", imageName, tmpDir)
|
|
if _, err := dockerInGvisorExecOutput(ctx, d, cmd); err != nil {
|
|
t.Fatalf("docker exec failed: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := removeDockerImage(ctx, imageName, d); err != nil {
|
|
t.Logf("failed to remove docker image %s: %v", imageName, err)
|
|
}
|
|
}()
|
|
if err := checkDockerImage(ctx, imageName, d); err != nil {
|
|
t.Fatalf("failed to find docker image: %v", err)
|
|
}
|
|
}
|
|
|
|
func testDockerExec(ctx context.Context, t *testing.T, d *dockerutil.Container, opts dockerCommandOptions) {
|
|
containerName := strings.ReplaceAll(testutil.RandomID("test_docker_exec"), "/", "-")
|
|
// Start a container with a sleep command to ensure that the container
|
|
// doesn't exit immediately.
|
|
cmd := []string{"docker", "run", "--rm", "-d", "--name", containerName, testAlpineImage, "sleep", "180"}
|
|
if _, err := dockerInGvisorExecOutput(ctx, d, cmd); err != nil {
|
|
t.Fatalf("docker run failed: %v", err)
|
|
}
|
|
// Kill the container at the end of the test.
|
|
defer func() {
|
|
if _, err := dockerInGvisorExecOutput(ctx, d, []string{"docker", "kill", containerName}); err != nil {
|
|
t.Logf("docker container kill failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
cb := backoff.NewConstantBackOff(500 * time.Millisecond)
|
|
err := backoff.Retry(func() error {
|
|
inspectOutput, err := dockerInGvisorExecOutput(ctx, d, []string{"docker", "container", "inspect", containerName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.Contains(inspectOutput, "\"Status\": \"running\"") {
|
|
return fmt.Errorf("container %s is not running yet", containerName)
|
|
}
|
|
return nil
|
|
}, backoff.WithMaxRetries(cb, 20))
|
|
if err != nil {
|
|
t.Fatalf("container failed to start: %v", err)
|
|
}
|
|
|
|
execCmd := []string{"docker", "exec"}
|
|
if opts.privileged {
|
|
execCmd = append(execCmd, "--privileged")
|
|
}
|
|
// Execute echo command in the container.
|
|
execCmd = append(execCmd, containerName, "echo", "exec in "+containerName)
|
|
|
|
output, err := dockerInGvisorExecOutput(ctx, d, execCmd)
|
|
if err != nil {
|
|
t.Fatalf("docker exec failed: %v", err)
|
|
}
|
|
expectedOutput := "exec in " + containerName
|
|
if !strings.Contains(output, expectedOutput) {
|
|
t.Fatalf("docker didn't get output expected: %q, got: %q", expectedOutput, output)
|
|
}
|
|
}
|
|
|
|
func testDockerComposeBuild(ctx context.Context, t *testing.T, d *dockerutil.Container, opts dockerCommandOptions) {
|
|
tmpDir, err := d.Exec(ctx, dockerutil.ExecOpts{}, "mktemp", "-d")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
tmpDir = strings.TrimSpace(tmpDir)
|
|
defer func() {
|
|
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "rm", "-rf", tmpDir); err != nil {
|
|
t.Logf("failed to cleanup temp dir %s: %v", tmpDir, err)
|
|
}
|
|
}()
|
|
|
|
// Letters in image name must be lowercase.
|
|
imageName := strings.ToLower(strings.ReplaceAll(testutil.RandomID("testdockercomposebuild"), "/", "-"))
|
|
network := ""
|
|
if opts.hostNetwork {
|
|
network = "host"
|
|
} else {
|
|
// TODO(https://gvisor.dev/issues/11937): re-enable bridge network test.
|
|
t.Skip("Skip docker compose build test with bridge network.")
|
|
}
|
|
config := dockerComposeConfig{
|
|
Name: "image_test",
|
|
Services: map[string]dockerService{
|
|
imageName: dockerService{
|
|
Image: imageName,
|
|
Build: dockerBuild{
|
|
Context: ".",
|
|
Network: network,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buildConfig, err := yaml.Marshal(config)
|
|
if err != nil {
|
|
t.Fatalf("error marshaling to docker-compose.yml: %v", err)
|
|
}
|
|
dockerfileContent := fmt.Sprintf("FROM %s\nRUN apk add curl\n", testAlpineImage)
|
|
dockerfilePath := path.Join(tmpDir, "Dockerfile")
|
|
dockerComposePath := path.Join(tmpDir, "docker-compose.yml")
|
|
|
|
if err := writeFileInContainer(ctx, d, dockerfilePath, dockerfileContent); err != nil {
|
|
t.Fatalf("failed to write Dockerfile: %v", err)
|
|
}
|
|
if err := writeFileInContainer(ctx, d, dockerComposePath, string(buildConfig)); err != nil {
|
|
t.Fatalf("failed to write docker-compose.yml: %v", err)
|
|
}
|
|
|
|
cmd := []string{"docker", "compose", "-f", dockerComposePath, "build"}
|
|
if _, err := dockerInGvisorExecOutput(ctx, d, cmd); err != nil {
|
|
t.Fatalf("docker compose build failed: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := removeDockerImage(ctx, imageName, d); err != nil {
|
|
t.Logf("failed to remove docker image %s: %v", imageName, err)
|
|
}
|
|
}()
|
|
|
|
if err := checkDockerImage(ctx, imageName, d); err != nil {
|
|
t.Fatalf("failed to find docker image: %v", err)
|
|
}
|
|
}
|
|
|
|
func testDockerComposeRun(ctx context.Context, t *testing.T, d *dockerutil.Container, opts dockerCommandOptions) {
|
|
tmpDir, err := d.Exec(ctx, dockerutil.ExecOpts{}, "mktemp", "-d")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
tmpDir = strings.TrimSpace(tmpDir)
|
|
defer func() {
|
|
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "rm", "-rf", tmpDir); err != nil {
|
|
t.Logf("failed to cleanup temp dir %s: %v", tmpDir, err)
|
|
}
|
|
}()
|
|
|
|
dockerComposeAppName := strings.ToLower(strings.ReplaceAll(testutil.RandomID("docker-compose-run-test-app"), "/", "-"))
|
|
dockerComposeFilePath := path.Join(tmpDir, "docker-compose.yml")
|
|
networkMode := ""
|
|
// TODO(b/436936268): test bridge network driver in docker compose run.
|
|
// The option now has no impact since the test command doesn't attempt to connect to internet.
|
|
if opts.hostNetwork {
|
|
networkMode = "host"
|
|
}
|
|
config := dockerComposeConfig{
|
|
Name: "image_test",
|
|
Services: map[string]dockerService{
|
|
dockerComposeAppName: dockerService{
|
|
Image: testAlpineImage,
|
|
Privileged: opts.privileged,
|
|
NetworkdMode: networkMode,
|
|
},
|
|
},
|
|
}
|
|
dockerComposeContent, err := yaml.Marshal(config)
|
|
if err != nil {
|
|
t.Fatalf("error marshaling to docker-compose.yml: %v", err)
|
|
}
|
|
|
|
if err := writeFileInContainer(ctx, d, dockerComposeFilePath, string(dockerComposeContent)); err != nil {
|
|
t.Fatalf("failed to write %s: %v", dockerComposeFilePath, err)
|
|
}
|
|
|
|
cmd := []string{"docker", "compose", "-f", dockerComposeFilePath, "run", "--rm", dockerComposeAppName, "sh", "-c", "echo hello gVisor"}
|
|
output, err := dockerInGvisorExecOutput(ctx, d, cmd)
|
|
if err != nil {
|
|
t.Fatalf("docker compose run failed: %v", err)
|
|
}
|
|
expectedOutput := "hello gVisor"
|
|
if !strings.Contains(output, expectedOutput) {
|
|
t.Fatalf("docker didn't get output expected: %q, got: %q", expectedOutput, output)
|
|
}
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
dockerutil.EnsureSupportedDockerVersion()
|
|
flag.Parse()
|
|
os.Exit(m.Run())
|
|
}
|