Bläddra i källkod

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 6 år sedan
förälder
incheckning
8f80e55111

+ 10 - 3
cmd/dockerd/daemon.go

@@ -75,14 +75,18 @@ func NewDaemonCli() *DaemonCli {
 }
 }
 
 
 func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
 func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
-	stopc := make(chan bool)
-	defer close(stopc)
-
 	opts.SetDefaultOptions(opts.flags)
 	opts.SetDefaultOptions(opts.flags)
 
 
 	if cli.Config, err = loadDaemonCliConfig(opts); err != nil {
 	if cli.Config, err = loadDaemonCliConfig(opts); err != nil {
 		return err
 		return err
 	}
 	}
+
+	if opts.Validate {
+		// If config wasn't OK we wouldn't have made it this far.
+		fmt.Fprintln(os.Stderr, "configuration OK")
+		return nil
+	}
+
 	warnOnDeprecatedConfigOptions(cli.Config)
 	warnOnDeprecatedConfigOptions(cli.Config)
 
 
 	if err := configureDaemonLogs(cli.Config); err != nil {
 	if err := configureDaemonLogs(cli.Config); err != nil {
@@ -178,6 +182,9 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
 	}
 	}
 	defer cancel()
 	defer cancel()
 
 
+	stopc := make(chan bool)
+	defer close(stopc)
+
 	signal.Trap(func() {
 	signal.Trap(func() {
 		cli.stop()
 		cli.stop()
 		<-stopc // wait for daemonCli.start() to return
 		<-stopc // wait for daemonCli.start() to return

+ 2 - 0
cmd/dockerd/options.go

@@ -41,6 +41,7 @@ type daemonOptions struct {
 	TLS          bool
 	TLS          bool
 	TLSVerify    bool
 	TLSVerify    bool
 	TLSOptions   *tlsconfig.Options
 	TLSOptions   *tlsconfig.Options
+	Validate     bool
 }
 }
 
 
 // newDaemonOptions returns a new daemonFlags
 // newDaemonOptions returns a new daemonFlags
@@ -59,6 +60,7 @@ func (o *daemonOptions) InstallFlags(flags *pflag.FlagSet) {
 	}
 	}
 
 
 	flags.BoolVarP(&o.Debug, "debug", "D", false, "Enable debug mode")
 	flags.BoolVarP(&o.Debug, "debug", "D", false, "Enable debug mode")
+	flags.BoolVar(&o.Validate, "validate", false, "Validate configuration file and exit")
 	flags.StringVarP(&o.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`)
 	flags.StringVarP(&o.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`)
 	flags.BoolVar(&o.TLS, FlagTLS, DefaultTLSValue, "Use TLS; implied by --tlsverify")
 	flags.BoolVar(&o.TLS, FlagTLS, DefaultTLSValue, "Use TLS; implied by --tlsverify")
 	flags.BoolVar(&o.TLSVerify, FlagTLSVerify, dockerTLSVerify || DefaultTLSValue, "Use TLS and verify the remote")
 	flags.BoolVar(&o.TLSVerify, FlagTLSVerify, dockerTLSVerify || DefaultTLSValue, "Use TLS and verify the remote")

+ 9 - 6
daemon/config/config.go

@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"bytes"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
 	"os"
 	"os"
@@ -394,11 +393,16 @@ func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Con
 	}
 	}
 
 
 	var config Config
 	var config Config
-	var reader io.Reader
+
+	b = bytes.TrimSpace(b)
+	if len(b) == 0 {
+		// empty config file
+		return &config, nil
+	}
+
 	if flags != nil {
 	if flags != nil {
 		var jsonConfig map[string]interface{}
 		var jsonConfig map[string]interface{}
-		reader = bytes.NewReader(b)
-		if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil {
+		if err := json.Unmarshal(b, &jsonConfig); err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
@@ -441,8 +445,7 @@ func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Con
 		config.ValuesSet = configSet
 		config.ValuesSet = configSet
 	}
 	}
 
 
-	reader = bytes.NewReader(b)
-	if err := json.NewDecoder(reader).Decode(&config); err != nil {
+	if err := json.Unmarshal(b, &config); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 

+ 0 - 23
integration/config/config_test.go

@@ -4,8 +4,6 @@ import (
 	"bytes"
 	"bytes"
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
-	"io/ioutil"
-	"path/filepath"
 	"sort"
 	"sort"
 	"testing"
 	"testing"
 	"time"
 	"time"
@@ -17,7 +15,6 @@ import (
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/integration/internal/swarm"
 	"github.com/docker/docker/integration/internal/swarm"
 	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/docker/docker/pkg/stdcopy"
-	"github.com/docker/docker/testutil/daemon"
 	"gotest.tools/v3/assert"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
 	is "gotest.tools/v3/assert/cmp"
 	"gotest.tools/v3/poll"
 	"gotest.tools/v3/poll"
@@ -379,26 +376,6 @@ func TestConfigCreateResolve(t *testing.T) {
 	assert.Assert(t, is.Equal(0, len(entries)))
 	assert.Assert(t, is.Equal(0, len(entries)))
 }
 }
 
 
-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")
-}
-
 func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
 func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
 	buf := bytes.NewBuffer(nil)
 	buf := bytes.NewBuffer(nil)
 	_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
 	_, err := stdcopy.StdCopy(buf, buf, attach.Reader)

+ 101 - 0
integration/daemon/daemon_test.go

@@ -0,0 +1,101 @@
+package daemon // import "github.com/docker/docker/integration/daemon"
+
+import (
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"github.com/docker/docker/testutil/daemon"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/skip"
+
+	is "gotest.tools/v3/assert/cmp"
+)
+
+func TestConfigDaemonLibtrustID(t *testing.T) {
+	skip.If(t, runtime.GOOS != "linux")
+
+	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")
+}
+
+func TestDaemonConfigValidation(t *testing.T) {
+	skip.If(t, runtime.GOOS != "linux")
+
+	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()
+			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)
+			}
+		})
+	}
+}

+ 10 - 0
integration/daemon/main_test.go

@@ -0,0 +1,10 @@
+package daemon // import "github.com/docker/docker/integration/daemon"
+
+import (
+	"os"
+	"testing"
+)
+
+func TestMain(m *testing.M) {
+	os.Exit(m.Run())
+}

+ 1 - 0
integration/daemon/testdata/empty-config-1.json

@@ -0,0 +1 @@
+

+ 1 - 0
integration/daemon/testdata/empty-config-2.json

@@ -0,0 +1 @@
+{}

+ 1 - 0
integration/daemon/testdata/invalid-config-1.json

@@ -0,0 +1 @@
+{"unknown-option": true}

+ 1 - 0
integration/daemon/testdata/malformed-config.json

@@ -0,0 +1 @@
+{

+ 1 - 0
integration/daemon/testdata/valid-config-1.json

@@ -0,0 +1 @@
+{"debug": true}

+ 11 - 2
testutil/daemon/daemon.go

@@ -217,6 +217,15 @@ func New(t testing.TB, ops ...Option) *Daemon {
 	return d
 	return d
 }
 }
 
 
+// BinaryPath returns the binary and its arguments.
+func (d *Daemon) BinaryPath() (string, error) {
+	dockerdBinary, err := exec.LookPath(d.dockerdBinary)
+	if err != nil {
+		return "", errors.Wrapf(err, "[%s] could not find docker binary in $PATH", d.id)
+	}
+	return dockerdBinary, nil
+}
+
 // ContainersNamespace returns the containerd namespace used for containers.
 // ContainersNamespace returns the containerd namespace used for containers.
 func (d *Daemon) ContainersNamespace() string {
 func (d *Daemon) ContainersNamespace() string {
 	return d.id
 	return d.id
@@ -307,9 +316,9 @@ func (d *Daemon) StartWithError(args ...string) error {
 // StartWithLogFile will start the daemon and attach its streams to a given file.
 // StartWithLogFile will start the daemon and attach its streams to a given file.
 func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error {
 func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error {
 	d.handleUserns()
 	d.handleUserns()
-	dockerdBinary, err := exec.LookPath(d.dockerdBinary)
+	dockerdBinary, err := d.BinaryPath()
 	if err != nil {
 	if err != nil {
-		return errors.Wrapf(err, "[%s] could not find docker binary in $PATH", d.id)
+		return err
 	}
 	}
 
 
 	if d.pidFile == "" {
 	if d.pidFile == "" {