Kaynağa Gözat

Verify that the configuration keys in the file are valid.

- Return an error if any of the keys don't match valid flags.
- Fix an issue ignoring merged values as named values.
- Fix tlsverify configuration key.
- Fix bug in mflag to avoid panics when one of the flag set doesn't have any flag.

Signed-off-by: David Calavera <david.calavera@gmail.com>
David Calavera 9 yıl önce
ebeveyn
işleme
ed4038676f

+ 32 - 5
daemon/config.go

@@ -80,7 +80,7 @@ type CommonConfig struct {
 	Hosts      []string         `json:"hosts,omitempty"`
 	Hosts      []string         `json:"hosts,omitempty"`
 	LogLevel   string           `json:"log-level,omitempty"`
 	LogLevel   string           `json:"log-level,omitempty"`
 	TLS        bool             `json:"tls,omitempty"`
 	TLS        bool             `json:"tls,omitempty"`
-	TLSVerify  bool             `json:"tls-verify,omitempty"`
+	TLSVerify  bool             `json:"tlsverify,omitempty"`
 	TLSOptions CommonTLSOptions `json:"tls-opts,omitempty"`
 	TLSOptions CommonTLSOptions `json:"tls-opts,omitempty"`
 
 
 	reloadLock sync.Mutex
 	reloadLock sync.Mutex
@@ -215,16 +215,43 @@ func configValuesSet(config map[string]interface{}) map[string]interface{} {
 }
 }
 
 
 // findConfigurationConflicts iterates over the provided flags searching for
 // findConfigurationConflicts iterates over the provided flags searching for
-// duplicated configurations. It returns an error with all the conflicts if
+// duplicated configurations and unknown keys. It returns an error with all the conflicts if
 // it finds any.
 // it finds any.
 func findConfigurationConflicts(config map[string]interface{}, flags *flag.FlagSet) error {
 func findConfigurationConflicts(config map[string]interface{}, flags *flag.FlagSet) error {
-	var conflicts []string
+	// 1. Search keys from the file that we don't recognize as flags.
+	unknownKeys := make(map[string]interface{})
+	for key, value := range config {
+		flagName := "-" + key
+		if flag := flags.Lookup(flagName); flag == nil {
+			unknownKeys[key] = value
+		}
+	}
+
+	// 2. Discard keys that might have a given name, like `labels`.
+	unknownNamedConflicts := func(f *flag.Flag) {
+		if namedOption, ok := f.Value.(opts.NamedOption); ok {
+			if _, valid := unknownKeys[namedOption.Name()]; valid {
+				delete(unknownKeys, namedOption.Name())
+			}
+		}
+	}
+	flags.VisitAll(unknownNamedConflicts)
+
+	if len(unknownKeys) > 0 {
+		var unknown []string
+		for key := range unknownKeys {
+			unknown = append(unknown, key)
+		}
+		return fmt.Errorf("the following directives don't match any configuration option: %s", strings.Join(unknown, ", "))
+	}
 
 
+	var conflicts []string
 	printConflict := func(name string, flagValue, fileValue interface{}) string {
 	printConflict := func(name string, flagValue, fileValue interface{}) string {
 		return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue)
 		return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue)
 	}
 	}
 
 
-	collectConflicts := func(f *flag.Flag) {
+	// 3. Search keys that are present as a flag and as a file option.
+	duplicatedConflicts := func(f *flag.Flag) {
 		// search option name in the json configuration payload if the value is a named option
 		// search option name in the json configuration payload if the value is a named option
 		if namedOption, ok := f.Value.(opts.NamedOption); ok {
 		if namedOption, ok := f.Value.(opts.NamedOption); ok {
 			if optsValue, ok := config[namedOption.Name()]; ok {
 			if optsValue, ok := config[namedOption.Name()]; ok {
@@ -243,7 +270,7 @@ func findConfigurationConflicts(config map[string]interface{}, flags *flag.FlagS
 		}
 		}
 	}
 	}
 
 
-	flags.Visit(collectConflicts)
+	flags.Visit(duplicatedConflicts)
 
 
 	if len(conflicts) > 0 {
 	if len(conflicts) > 0 {
 		return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", "))
 		return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", "))

+ 42 - 9
daemon/config_test.go

@@ -89,21 +89,16 @@ func TestFindConfigurationConflicts(t *testing.T) {
 	config := map[string]interface{}{"authorization-plugins": "foobar"}
 	config := map[string]interface{}{"authorization-plugins": "foobar"}
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
 
 
-	err := findConfigurationConflicts(config, flags)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	flags.String([]string{"authorization-plugins"}, "", "")
-	if err := flags.Set("authorization-plugins", "asdf"); err != nil {
+	flags.String([]string{"-authorization-plugins"}, "", "")
+	if err := flags.Set("-authorization-plugins", "asdf"); err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 
 
-	err = findConfigurationConflicts(config, flags)
+	err := findConfigurationConflicts(config, flags)
 	if err == nil {
 	if err == nil {
 		t.Fatal("expected error, got nil")
 		t.Fatal("expected error, got nil")
 	}
 	}
-	if !strings.Contains(err.Error(), "authorization-plugins") {
+	if !strings.Contains(err.Error(), "authorization-plugins: (from flag: asdf, from file: foobar)") {
 		t.Fatalf("expected authorization-plugins conflict, got %v", err)
 		t.Fatalf("expected authorization-plugins conflict, got %v", err)
 	}
 	}
 }
 }
@@ -175,3 +170,41 @@ func TestDaemonConfigurationMergeConflictsWithInnerStructs(t *testing.T) {
 		t.Fatalf("expected tlscacert conflict, got %v", err)
 		t.Fatalf("expected tlscacert conflict, got %v", err)
 	}
 	}
 }
 }
+
+func TestFindConfigurationConflictsWithUnknownKeys(t *testing.T) {
+	config := map[string]interface{}{"tls-verify": "true"}
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
+
+	flags.Bool([]string{"-tlsverify"}, false, "")
+	err := findConfigurationConflicts(config, flags)
+	if err == nil {
+		t.Fatal("expected error, got nil")
+	}
+	if !strings.Contains(err.Error(), "the following directives don't match any configuration option: tls-verify") {
+		t.Fatalf("expected tls-verify conflict, got %v", err)
+	}
+}
+
+func TestFindConfigurationConflictsWithMergedValues(t *testing.T) {
+	var hosts []string
+	config := map[string]interface{}{"hosts": "tcp://127.0.0.1:2345"}
+	base := mflag.NewFlagSet("base", mflag.ContinueOnError)
+	base.Var(opts.NewNamedListOptsRef("hosts", &hosts, nil), []string{"H", "-host"}, "")
+
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
+	mflag.Merge(flags, base)
+
+	err := findConfigurationConflicts(config, flags)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	flags.Set("-host", "unix:///var/run/docker.sock")
+	err = findConfigurationConflicts(config, flags)
+	if err == nil {
+		t.Fatal("expected error, got nil")
+	}
+	if !strings.Contains(err.Error(), "hosts: (from flag: [unix:///var/run/docker.sock], from file: tcp://127.0.0.1:2345)") {
+		t.Fatalf("expected hosts conflict, got %v", err)
+	}
+}

+ 2 - 1
docker/common.go

@@ -18,6 +18,7 @@ const (
 	defaultCaFile       = "ca.pem"
 	defaultCaFile       = "ca.pem"
 	defaultKeyFile      = "key.pem"
 	defaultKeyFile      = "key.pem"
 	defaultCertFile     = "cert.pem"
 	defaultCertFile     = "cert.pem"
+	tlsVerifyKey        = "tlsverify"
 )
 )
 
 
 var (
 var (
@@ -60,7 +61,7 @@ func postParseCommon() {
 	// Regardless of whether the user sets it to true or false, if they
 	// Regardless of whether the user sets it to true or false, if they
 	// specify --tlsverify at all then we need to turn on tls
 	// specify --tlsverify at all then we need to turn on tls
 	// TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need to check that here as well
 	// TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need to check that here as well
-	if cmd.IsSet("-tlsverify") || commonFlags.TLSVerify {
+	if cmd.IsSet("-"+tlsVerifyKey) || commonFlags.TLSVerify {
 		commonFlags.TLS = true
 		commonFlags.TLS = true
 	}
 	}
 
 

+ 1 - 1
docker/daemon.go

@@ -362,7 +362,7 @@ func loadDaemonCliConfig(config *daemon.Config, daemonFlags *flag.FlagSet, commo
 
 
 	// Regardless of whether the user sets it to true or false, if they
 	// Regardless of whether the user sets it to true or false, if they
 	// specify TLSVerify at all then we need to turn on TLS
 	// specify TLSVerify at all then we need to turn on TLS
-	if config.IsValueSet("tls-verify") {
+	if config.IsValueSet(tlsVerifyKey) {
 		config.TLS = true
 		config.TLS = true
 	}
 	}
 
 

+ 32 - 2
docker/daemon_test.go

@@ -105,10 +105,11 @@ func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) {
 	}
 	}
 
 
 	configFile := f.Name()
 	configFile := f.Name()
-	f.Write([]byte(`{"tls-verify": true}`))
+	f.Write([]byte(`{"tlsverify": true}`))
 	f.Close()
 	f.Close()
 
 
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
+	flags.Bool([]string{"-tlsverify"}, false, "")
 	loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile)
 	loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
@@ -136,10 +137,11 @@ func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) {
 	}
 	}
 
 
 	configFile := f.Name()
 	configFile := f.Name()
-	f.Write([]byte(`{"tls-verify": false}`))
+	f.Write([]byte(`{"tlsverify": false}`))
 	f.Close()
 	f.Close()
 
 
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
+	flags.Bool([]string{"-tlsverify"}, false, "")
 	loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile)
 	loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
@@ -198,6 +200,7 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
 	f.Close()
 	f.Close()
 
 
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
 	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
+	flags.String([]string{"-log-level"}, "", "")
 	loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile)
 	loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
@@ -213,3 +216,30 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
 		t.Fatalf("expected warn log level, got %v", logrus.GetLevel())
 		t.Fatalf("expected warn log level, got %v", logrus.GetLevel())
 	}
 	}
 }
 }
+
+func TestLoadDaemonCliConfigWithTLSOptions(t *testing.T) {
+	c := &daemon.Config{}
+	common := &cli.CommonFlags{}
+
+	f, err := ioutil.TempFile("", "docker-config-")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	configFile := f.Name()
+	f.Write([]byte(`{"tls-opts": {"tlscacert": "/etc/certs/ca.pem"}}`))
+	f.Close()
+
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
+	flags.String([]string{"-tlscacert"}, "", "")
+	loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if loadedConfig == nil {
+		t.Fatalf("expected configuration %v, got nil", c)
+	}
+	if loadedConfig.TLSOptions.CAFile != "/etc/certs/ca.pem" {
+		t.Fatalf("expected CA file path /etc/certs/ca.pem, got %v", loadedConfig.TLSOptions.CAFile)
+	}
+}

+ 1 - 1
docs/reference/commandline/daemon.md

@@ -852,7 +852,7 @@ This is a full example of the allowed configuration options in the file:
 	"hosts": [],
 	"hosts": [],
 	"log-level": "",
 	"log-level": "",
 	"tls": true,
 	"tls": true,
-	"tls-verify": true,
+	"tlsverify": true,
 	"tls-opts": {
 	"tls-opts": {
 		"tlscacert": "",
 		"tlscacert": "",
 		"tlscert": "",
 		"tlscert": "",

+ 19 - 0
pkg/mflag/flag.go

@@ -1223,11 +1223,27 @@ func (v mergeVal) IsBoolFlag() bool {
 	return false
 	return false
 }
 }
 
 
+// Name returns the name of a mergeVal.
+// If the original value had a name, return the original name,
+// otherwise, return the key asinged to this mergeVal.
+func (v mergeVal) Name() string {
+	type namedValue interface {
+		Name() string
+	}
+	if nVal, ok := v.Value.(namedValue); ok {
+		return nVal.Name()
+	}
+	return v.key
+}
+
 // Merge is an helper function that merges n FlagSets into a single dest FlagSet
 // Merge is an helper function that merges n FlagSets into a single dest FlagSet
 // In case of name collision between the flagsets it will apply
 // In case of name collision between the flagsets it will apply
 // the destination FlagSet's errorHandling behavior.
 // the destination FlagSet's errorHandling behavior.
 func Merge(dest *FlagSet, flagsets ...*FlagSet) error {
 func Merge(dest *FlagSet, flagsets ...*FlagSet) error {
 	for _, fset := range flagsets {
 	for _, fset := range flagsets {
+		if fset.formal == nil {
+			continue
+		}
 		for k, f := range fset.formal {
 		for k, f := range fset.formal {
 			if _, ok := dest.formal[k]; ok {
 			if _, ok := dest.formal[k]; ok {
 				var err error
 				var err error
@@ -1249,6 +1265,9 @@ func Merge(dest *FlagSet, flagsets ...*FlagSet) error {
 			}
 			}
 			newF := *f
 			newF := *f
 			newF.Value = mergeVal{f.Value, k, fset}
 			newF.Value = mergeVal{f.Value, k, fset}
+			if dest.formal == nil {
+				dest.formal = make(map[string]*Flag)
+			}
 			dest.formal[k] = &newF
 			dest.formal[k] = &newF
 		}
 		}
 	}
 	}

+ 11 - 0
pkg/mflag/flag_test.go

@@ -514,3 +514,14 @@ func TestSortFlags(t *testing.T) {
 		t.Fatalf("NFlag (%d) != fs.NFlag() (%d) of elements visited", nflag, fs.NFlag())
 		t.Fatalf("NFlag (%d) != fs.NFlag() (%d) of elements visited", nflag, fs.NFlag())
 	}
 	}
 }
 }
+
+func TestMergeFlags(t *testing.T) {
+	base := NewFlagSet("base", ContinueOnError)
+	base.String([]string{"f"}, "", "")
+
+	fs := NewFlagSet("test", ContinueOnError)
+	Merge(fs, base)
+	if len(fs.formal) != 1 {
+		t.Fatalf("FlagCount (%d) != number (1) of elements merged", len(fs.formal))
+	}
+}