moby/integration/daemon/daemon_test.go
Sebastiaan van Stijn ebef4efb88
api/types: move ContainerLogsOptions to api/types/container
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-10-12 11:30:12 +02:00

563 lines
18 KiB
Go

package daemon // import "github.com/docker/docker/integration/daemon"
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"testing"
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/daemon/config"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/daemon"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
"gotest.tools/v3/skip"
)
func TestConfigDaemonID(t *testing.T) {
skip.If(t, runtime.GOOS == "windows")
_ = testutil.StartSpan(baseContext, t)
d := daemon.New(t)
defer d.Stop(t)
d.Start(t, "--iptables=false")
info := d.Info(t)
assert.Check(t, info.ID != "")
d.Stop(t)
// Verify that (if present) the engine-id file takes precedence
const engineID = "this-is-the-engine-id"
idFile := filepath.Join(d.RootDir(), "engine-id")
assert.Check(t, os.Remove(idFile))
// Using 0644 to allow rootless daemons to read the file (ideally
// we'd chown the file to have the remapped user as owner).
err := os.WriteFile(idFile, []byte(engineID), 0o644)
assert.NilError(t, err)
d.Start(t, "--iptables=false")
info = d.Info(t)
assert.Equal(t, info.ID, engineID)
d.Stop(t)
}
func TestDaemonConfigValidation(t *testing.T) {
skip.If(t, runtime.GOOS == "windows")
ctx := testutil.StartSpan(baseContext, t)
d := daemon.New(t)
dockerBinary, err := d.BinaryPath()
assert.NilError(t, err)
params := []string{"--validate", "--config-file"}
dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
if dest == "" {
dest = os.Getenv("DEST")
}
testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata")
const (
validOut = "configuration OK"
failedOut = "unable to configure the Docker daemon with file"
)
tests := []struct {
name string
args []string
expectedOut string
}{
{
name: "config with no content",
args: append(params, filepath.Join(testdata, "empty-config-1.json")),
expectedOut: validOut,
},
{
name: "config with {}",
args: append(params, filepath.Join(testdata, "empty-config-2.json")),
expectedOut: validOut,
},
{
name: "invalid config",
args: append(params, filepath.Join(testdata, "invalid-config-1.json")),
expectedOut: failedOut,
},
{
name: "malformed config",
args: append(params, filepath.Join(testdata, "malformed-config.json")),
expectedOut: failedOut,
},
{
name: "valid config",
args: append(params, filepath.Join(testdata, "valid-config-1.json")),
expectedOut: validOut,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_ = testutil.StartSpan(ctx, t)
cmd := exec.Command(dockerBinary, tc.args...)
out, err := cmd.CombinedOutput()
assert.Check(t, is.Contains(string(out), tc.expectedOut))
if tc.expectedOut == failedOut {
assert.ErrorContains(t, err, "", "expected an error, but got none")
} else {
assert.NilError(t, err)
}
})
}
}
func TestConfigDaemonSeccompProfiles(t *testing.T) {
skip.If(t, runtime.GOOS == "windows")
ctx := testutil.StartSpan(baseContext, t)
d := daemon.New(t)
defer d.Stop(t)
tests := []struct {
doc string
profile string
expectedProfile string
}{
{
doc: "empty profile set",
profile: "",
expectedProfile: config.SeccompProfileDefault,
},
{
doc: "default profile",
profile: config.SeccompProfileDefault,
expectedProfile: config.SeccompProfileDefault,
},
{
doc: "unconfined profile",
profile: config.SeccompProfileUnconfined,
expectedProfile: config.SeccompProfileUnconfined,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.doc, func(t *testing.T) {
_ = testutil.StartSpan(ctx, t)
d.Start(t, "--seccomp-profile="+tc.profile)
info := d.Info(t)
assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile))
d.Stop(t)
cfg := filepath.Join(d.RootDir(), "daemon.json")
err := os.WriteFile(cfg, []byte(`{"seccomp-profile": "`+tc.profile+`"}`), 0o644)
assert.NilError(t, err)
d.Start(t, "--config-file", cfg)
info = d.Info(t)
assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile))
d.Stop(t)
})
}
}
func TestDaemonProxy(t *testing.T) {
skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
skip.If(t, os.Getenv("DOCKER_ROOTLESS") != "", "cannot connect to localhost proxy in rootless environment")
ctx := testutil.StartSpan(baseContext, t)
newProxy := func(rcvd *string, t *testing.T) *httptest.Server {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*rcvd = r.Host
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte("OK"))
}))
t.Cleanup(s.Close)
return s
}
const userPass = "myuser:mypassword@"
// Configure proxy through env-vars
t.Run("environment variables", func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
var received string
proxyServer := newProxy(&received, t)
d := daemon.New(t, daemon.WithEnvVars(
"HTTP_PROXY="+proxyServer.URL,
"HTTPS_PROXY="+proxyServer.URL,
"NO_PROXY=example.com",
))
c := d.NewClientT(t)
d.Start(t, "--iptables=false")
defer d.Stop(t)
info := d.Info(t)
assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL))
assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL))
assert.Check(t, is.Equal(info.NoProxy, "example.com"))
_, err := c.ImagePull(ctx, "example.org:5000/some/image:latest", types.ImagePullOptions{})
assert.ErrorContains(t, err, "", "pulling should have failed")
assert.Equal(t, received, "example.org:5000")
// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
assert.ErrorContains(t, err, "", "pulling should have failed")
assert.Equal(t, received, "example.org:5000", "should not have used proxy")
})
// Configure proxy through command-line flags
t.Run("command-line options", func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
var received string
proxyServer := newProxy(&received, t)
d := daemon.New(t, daemon.WithEnvVars(
"HTTP_PROXY="+"http://"+userPass+"from-env-http.invalid",
"http_proxy="+"http://"+userPass+"from-env-http.invalid",
"HTTPS_PROXY="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
"https_proxy="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
"NO_PROXY=ignore.invalid",
"no_proxy=ignore.invalid",
))
d.Start(t, "--iptables=false", "--http-proxy", proxyServer.URL, "--https-proxy", proxyServer.URL, "--no-proxy", "example.com")
defer d.Stop(t)
c := d.NewClientT(t)
info := d.Info(t)
assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL))
assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL))
assert.Check(t, is.Equal(info.NoProxy, "example.com"))
ok, _ := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchAll(
"overriding existing proxy variable with value from configuration",
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
))
assert.Assert(t, ok)
ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass))
assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs)
_, err := c.ImagePull(ctx, "example.org:5001/some/image:latest", types.ImagePullOptions{})
assert.ErrorContains(t, err, "", "pulling should have failed")
assert.Equal(t, received, "example.org:5001")
// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
assert.ErrorContains(t, err, "", "pulling should have failed")
assert.Equal(t, received, "example.org:5001", "should not have used proxy")
})
// Configure proxy through configuration file
t.Run("configuration file", func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
var received string
proxyServer := newProxy(&received, t)
d := daemon.New(t, daemon.WithEnvVars(
"HTTP_PROXY="+"http://"+userPass+"from-env-http.invalid",
"http_proxy="+"http://"+userPass+"from-env-http.invalid",
"HTTPS_PROXY="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
"https_proxy="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
"NO_PROXY=ignore.invalid",
"no_proxy=ignore.invalid",
))
c := d.NewClientT(t)
configFile := filepath.Join(d.RootDir(), "daemon.json")
configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyServer.URL)
assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0o644))
d.Start(t, "--iptables=false", "--config-file", configFile)
defer d.Stop(t)
info := d.Info(t)
assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL))
assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL))
assert.Check(t, is.Equal(info.NoProxy, "example.com"))
d.ScanLogsT(ctx, t, daemon.ScanLogsMatchAll(
"overriding existing proxy variable with value from configuration",
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
))
_, err := c.ImagePull(ctx, "example.org:5002/some/image:latest", types.ImagePullOptions{})
assert.ErrorContains(t, err, "", "pulling should have failed")
assert.Equal(t, received, "example.org:5002")
// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
assert.ErrorContains(t, err, "", "pulling should have failed")
assert.Equal(t, received, "example.org:5002", "should not have used proxy")
})
// Conflicting options (passed both through command-line options and config file)
t.Run("conflicting options", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
const (
proxyRawURL = "https://" + userPass + "example.org"
proxyURL = "https://xxxxx:xxxxx@example.org"
)
d := daemon.New(t)
configFile := filepath.Join(d.RootDir(), "daemon.json")
configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyRawURL)
assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0o644))
err := d.StartWithError("--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com", "--config-file", configFile, "--validate")
assert.ErrorContains(t, err, "daemon exited during startup")
expected := fmt.Sprintf(
`the following directives are specified both as a flag and in the configuration file: http-proxy: (from flag: %[1]s, from file: %[1]s), https-proxy: (from flag: %[1]s, from file: %[1]s), no-proxy: (from flag: example.com, from file: example.com)`,
proxyURL,
)
poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchString(expected)))
})
// Make sure values are sanitized when reloading the daemon-config
t.Run("reload sanitized", func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
const (
proxyRawURL = "https://" + userPass + "example.org"
proxyURL = "https://xxxxx:xxxxx@example.org"
)
d := daemon.New(t)
d.Start(t, "--iptables=false", "--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com")
defer d.Stop(t)
err := d.Signal(syscall.SIGHUP)
assert.NilError(t, err)
poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchAll("Reloaded configuration:", proxyURL)))
ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass))
assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs)
})
}
func TestLiveRestore(t *testing.T) {
skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
_ = testutil.StartSpan(baseContext, t)
t.Run("volume references", testLiveRestoreVolumeReferences)
}
func testLiveRestoreVolumeReferences(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(baseContext, t)
d := daemon.New(t)
d.StartWithBusybox(ctx, t, "--live-restore", "--iptables=false")
defer func() {
d.Stop(t)
d.Cleanup(t)
}()
c := d.NewClientT(t)
runTest := func(t *testing.T, policy containertypes.RestartPolicyMode) {
t.Run(string(policy), func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
volName := "test-live-restore-volume-references-" + string(policy)
_, err := c.VolumeCreate(ctx, volume.CreateOptions{Name: volName})
assert.NilError(t, err)
// Create a container that uses the volume
m := mount.Mount{
Type: mount.TypeVolume,
Source: volName,
Target: "/foo",
}
cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"), container.WithRestartPolicy(policy))
defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
// Stop the daemon
d.Restart(t, "--live-restore", "--iptables=false")
// Try to remove the volume
err = c.VolumeRemove(ctx, volName, false)
assert.ErrorContains(t, err, "volume is in use")
_, err = c.VolumeInspect(ctx, volName)
assert.NilError(t, err)
})
}
t.Run("restartPolicy", func(t *testing.T) {
runTest(t, containertypes.RestartPolicyAlways)
runTest(t, containertypes.RestartPolicyUnlessStopped)
runTest(t, containertypes.RestartPolicyOnFailure)
runTest(t, containertypes.RestartPolicyDisabled)
})
// Make sure that the local volume driver's mount ref count is restored
// Addresses https://github.com/moby/moby/issues/44422
t.Run("local volume with mount options", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
v, err := c.VolumeCreate(ctx, volume.CreateOptions{
Driver: "local",
Name: "test-live-restore-volume-references-local",
DriverOpts: map[string]string{
"type": "tmpfs",
"device": "tmpfs",
},
})
assert.NilError(t, err)
m := mount.Mount{
Type: mount.TypeVolume,
Source: v.Name,
Target: "/foo",
}
const testContent = "hello"
cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("sh", "-c", "echo "+testContent+">>/foo/test.txt; sleep infinity"))
defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
// Wait until container creates a file in the volume.
poll.WaitOn(t, func(t poll.LogT) poll.Result {
stat, err := c.ContainerStatPath(ctx, cID, "/foo/test.txt")
if err != nil {
if errdefs.IsNotFound(err) {
return poll.Continue("file doesn't yet exist")
}
return poll.Error(err)
}
if int(stat.Size) != len(testContent)+1 {
return poll.Error(fmt.Errorf("unexpected test file size: %d", stat.Size))
}
return poll.Success()
})
d.Restart(t, "--live-restore", "--iptables=false")
// Try to remove the volume
// This should fail since its used by a container
err = c.VolumeRemove(ctx, v.Name, false)
assert.ErrorContains(t, err, "volume is in use")
t.Run("volume still mounted", func(t *testing.T) {
skip.If(t, testEnv.IsRootless(), "restarted rootless daemon has a new mount namespace and it won't have the previous mounts")
// Check if a new container with the same volume has access to the previous content.
// This fails if the volume gets unmounted at startup.
cID2 := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("cat", "/foo/test.txt"))
defer c.ContainerRemove(ctx, cID2, containertypes.RemoveOptions{Force: true})
poll.WaitOn(t, container.IsStopped(ctx, c, cID2))
inspect, err := c.ContainerInspect(ctx, cID2)
if assert.Check(t, err) {
assert.Check(t, is.Equal(inspect.State.ExitCode, 0), "volume doesn't have the same file")
}
logs, err := c.ContainerLogs(ctx, cID2, containertypes.LogsOptions{ShowStdout: true})
assert.NilError(t, err)
defer logs.Close()
var stdoutBuf bytes.Buffer
_, err = stdcopy.StdCopy(&stdoutBuf, io.Discard, logs)
assert.NilError(t, err)
assert.Check(t, is.Equal(strings.TrimSpace(stdoutBuf.String()), testContent))
})
// Remove that container which should free the references in the volume
err = c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
assert.NilError(t, err)
// Now we should be able to remove the volume
err = c.VolumeRemove(ctx, v.Name, false)
assert.NilError(t, err)
})
// Make sure that we don't panic if the container has bind-mounts
// (which should not be "restored")
// Regression test for https://github.com/moby/moby/issues/45898
t.Run("container with bind-mounts", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
m := mount.Mount{
Type: mount.TypeBind,
Source: os.TempDir(),
Target: "/foo",
}
cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"))
defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
d.Restart(t, "--live-restore", "--iptables=false")
err := c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
assert.NilError(t, err)
})
}
func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) {
skip.If(t, runtime.GOOS == "windows")
ctx := testutil.StartSpan(baseContext, t)
bridgeName := "ext-bridge1"
d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_CREATE_DEFAULT_BRIDGE="+bridgeName))
defer func() {
d.Stop(t)
d.Cleanup(t)
}()
defer func() {
// No need to clean up when running this test in rootless mode, as the
// interface is deleted when the daemon is stopped and the netns
// reclaimed by the kernel.
if !testEnv.IsRootless() {
deleteInterface(t, bridgeName)
}
}()
d.StartWithBusybox(ctx, t, "--bridge", bridgeName, "--fixed-cidr", "192.168.130.0/24")
}
func deleteInterface(t *testing.T, ifName string) {
icmd.RunCommand("ip", "link", "delete", ifName).Assert(t, icmd.Success)
icmd.RunCommand("iptables", "-t", "nat", "--flush").Assert(t, icmd.Success)
icmd.RunCommand("iptables", "--flush").Assert(t, icmd.Success)
}