Browse code

Add configuration validation option and tests.

Fixes #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>

Rich Horwood authored on 2018/11/05 09:52:26
Showing 12 changed files
... ...
@@ -75,14 +75,18 @@ func NewDaemonCli() *DaemonCli {
75 75
 }
76 76
 
77 77
 func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
78
-	stopc := make(chan bool)
79
-	defer close(stopc)
80
-
81 78
 	opts.SetDefaultOptions(opts.flags)
82 79
 
83 80
 	if cli.Config, err = loadDaemonCliConfig(opts); err != nil {
84 81
 		return err
85 82
 	}
83
+
84
+	if opts.Validate {
85
+		// If config wasn't OK we wouldn't have made it this far.
86
+		fmt.Fprintln(os.Stderr, "configuration OK")
87
+		return nil
88
+	}
89
+
86 90
 	warnOnDeprecatedConfigOptions(cli.Config)
87 91
 
88 92
 	if err := configureDaemonLogs(cli.Config); err != nil {
... ...
@@ -178,6 +182,9 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
178 178
 	}
179 179
 	defer cancel()
180 180
 
181
+	stopc := make(chan bool)
182
+	defer close(stopc)
183
+
181 184
 	signal.Trap(func() {
182 185
 		cli.stop()
183 186
 		<-stopc // wait for daemonCli.start() to return
... ...
@@ -41,6 +41,7 @@ type daemonOptions struct {
41 41
 	TLS          bool
42 42
 	TLSVerify    bool
43 43
 	TLSOptions   *tlsconfig.Options
44
+	Validate     bool
44 45
 }
45 46
 
46 47
 // newDaemonOptions returns a new daemonFlags
... ...
@@ -59,6 +60,7 @@ func (o *daemonOptions) InstallFlags(flags *pflag.FlagSet) {
59 59
 	}
60 60
 
61 61
 	flags.BoolVarP(&o.Debug, "debug", "D", false, "Enable debug mode")
62
+	flags.BoolVar(&o.Validate, "validate", false, "Validate configuration file and exit")
62 63
 	flags.StringVarP(&o.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`)
63 64
 	flags.BoolVar(&o.TLS, FlagTLS, DefaultTLSValue, "Use TLS; implied by --tlsverify")
64 65
 	flags.BoolVar(&o.TLSVerify, FlagTLSVerify, dockerTLSVerify || DefaultTLSValue, "Use TLS and verify the remote")
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"bytes"
5 5
 	"encoding/json"
6 6
 	"fmt"
7
-	"io"
8 7
 	"io/ioutil"
9 8
 	"net"
10 9
 	"os"
... ...
@@ -394,11 +393,16 @@ func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Con
394 394
 	}
395 395
 
396 396
 	var config Config
397
-	var reader io.Reader
397
+
398
+	b = bytes.TrimSpace(b)
399
+	if len(b) == 0 {
400
+		// empty config file
401
+		return &config, nil
402
+	}
403
+
398 404
 	if flags != nil {
399 405
 		var jsonConfig map[string]interface{}
400
-		reader = bytes.NewReader(b)
401
-		if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil {
406
+		if err := json.Unmarshal(b, &jsonConfig); err != nil {
402 407
 			return nil, err
403 408
 		}
404 409
 
... ...
@@ -441,8 +445,7 @@ func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Con
441 441
 		config.ValuesSet = configSet
442 442
 	}
443 443
 
444
-	reader = bytes.NewReader(b)
445
-	if err := json.NewDecoder(reader).Decode(&config); err != nil {
444
+	if err := json.Unmarshal(b, &config); err != nil {
446 445
 		return nil, err
447 446
 	}
448 447
 
... ...
@@ -4,8 +4,6 @@ import (
4 4
 	"bytes"
5 5
 	"context"
6 6
 	"encoding/json"
7
-	"io/ioutil"
8
-	"path/filepath"
9 7
 	"sort"
10 8
 	"testing"
11 9
 	"time"
... ...
@@ -17,7 +15,6 @@ import (
17 17
 	"github.com/docker/docker/errdefs"
18 18
 	"github.com/docker/docker/integration/internal/swarm"
19 19
 	"github.com/docker/docker/pkg/stdcopy"
20
-	"github.com/docker/docker/testutil/daemon"
21 20
 	"gotest.tools/v3/assert"
22 21
 	is "gotest.tools/v3/assert/cmp"
23 22
 	"gotest.tools/v3/poll"
... ...
@@ -379,26 +376,6 @@ func TestConfigCreateResolve(t *testing.T) {
379 379
 	assert.Assert(t, is.Equal(0, len(entries)))
380 380
 }
381 381
 
382
-func TestConfigDaemonLibtrustID(t *testing.T) {
383
-	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
384
-	defer setupTest(t)()
385
-
386
-	d := daemon.New(t)
387
-	defer d.Stop(t)
388
-
389
-	trustKey := filepath.Join(d.RootDir(), "key.json")
390
-	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)
391
-	assert.NilError(t, err)
392
-
393
-	config := filepath.Join(d.RootDir(), "daemon.json")
394
-	err = ioutil.WriteFile(config, []byte(`{"deprecated-key-path": "`+trustKey+`"}`), 0644)
395
-	assert.NilError(t, err)
396
-
397
-	d.Start(t, "--config-file", config)
398
-	info := d.Info(t)
399
-	assert.Equal(t, info.ID, "WTJ3:YSIP:CE2E:G6KJ:PSBD:YX2Y:WEYD:M64G:NU2V:XPZV:H2CR:VLUB")
400
-}
401
-
402 382
 func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
403 383
 	buf := bytes.NewBuffer(nil)
404 384
 	_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
405 385
new file mode 100644
... ...
@@ -0,0 +1,101 @@
0
+package daemon // import "github.com/docker/docker/integration/daemon"
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"os/exec"
6
+	"path/filepath"
7
+	"runtime"
8
+	"testing"
9
+
10
+	"github.com/docker/docker/testutil/daemon"
11
+	"gotest.tools/v3/assert"
12
+	"gotest.tools/v3/skip"
13
+
14
+	is "gotest.tools/v3/assert/cmp"
15
+)
16
+
17
+func TestConfigDaemonLibtrustID(t *testing.T) {
18
+	skip.If(t, runtime.GOOS != "linux")
19
+
20
+	d := daemon.New(t)
21
+	defer d.Stop(t)
22
+
23
+	trustKey := filepath.Join(d.RootDir(), "key.json")
24
+	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)
25
+	assert.NilError(t, err)
26
+
27
+	config := filepath.Join(d.RootDir(), "daemon.json")
28
+	err = ioutil.WriteFile(config, []byte(`{"deprecated-key-path": "`+trustKey+`"}`), 0644)
29
+	assert.NilError(t, err)
30
+
31
+	d.Start(t, "--config-file", config)
32
+	info := d.Info(t)
33
+	assert.Equal(t, info.ID, "WTJ3:YSIP:CE2E:G6KJ:PSBD:YX2Y:WEYD:M64G:NU2V:XPZV:H2CR:VLUB")
34
+}
35
+
36
+func TestDaemonConfigValidation(t *testing.T) {
37
+	skip.If(t, runtime.GOOS != "linux")
38
+
39
+	d := daemon.New(t)
40
+	dockerBinary, err := d.BinaryPath()
41
+	assert.NilError(t, err)
42
+	params := []string{"--validate", "--config-file"}
43
+
44
+	dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
45
+	if dest == "" {
46
+		dest = os.Getenv("DEST")
47
+	}
48
+	testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata")
49
+
50
+	const (
51
+		validOut  = "configuration OK"
52
+		failedOut = "unable to configure the Docker daemon with file"
53
+	)
54
+
55
+	tests := []struct {
56
+		name        string
57
+		args        []string
58
+		expectedOut string
59
+	}{
60
+		{
61
+			name:        "config with no content",
62
+			args:        append(params, filepath.Join(testdata, "empty-config-1.json")),
63
+			expectedOut: validOut,
64
+		},
65
+		{
66
+			name:        "config with {}",
67
+			args:        append(params, filepath.Join(testdata, "empty-config-2.json")),
68
+			expectedOut: validOut,
69
+		},
70
+		{
71
+			name:        "invalid config",
72
+			args:        append(params, filepath.Join(testdata, "invalid-config-1.json")),
73
+			expectedOut: failedOut,
74
+		},
75
+		{
76
+			name:        "malformed config",
77
+			args:        append(params, filepath.Join(testdata, "malformed-config.json")),
78
+			expectedOut: failedOut,
79
+		},
80
+		{
81
+			name:        "valid config",
82
+			args:        append(params, filepath.Join(testdata, "valid-config-1.json")),
83
+			expectedOut: validOut,
84
+		},
85
+	}
86
+	for _, tc := range tests {
87
+		tc := tc
88
+		t.Run(tc.name, func(t *testing.T) {
89
+			t.Parallel()
90
+			cmd := exec.Command(dockerBinary, tc.args...)
91
+			out, err := cmd.CombinedOutput()
92
+			assert.Check(t, is.Contains(string(out), tc.expectedOut))
93
+			if tc.expectedOut == failedOut {
94
+				assert.ErrorContains(t, err, "", "expected an error, but got none")
95
+			} else {
96
+				assert.NilError(t, err)
97
+			}
98
+		})
99
+	}
100
+}
0 101
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+package daemon // import "github.com/docker/docker/integration/daemon"
1
+
2
+import (
3
+	"os"
4
+	"testing"
5
+)
6
+
7
+func TestMain(m *testing.M) {
8
+	os.Exit(m.Run())
9
+}
0 10
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{}
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{"unknown-option": true}
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{"debug": true}
... ...
@@ -217,6 +217,15 @@ func New(t testing.TB, ops ...Option) *Daemon {
217 217
 	return d
218 218
 }
219 219
 
220
+// BinaryPath returns the binary and its arguments.
221
+func (d *Daemon) BinaryPath() (string, error) {
222
+	dockerdBinary, err := exec.LookPath(d.dockerdBinary)
223
+	if err != nil {
224
+		return "", errors.Wrapf(err, "[%s] could not find docker binary in $PATH", d.id)
225
+	}
226
+	return dockerdBinary, nil
227
+}
228
+
220 229
 // ContainersNamespace returns the containerd namespace used for containers.
221 230
 func (d *Daemon) ContainersNamespace() string {
222 231
 	return d.id
... ...
@@ -307,9 +316,9 @@ func (d *Daemon) StartWithError(args ...string) error {
307 307
 // StartWithLogFile will start the daemon and attach its streams to a given file.
308 308
 func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error {
309 309
 	d.handleUserns()
310
-	dockerdBinary, err := exec.LookPath(d.dockerdBinary)
310
+	dockerdBinary, err := d.BinaryPath()
311 311
 	if err != nil {
312
-		return errors.Wrapf(err, "[%s] could not find docker binary in $PATH", d.id)
312
+		return err
313 313
 	}
314 314
 
315 315
 	if d.pidFile == "" {