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
Daemon config validation #42393
Conversation
There was a problem hiding this 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
Outdated
|
||
if opts.Validate { | ||
// If config wasn't OK we wouldn't have made it this far. | ||
logrus.Infof("Config OK") |
There was a problem hiding this comment.
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 printConfig 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
moby/integration/config/config_test.go
Lines 382 to 400 in 7b9275c
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 usingos/exec
to start the binary and check stdout/stderr would work.
Failure is unrelated, but .. interesting? edit: opened #42416 for tracking
|
e54bcb8
to
c905a87
Compare
integration/daemon/daemon_test.go
Outdated
validOut := "Config OK" | ||
failedOut := "unable to configure the Docker daemon with file" |
There was a problem hiding this comment.
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";
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" | |
) |
integration/daemon/daemon_test.go
Outdated
out := runCmd([]string{dockerdBinary, "--validate", "--config-file", config}) | ||
|
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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
be26d52
to
19976a0
Compare
Rootless is failing (which we somewhat expected could happen);
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. |
There was a problem hiding this 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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
@AkihiroSuda @cpuguy83 PTAL @aiordache Could you also prepare a pull request in the docker/cli repository to;
|
RS5 failure is unrelated, but posting here in case it becomes an issue;
|
There was a problem hiding this 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
cmd/dockerd/daemon.go
Outdated
|
||
if opts.Validate { | ||
// If config wasn't OK we wouldn't have made it this far. | ||
fmt.Println("Config OK") |
There was a problem hiding this comment.
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
integration/daemon/daemon_test.go
Outdated
testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata") | ||
|
||
const ( | ||
validOut = "Config OK" |
There was a problem hiding this comment.
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)
@samuelkarp or @tonistiigi ptal 🤗 |
daemon/config/config.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 {}
)
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
367683c
to
136dbed
Compare
daemon/config/config.go
Outdated
// 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 |
There was a problem hiding this comment.
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:
// 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)
8c7c9bd
to
d5d3231
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
@tonistiigi PTAL |
integration/daemon/main_test.go
Outdated
fmt.Println(err) | ||
os.Exit(1) | ||
} | ||
err = environment.EnsureFrozenImagesLinux(testEnv) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
02bbfa5
to
e39f972
Compare
Just realised a minor inconsistency:
If no config-file is configured, it will use the default location, but the default is "optional" (as in: Perhaps we should change the message back to the original |
d608765
to
8f80e55
Compare
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>
There was a problem hiding this 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
@tonistiigi PTAL: the test setup was updated ( |
CI failure is unrelated (a windows machine running out of disk space) |
On
dockerd --validate
, return after the daemon config has been loaded and validated.Continuation of #38138, avoids the
os.Exit
.closes #38138
fixes #36911