mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
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>
This commit is contained in:
parent
0f124aba2e
commit
8f80e55111
12 changed files with 148 additions and 34 deletions
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.Unmarshal(b, &jsonConfig); err != nil {
|
||||||
if err := json.NewDecoder(reader).Decode(&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.Unmarshal(b, &config); err != nil {
|
||||||
if err := json.NewDecoder(reader).Decode(&config); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
integration/daemon/daemon_test.go
Normal file
101
integration/daemon/daemon_test.go
Normal file
|
@ -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
integration/daemon/main_test.go
Normal file
10
integration/daemon/main_test.go
Normal file
|
@ -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
integration/daemon/testdata/empty-config-1.json
vendored
Normal file
1
integration/daemon/testdata/empty-config-1.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
1
integration/daemon/testdata/empty-config-2.json
vendored
Normal file
1
integration/daemon/testdata/empty-config-2.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
1
integration/daemon/testdata/invalid-config-1.json
vendored
Normal file
1
integration/daemon/testdata/invalid-config-1.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"unknown-option": true}
|
1
integration/daemon/testdata/malformed-config.json
vendored
Normal file
1
integration/daemon/testdata/malformed-config.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{
|
1
integration/daemon/testdata/valid-config-1.json
vendored
Normal file
1
integration/daemon/testdata/valid-config-1.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"debug": true}
|
|
@ -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 == "" {
|
||||||
|
|
Loading…
Add table
Reference in a new issue