Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Daemon config validation #42393

Merged
merged 1 commit into from Jun 23, 2021
Merged

Daemon config validation #42393

merged 1 commit into from Jun 23, 2021

Conversation

aiordache
Copy link
Contributor

@aiordache aiordache commented May 19, 2021

On dockerd --validate, return after the daemon config has been loaded and validated.

Continuation of #38138, avoids the os.Exit.
closes #38138
fixes #36911

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! left some comments / thoughts

cmd/dockerd/daemon.go Show resolved Hide resolved

if opts.Validate {
// If config wasn't OK we wouldn't have made it this far.
logrus.Infof("Config OK")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mkdir /foo
echo '{' > /foo/malformed-config.json
echo '' > /foo/empty-config-1.json
echo '{}' > /foo/empty-config-2.json
echo '{"unknown-option": true}' > /foo/invalid-config-1.json
echo '{"debug": true}' > /foo/valid-config-1.json

Then trying them;

for file in /foo/*.json; do sh -xc "dockerd --validate --config-file=${file}"; done
+ dockerd --validate --config-file=/foo/empty-config-1.json
unable to configure the Docker daemon with file /foo/empty-config-1.json: EOF
+ dockerd --validate --config-file=/foo/empty-config-2.json
INFO[2021-05-19T10:31:30.747037342Z] Config OK
+ dockerd --validate --config-file=/foo/invalid-config-1.json
unable to configure the Docker daemon with file /foo/invalid-config-1.json: the following directives don't match any configuration option: unknown-option
+ dockerd --validate --config-file=/foo/malformed-config.json
unable to configure the Docker daemon with file /foo/malformed-config.json: unexpected EOF
+ dockerd --validate --config-file=/foo/valid-config-1.json
INFO[2021-05-19T10:31:30.883917472Z] Config OK

Looking at the output:

  • For a follow-up: I think we should ignore the "empty" file. It's probably ok to ignore an empty daemon.json (same as how we ignore empty json ({})
  • Looking at it now; using logrus.Infof() adds a lot of noise; I think we should just print Config OK. This also allows the output to be more easily used in scripts (check if the output is "Config OK"

I think we should also have a simple integration test for this. I see there's a TestConfigDaemonLibtrustID test in

func TestConfigDaemonLibtrustID(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
defer setupTest(t)()
d := daemon.New(t)
defer d.Stop(t)
trustKey := filepath.Join(d.RootDir(), "key.json")
err := ioutil.WriteFile(trustKey, []byte(`{"crv":"P-256","d":"dm28PH4Z4EbyUN8L0bPonAciAQa1QJmmyYd876mnypY","kid":"WTJ3:YSIP:CE2E:G6KJ:PSBD:YX2Y:WEYD:M64G:NU2V:XPZV:H2CR:VLUB","kty":"EC","x":"Mh5-JINSjaa_EZdXDttri255Z5fbCEOTQIZjAcScFTk","y":"eUyuAjfxevb07hCCpvi4Zi334Dy4GDWQvEToGEX4exQ"}`), 0644)
assert.NilError(t, err)
config := filepath.Join(d.RootDir(), "daemon.json")
err = ioutil.WriteFile(config, []byte(`{"deprecated-key-path": "`+trustKey+`"}`), 0644)
assert.NilError(t, err)
d.Start(t, "--config-file", config)
info := d.Info(t)
assert.Equal(t, info.ID, "WTJ3:YSIP:CE2E:G6KJ:PSBD:YX2Y:WEYD:M64G:NU2V:XPZV:H2CR:VLUB")
}

Some notes on that test though;

  • it looks like it's "actually" in the wrong place (that test suite is testing docker config (swarm configs), so perhaps should be moved to a new suite, but we can look at that later
  • the "test utils" we have to start the daemon may be a bit complicated for this (they're mostly to setup a daemon and then interact with the running daemon, but not so much to test the daemon CLI); it would need a bit looking into (but happy to help if needed). This particular would probably have been easier to implement in the old integration-cli tests, but those were marked "deprecated", so we can't add new tests there. So instead, perhaps just using os/exec to start the binary and check stdout/stderr would work.

@thaJeztah
Copy link
Member

thaJeztah commented May 19, 2021

Failure is unrelated, but .. interesting? edit: opened #42416 for tracking

=== RUN   TestDockerSuite/TestRunPIDsLimit
    --- FAIL: TestDockerSuite/TestRunPIDsLimit (0.96s)
        docker_cli_run_unix_test.go:1395: assertion failed: 
            Command:  /usr/local/cli/docker run --name skittles --pids-limit 4 busybox cat /sys/fs/cgroup/pids/pids.max
            ExitCode: 125
            Error:    exit status 125
            Stdout:   
            Stderr:   runtime/cgo: pthread_create failed: Resource temporarily unavailable
            SIGABRT: abort
            PC=0x91c26b m=2 sigcode=18446744073709551610
            
            goroutine 0 [idle]:
            runtime: unknown pc 0x91c26b
            stack: frame={sp:0x7f3c2f3996c0, fp:0x0} stack=[0x7f3c2eb9a148,0x7f3c2f399d48)
            00007f3c2f3995c0:  0000000000000000  0000000000000000 
            00007f3c2f3995d0:  0000000000000000  0000000000000000 
            00007f3c2f3995e0:  0000000000000000  0000000000000000 
            00007f3c2f3995f0:  0000000000000000  0000000000000000 
            00007f3c2f399600:  0000000000000000  0000000000000000 
            00007f3c2f399610:  0000000000000000  0000000000000000 
            00007f3c2f399620:  0000000000000000  0000000000000000 
            00007f3c2f399630:  0000000000000000  0000000000000000 
            00007f3c2f399640:  0000000000000000  0000000000000000 
            00007f3c2f399650:  0000000000000000  0000000000000000 
            00007f3c2f399660:  0000000000000000  000000000042cf30 <runtime.netpoll+320> 
            00007f3c2f399670:  0000000000000007  00007f3c2f3996a8 
            00007f3c2f399680:  0000000000000080  0000000000000000 
            00007f3c2f399690:  0000000000000000  0000000000000000 
            00007f3c2f3996a0:  0000000000000000  0000000000000000 
            00007f3c2f3996b0:  0000000000000000  0000000000000000 
            00007f3c2f3996c0: <0000000000000000  0000000000000000 
            00007f3c2f3996d0:  0000000000000000  0000000000000000 
            00007f3c2f3996e0:  0000000000000000  0000000000000000 
            00007f3c2f3996f0:  0000000000000000  0000000000000000 
            00007f3c2f399700:  0000000000000000  0000000000000000 
            00007f3c2f399710:  0000000000000000  0000000000000000 
            00007f3c2f399720:  0000000000000000  0000000000000000 
            00007f3c2f399730:  0000000000000000  0000000000000000 
            00007f3c2f399740:  fffffffe7fffffff  ffffffffffffffff 
            00007f3c2f399750:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399760:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399770:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399780:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399790:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f3997a0:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f3997b0:  ffffffffffffffff  ffffffffffffffff 
            runtime: unknown pc 0x91c26b
            stack: frame={sp:0x7f3c2f3996c0, fp:0x0} stack=[0x7f3c2eb9a148,0x7f3c2f399d48)
            00007f3c2f3995c0:  0000000000000000  0000000000000000 
            00007f3c2f3995d0:  0000000000000000  0000000000000000 
            00007f3c2f3995e0:  0000000000000000  0000000000000000 
            00007f3c2f3995f0:  0000000000000000  0000000000000000 
            00007f3c2f399600:  0000000000000000  0000000000000000 
            00007f3c2f399610:  0000000000000000  0000000000000000 
            00007f3c2f399620:  0000000000000000  0000000000000000 
            00007f3c2f399630:  0000000000000000  0000000000000000 
            00007f3c2f399640:  0000000000000000  0000000000000000 
            00007f3c2f399650:  0000000000000000  0000000000000000 
            00007f3c2f399660:  0000000000000000  000000000042cf30 <runtime.netpoll+320> 
            00007f3c2f399670:  0000000000000007  00007f3c2f3996a8 
            00007f3c2f399680:  0000000000000080  0000000000000000 
            00007f3c2f399690:  0000000000000000  0000000000000000 
            00007f3c2f3996a0:  0000000000000000  0000000000000000 
            00007f3c2f3996b0:  0000000000000000  0000000000000000 
            00007f3c2f3996c0: <0000000000000000  0000000000000000 
            00007f3c2f3996d0:  0000000000000000  0000000000000000 
            00007f3c2f3996e0:  0000000000000000  0000000000000000 
            00007f3c2f3996f0:  0000000000000000  0000000000000000 
            00007f3c2f399700:  0000000000000000  0000000000000000 
            00007f3c2f399710:  0000000000000000  0000000000000000 
            00007f3c2f399720:  0000000000000000  0000000000000000 
            00007f3c2f399730:  0000000000000000  0000000000000000 
            00007f3c2f399740:  fffffffe7fffffff  ffffffffffffffff 
            00007f3c2f399750:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399760:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399770:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399780:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f399790:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f3997a0:  ffffffffffffffff  ffffffffffffffff 
            00007f3c2f3997b0:  ffffffffffffffff  ffffffffffffffff 
            
            goroutine 1 [runnable, locked to thread]:
            syscall.Syscall6(0x101, 0xffffffffffffff9c, 0xc0001edce0, 0x80001, 0x0, 0x0, 0x0, 0x3, 0x80001, 0x0)
            	syscall/asm_linux_amd64.s:44 +0x5
            github.com/opencontainers/runc/vendor/golang.org/x/sys/unix.openat(0xffffffffffffff9c, 0xc00019b0d8, 0xf, 0x80001, 0x0, 0xc00019b0d8, 0xf, 0xa01dc0)
            	github.com/opencontainers/runc/vendor/golang.org/x/sys/unix/zsyscall_linux.go:77 +0xbb
            github.com/opencontainers/runc/vendor/golang.org/x/sys/unix.Open(...)
            	github.com/opencontainers/runc/vendor/golang.org/x/sys/unix/syscall_linux.go:90
            github.com/opencontainers/runc/libcontainer.(*linuxStandardInit).Init(0xc000158f90, 0x0, 0x0)
            	github.com/opencontainers/runc/libcontainer/standard_init_linux.go:197 +0x7b6
            github.com/opencontainers/runc/libcontainer.(*LinuxFactory).StartInitialization(0xc000102120, 0x0, 0x0)
            	github.com/opencontainers/runc/libcontainer/factory_linux.go:402 +0x4e7
            main.glob..func6(0xc0000c09a0, 0x0, 0xc000050870)
            	github.com/opencontainers/runc/init.go:49 +0x4b
            github.com/opencontainers/runc/vendor/github.com/urfave/cli.HandleAction(0xa11300, 0xb095a0, 0xc0000c09a0, 0xc0000c09a0, 0x0)
            	github.com/opencontainers/runc/vendor/github.com/urfave/cli/app.go:523 +0xbe
            github.com/opencontainers/runc/vendor/github.com/urfave/cli.Command.Run(0xae22bf, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb02c62, 0x51, 0x0, ...)
            	github.com/opencontainers/runc/vendor/github.com/urfave/cli/command.go:174 +0x51c
            github.com/opencontainers/runc/vendor/github.com/urfave/cli.(*App).Run(0xc0000d4540, 0xc000090020, 0x2, 0x2, 0x0, 0x0)
            	github.com/opencontainers/runc/vendor/github.com/urfave/cli/app.go:276 +0x718
            main.main()
            	github.com/opencontainers/runc/main.go:165 +0xbb1
            
            goroutine 20 [syscall]:
            os/signal.signal_recv(0x0)
            	runtime/sigqueue.go:147 +0x9c
            os/signal.loop()
            	os/signal/signal_unix.go:23 +0x22
            created by os/signal.init.0
            	os/signal/signal_unix.go:29 +0x41
            
            rax    0x0
            rbx    0x6
            rcx    0x91c26b
            rdx    0x0
            rdi    0x2
            rsi    0x7f3c2f3996c0
            rbp    0xbd79da
            rsp    0x7f3c2f3996c0
            r8     0x0
            r9     0x7f3c2f3996c0
            r10    0x8
            r11    0x246
            r12    0x1da9d20
            r13    0x0
            r14    0xb95690
            r15    0x0
            rip    0x91c26b
            rflags 0x246
            cs     0x33
            fs     0x0
            gs     0x0
            /usr/local/cli/docker: Error response from daemon: OCI runtime start failed: cannot start an already running container: unknown.
            time="2021-05-19T10:48:19Z" level=error msg="error waiting for container: context canceled" 
            
            
            Failures:
            ExitCode was 125 expected 0
            Expected no error

@aiordache aiordache force-pushed the daemon_config branch 2 times, most recently from e54bcb8 to c905a87 Compare May 20, 2021 15:59
Comment on lines 58 to 65
validOut := "Config OK"
failedOut := "unable to configure the Docker daemon with file"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could make these cons to be explicit they're "immutable";

Suggested change
validOut := "Config OK"
failedOut := "unable to configure the Docker daemon with file"
const (
validOut = "Config OK"
failedOut = "unable to configure the Docker daemon with file"
)

Comment on lines 71 to 78
out := runCmd([]string{dockerdBinary, "--validate", "--config-file", config})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could use CombinedOutput(), which both executes the command, and collects the output;

cmd := exec.Command(dockerdBinary, "--validate", "--config-file", config)
out, err := cmd.CombinedOutput()

However, it would be useful to verify that in the "fail" case, it exists with an error code, and in the "non-fail" case exits with a "zero" exit status; perhaps something like;

if expectedOut == failedOut {
	assert.ErrorContains(t, err, "", "expected an error, but got none")
} else {
	assert.NilError(t, err)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For better debugging which of the tests failed, you could use sub-tests here; basically, that's wrapping everything inside the loop in a t.Run(name, func(){});

	for filename, expectedOut := range tests {
		t.Run(filename, func(t *testing.T) {
			config := filepath.Join("testdata", filename)
			cmd := exec.Command(dockerdBinary, "--validate", "--config-file", config)
			out, err := cmd.CombinedOutput()
			assert.Check(t, is.Contains(string(out), expectedOut))
			if expectedOut == failedOut {
				assert.ErrorContains(t, err, "", "expected an error, but got none")
			} else {
				assert.NilError(t, err)
			}
		})
	}

I just checked out the branch to test that (with my other suggestions), and; with that;

make DOCKER_GRAPHDRIVER=vfs BIND_DIR=. TESTDIRS='github.com/docker/docker/integration/daemon' TESTFLAGS='-test.run ^TestDaemonConfigValidation$' test-integration
Running /go/src/github.com/docker/docker/integration/daemon (amd64.integration.daemon) flags=-test.v -test.timeout=5m -test.run ^TestDaemonConfigValidation
INFO: Testing against a local daemon
=== RUN   TestDaemonConfigValidation
=== RUN   TestDaemonConfigValidation/valid-config-1.json
=== RUN   TestDaemonConfigValidation/empty-config-1.json
=== RUN   TestDaemonConfigValidation/empty-config-2.json
=== RUN   TestDaemonConfigValidation/invalid-config-1.json
=== RUN   TestDaemonConfigValidation/malformed-config.json
--- PASS: TestDaemonConfigValidation (0.40s)
    --- PASS: TestDaemonConfigValidation/valid-config-1.json (0.08s)
    --- PASS: TestDaemonConfigValidation/empty-config-1.json (0.07s)
    --- PASS: TestDaemonConfigValidation/empty-config-2.json (0.08s)
    --- PASS: TestDaemonConfigValidation/invalid-config-1.json (0.07s)
    --- PASS: TestDaemonConfigValidation/malformed-config.json (0.07s)
PASS

@aiordache aiordache force-pushed the daemon_config branch 3 times, most recently from be26d52 to 19976a0 Compare May 20, 2021 17:55
@thaJeztah
Copy link
Member

Rootless is failing (which we somewhat expected could happen);

--- FAIL: TestDaemonConfigValidation/malformed_config (0.23s)
    daemon_test.go:94: assertion failed: string "+ [ -w /go/src/github.com/docker/docker/bundles/test-integration/TestDaemonConfigValidation/d0ceeee61c91a/xdgrun ]\n+ [ -d /home/unprivilegeduser ]\n+ rootlesskit=\n+ command -v docker-rootlesskit\n+ command -v rootlesskit\n+ rootlesskit=rootlesskit\n+ break\n+ [ -z rootlesskit ]\n+ : \n+ : \n+ : builtin\n+ : auto\n+ : auto\n+ net=\n+ mtu=\n+ [ -z ]\n+ command -v slirp4netns\n+ [ -z ]\n+ command -v vpnkit\n+ net=vpnkit\n+ [ -z ]\n+ mtu=1500\n+ [ -z ]\n+ _DOCKERD_ROOTLESS_CHILD=1\n+ export _DOCKERD_ROOTLESS_CHILD\n+ id -u\n+ [ 1000 = 0 ]\n+ exec rootlesskit --net=vpnkit --mtu=1500 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave /usr/local/bin/dockerd-rootless.sh --validate --config-file /go/src/github.com/docker/docker/integration/daemon/testdata/valid-config-1.json\ntime=\"2021-05-20T18:08:14Z\" level=warning msg=\"The host root filesystem is mounted as \\\"master:177\\\". Setting child propagation to \\\"rslave\\\" is not supported.\"\n+ [ -w /go/src/github.com/docker/docker/bundles/test-integration/TestDaemonConfigValidation/d0ceeee61c91a/xdgrun ]\n+ [ -d /home/unprivilegeduser ]\n+ rootlesskit=\n+ command -v docker-rootlesskit\n+ command -v rootlesskit\n+ rootlesskit=rootlesskit\n+ break\n+ [ -z rootlesskit ]\n+ : \n+ : \n+ : builtin\n+ : auto\n+ : auto\n+ net=\n+ mtu=\n+ [ -z ]\n+ command -v slirp4netns\n+ [ -z ]\n+ command -v vpnkit\n+ net=vpnkit\n+ [ -z ]\n+ mtu=1500\n+ [ -z 1 ]\n+ [ 1 = 1 ]\n+ rm -f /run/docker /run/containerd /run/xtables.lock\n+ exec dockerd --validate --config-file /go/src/github.com/docker/docker/integration/daemon/testdata/valid-config-1.json\nConfig OK\n" does not contain "unable to configure the Docker daemon with file"
    daemon_test.go:96: assertion failed: expected an error, got nil: expected an error, but got none

I think it's ok to skip the test on rootless, because it's not testing anything that's specific to rootless / non-rootless, so we'd have enough coverage for the other situations.

daemon/config/config.go Outdated Show resolved Hide resolved
daemon/config/config.go Outdated Show resolved Hide resolved
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes LGTM, but can you squash the commits?

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@thaJeztah
Copy link
Member

RS5 failure is unrelated, but posting here in case it becomes an issue;

verifying gotest.tools/gotestsum@v0.5.3: gotest.tools/gotestsum@v0.5.3: Get https://sum.golang.org/lookup/gotest.tools/gotestsum@v0.5.3: x509: certificate signed by unknown authority
[2021-05-25T11:15:57.762Z] gotestsum build failed...
[2021-05-25T11:15:57.762Z] At line:1 char:427

Copy link

@ebriney ebriney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code is clear and it do the job for me.
LGTM


if opts.Validate {
// If config wasn't OK we wouldn't have made it this far.
fmt.Println("Config OK")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussing with @tonistiigi @tianon and (for comparison), NGINX prints this on stderr

nginx -t > /dev/null 
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata")

const (
validOut = "Config OK"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is now out of date:

[2021-06-04T18:45:32.940Z] --- FAIL: TestDaemonConfigValidation (0.01s)
[2021-06-04T18:45:32.940Z]     --- PASS: TestDaemonConfigValidation/malformed_config (0.11s)
[2021-06-04T18:45:32.940Z]     --- FAIL: TestDaemonConfigValidation/config_with_no_content (0.11s)
[2021-06-04T18:45:32.940Z]         daemon_test.go:96: assertion failed: string "the configuration file /go/src/github.com/docker/docker/integration/daemon/testdata/empty-config-1.json is valid\n" does not contain "Config OK"
[2021-06-04T18:45:32.940Z]     --- PASS: TestDaemonConfigValidation/invalid_config (0.08s)
[2021-06-04T18:45:32.940Z]     --- FAIL: TestDaemonConfigValidation/config_with_{} (0.10s)
[2021-06-04T18:45:32.940Z]         daemon_test.go:96: assertion failed: string "the configuration file /go/src/github.com/docker/docker/integration/daemon/testdata/empty-config-2.json is valid\n" does not contain "Config OK"
[2021-06-04T18:45:32.940Z]     --- FAIL: TestDaemonConfigValidation/valid_config (0.06s)
[2021-06-04T18:45:32.940Z]         daemon_test.go:96: assertion failed: string "the configuration file /go/src/github.com/docker/docker/integration/daemon/testdata/valid-config-1.json is valid\n" does not contain "Config OK"
[2021-06-04T18:45:32.940Z] FAIL
[2021-06-04T18:45:32.940Z] 
[2021-06-04T18:45:32.940Z] === Failed
[2021-06-04T18:45:32.940Z] === FAIL: amd64.integration.daemon TestDaemonConfigValidation/config_with_no_content (0.11s)
[2021-06-04T18:45:32.940Z]     --- FAIL: TestDaemonConfigValidation/config_with_no_content (0.11s)
[2021-06-04T18:45:32.940Z]         daemon_test.go:96: assertion failed: string "the configuration file /go/src/github.com/docker/docker/integration/daemon/testdata/empty-config-1.json is valid\n" does not contain "Config OK"
[2021-06-04T18:45:32.940Z] 
[2021-06-04T18:45:32.940Z] === FAIL: amd64.integration.daemon TestDaemonConfigValidation/config_with_{} (0.10s)
[2021-06-04T18:45:32.940Z]     --- FAIL: TestDaemonConfigValidation/config_with_{} (0.10s)
[2021-06-04T18:45:32.940Z]         daemon_test.go:96: assertion failed: string "the configuration file /go/src/github.com/docker/docker/integration/daemon/testdata/empty-config-2.json is valid\n" does not contain "Config OK"
[2021-06-04T18:45:32.940Z] 
[2021-06-04T18:45:32.940Z] === FAIL: amd64.integration.daemon TestDaemonConfigValidation/valid_config (0.06s)
[2021-06-04T18:45:32.940Z]     --- FAIL: TestDaemonConfigValidation/valid_config (0.06s)
[2021-06-04T18:45:32.940Z]         daemon_test.go:96: assertion failed: string "the configuration file /go/src/github.com/docker/docker/integration/daemon/testdata/valid-config-1.json is valid\n" does not contain "Config OK"
[2021-06-04T18:45:32.940Z] 
[2021-06-04T18:45:32.940Z] === FAIL: amd64.integration.daemon TestDaemonConfigValidation (0.01s)

@thaJeztah
Copy link
Member

@samuelkarp or @tonistiigi ptal 🤗

@@ -398,7 +398,7 @@ func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Con
if flags != nil {
var jsonConfig map[string]interface{}
reader = bytes.NewReader(b)
if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil {
if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil && err != io.EOF {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what change caused this? Also, this ignores the errors after first json block. As the data is already in byte slice then Unmarshal is cleaner.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was added to allow for an empty daemon.json file (empty file, not empty {})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is a good idea. But if added should be an explicit empty check then. Tbh I'm surprised this returns io.EOF and not ErrUnexpectedEOF.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would return ErrUnexpectedEOF if the JSON is incomplete ({), and EOF if the file is empty (or only whitespace);

https://play.golang.org/p/1_N1QidcM7T

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
)

func main() {
	var reader io.Reader
	var jsonConfig map[string]interface{}

	for _, config := range []string{"{", "[", `""`, "", "     ", `[]`, `{}`} {
		reader = bytes.NewReader([]byte(config))

		if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil {
			fmt.Println(err)
			continue
		}
		fmt.Println(config, "is ok")

	}
}

Prints:

unexpected EOF
unexpected EOF
json: cannot unmarshal string into Go value of type map[string]interface {}
EOF
EOF
json: cannot unmarshal array into Go value of type map[string]interface {}
{} is ok

Not sure if this is a good idea

I thought slightly relaxing would be ok; we already did allow an empty {}; considering an empty file to be the equivalent (config with nothing set).

Do you have specific concerns?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, idk why empty and spaces are considered as valid json for Decode. JSON.parse in js in unexpectedeof for both cases.

I'm not strictly against allowing empty file, if detection for it is clear in code. It might lead to error going unnoticed though. If Desktop fills this file from a field I do think correct behavior would be to not create a file if the field is empty, not to create an empty file.

@aiordache aiordache force-pushed the daemon_config branch 3 times, most recently from 367683c to 136dbed Compare June 14, 2021 11:05
Comment on lines 394 to 401
// trim leading and trailing spaces
b = bytes.TrimSpace(b)
// accept empty config files, set content to {}
if len(b) == 0 {
b = []byte("{}")
}

var config Config
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to do the reverse here; not convert "empty" to {}, but check if it's empty, and if so, return early:

Suggested change
// trim leading and trailing spaces
b = bytes.TrimSpace(b)
// accept empty config files, set content to {}
if len(b) == 0 {
b = []byte("{}")
}
var config Config
var config Config
b = bytes.TrimSpace(b)
if len(b) == 0 {
// empty config file
return &config, nil
}

Looks like all the code below should only be for loading the JSON file, and for validating if there's conflicts, so we wouldn't have to run that code afaics (yeah, this code and logic needs a rewrite, as it's quite messy)

@aiordache aiordache force-pushed the daemon_config branch 2 times, most recently from 8c7c9bd to d5d3231 Compare June 17, 2021 17:52
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@thaJeztah
Copy link
Member

@tonistiigi PTAL

fmt.Println(err)
os.Exit(1)
}
err = environment.EnsureFrozenImagesLinux(testEnv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these tests use frozen images? Or any of this environment/protect calls actually. The added tests do not seem to use shared daemon at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, looks like we should be able to remove for now as the current tests don't use the shared daemon.

w.r.t. the testEnv, perhaps we can just skip on ! linux instead.

@aiordache could you have a look at removing the unneeded setup code?

@aiordache aiordache force-pushed the daemon_config branch 4 times, most recently from 02bbfa5 to e39f972 Compare June 23, 2021 07:54
@thaJeztah
Copy link
Member

Just realised a minor inconsistency:

dockerd --validate
the configuration file /etc/docker/daemon.json is valid
dockerd --validate --config-file=/etc/docker/daemon.json
unable to configure the Docker daemon with file /etc/docker/daemon.json: open /etc/docker/daemon.json: no such file or directory

If no config-file is configured, it will use the default location, but the default is "optional" (as in: daemon.json doesn't have to exist), however, now the message mentions that /etc/docker/daemon.json is OK, even though it doesn't exist, which may be confusing.

Perhaps we should change the message back to the original Config OK or configuration OK. This makes it a bit more "generic" (also for situations where, e.g. a --flag is invalid or conflicts (but the configuration file is "technically" OK).

@aiordache aiordache force-pushed the daemon_config branch 2 times, most recently from d608765 to 8f80e55 Compare June 23, 2021 08:53
Fixes moby#36911

If config file is invalid we'll exit anyhow, so this just prevents
the daemon from starting if the configuration is fine.

Mainly useful for making config changes and restarting the daemon
iff the config is valid.

Signed-off-by: Rich Horwood <rjhorwood@apple.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Anca Iordache <anca.iordache@docker.com>
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

failure is unrelated (TestBuildWCOWSandboxSize) and a known flaky

@thaJeztah
Copy link
Member

@tonistiigi PTAL: the test setup was updated (testEnv things were removed)

@thaJeztah
Copy link
Member

CI failure is unrelated (a windows machine running out of disk space)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dockerd needs a 'validate config' option
5 participants