github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/daemon/config/config_test.go (about)

     1  package config // import "github.com/Prakhar-Agarwal-byte/moby/daemon/config"
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"reflect"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/Prakhar-Agarwal-byte/moby/libnetwork/ipamutils"
    11  	"github.com/Prakhar-Agarwal-byte/moby/opts"
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/google/go-cmp/cmp/cmpopts"
    14  	"github.com/imdario/mergo"
    15  	"github.com/spf13/pflag"
    16  	"golang.org/x/text/encoding"
    17  	"golang.org/x/text/encoding/unicode"
    18  	"gotest.tools/v3/assert"
    19  	is "gotest.tools/v3/assert/cmp"
    20  	"gotest.tools/v3/skip"
    21  )
    22  
    23  func makeConfigFile(t *testing.T, content string) string {
    24  	t.Helper()
    25  	name := filepath.Join(t.TempDir(), "daemon.json")
    26  	err := os.WriteFile(name, []byte(content), 0o666)
    27  	assert.NilError(t, err)
    28  	return name
    29  }
    30  
    31  func TestDaemonConfigurationNotFound(t *testing.T) {
    32  	_, err := MergeDaemonConfigurations(&Config{}, nil, "/tmp/foo-bar-baz-docker")
    33  	assert.Check(t, os.IsNotExist(err), "got: %[1]T: %[1]v", err)
    34  }
    35  
    36  func TestDaemonBrokenConfiguration(t *testing.T) {
    37  	configFile := makeConfigFile(t, `{"Debug": tru`)
    38  
    39  	_, err := MergeDaemonConfigurations(&Config{}, nil, configFile)
    40  	assert.ErrorContains(t, err, `invalid character ' ' in literal true`)
    41  }
    42  
    43  // TestDaemonConfigurationUnicodeVariations feeds various variations of Unicode into the JSON parser, ensuring that we
    44  // respect a BOM and otherwise default to UTF-8.
    45  func TestDaemonConfigurationUnicodeVariations(t *testing.T) {
    46  	jsonData := `{"debug": true}`
    47  
    48  	testCases := []struct {
    49  		name     string
    50  		encoding encoding.Encoding
    51  	}{
    52  		{
    53  			name:     "UTF-8",
    54  			encoding: unicode.UTF8,
    55  		},
    56  		{
    57  			name:     "UTF-8 (with BOM)",
    58  			encoding: unicode.UTF8BOM,
    59  		},
    60  		{
    61  			name:     "UTF-16 (BE with BOM)",
    62  			encoding: unicode.UTF16(unicode.BigEndian, unicode.UseBOM),
    63  		},
    64  		{
    65  			name:     "UTF-16 (LE with BOM)",
    66  			encoding: unicode.UTF16(unicode.LittleEndian, unicode.UseBOM),
    67  		},
    68  	}
    69  	for _, tc := range testCases {
    70  		t.Run(tc.name, func(t *testing.T) {
    71  			encodedJson, err := tc.encoding.NewEncoder().String(jsonData)
    72  			assert.NilError(t, err)
    73  			configFile := makeConfigFile(t, encodedJson)
    74  			_, err = MergeDaemonConfigurations(&Config{}, nil, configFile)
    75  			assert.NilError(t, err)
    76  		})
    77  	}
    78  }
    79  
    80  // TestDaemonConfigurationInvalidUnicode ensures that the JSON parser returns a useful error message if malformed UTF-8
    81  // is provided.
    82  func TestDaemonConfigurationInvalidUnicode(t *testing.T) {
    83  	configFileBOM := makeConfigFile(t, "\xef\xbb\xbf{\"debug\": true}\xff")
    84  	_, err := MergeDaemonConfigurations(&Config{}, nil, configFileBOM)
    85  	assert.ErrorIs(t, err, encoding.ErrInvalidUTF8)
    86  
    87  	configFileNoBOM := makeConfigFile(t, "{\"debug\": true}\xff")
    88  	_, err = MergeDaemonConfigurations(&Config{}, nil, configFileNoBOM)
    89  	assert.ErrorIs(t, err, encoding.ErrInvalidUTF8)
    90  }
    91  
    92  func TestFindConfigurationConflicts(t *testing.T) {
    93  	config := map[string]interface{}{"authorization-plugins": "foobar"}
    94  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
    95  
    96  	flags.String("authorization-plugins", "", "")
    97  	assert.Check(t, flags.Set("authorization-plugins", "asdf"))
    98  	assert.Check(t, is.ErrorContains(findConfigurationConflicts(config, flags), "authorization-plugins: (from flag: asdf, from file: foobar)"))
    99  }
   100  
   101  func TestFindConfigurationConflictsWithNamedOptions(t *testing.T) {
   102  	config := map[string]interface{}{"hosts": []string{"qwer"}}
   103  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   104  
   105  	var hosts []string
   106  	flags.VarP(opts.NewNamedListOptsRef("hosts", &hosts, opts.ValidateHost), "host", "H", "Daemon socket(s) to connect to")
   107  	assert.Check(t, flags.Set("host", "tcp://127.0.0.1:4444"))
   108  	assert.Check(t, flags.Set("host", "unix:///var/run/docker.sock"))
   109  	assert.Check(t, is.ErrorContains(findConfigurationConflicts(config, flags), "hosts"))
   110  }
   111  
   112  func TestDaemonConfigurationMergeConflicts(t *testing.T) {
   113  	configFile := makeConfigFile(t, `{"debug": true}`)
   114  
   115  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   116  	flags.Bool("debug", false, "")
   117  	assert.Check(t, flags.Set("debug", "false"))
   118  
   119  	_, err := MergeDaemonConfigurations(&Config{}, flags, configFile)
   120  	if err == nil {
   121  		t.Fatal("expected error, got nil")
   122  	}
   123  	if !strings.Contains(err.Error(), "debug") {
   124  		t.Fatalf("expected debug conflict, got %v", err)
   125  	}
   126  }
   127  
   128  func TestDaemonConfigurationMergeConcurrent(t *testing.T) {
   129  	configFile := makeConfigFile(t, `{"max-concurrent-downloads": 1}`)
   130  
   131  	_, err := MergeDaemonConfigurations(&Config{}, nil, configFile)
   132  	assert.NilError(t, err)
   133  }
   134  
   135  func TestDaemonConfigurationMergeConcurrentError(t *testing.T) {
   136  	configFile := makeConfigFile(t, `{"max-concurrent-downloads": -1}`)
   137  
   138  	_, err := MergeDaemonConfigurations(&Config{}, nil, configFile)
   139  	assert.ErrorContains(t, err, `invalid max concurrent downloads: -1`)
   140  }
   141  
   142  func TestDaemonConfigurationMergeConflictsWithInnerStructs(t *testing.T) {
   143  	configFile := makeConfigFile(t, `{"tlscacert": "/etc/certificates/ca.pem"}`)
   144  
   145  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   146  	flags.String("tlscacert", "", "")
   147  	assert.Check(t, flags.Set("tlscacert", "~/.docker/ca.pem"))
   148  
   149  	_, err := MergeDaemonConfigurations(&Config{}, flags, configFile)
   150  	assert.ErrorContains(t, err, `the following directives are specified both as a flag and in the configuration file: tlscacert`)
   151  }
   152  
   153  // TestDaemonConfigurationMergeDefaultAddressPools is a regression test for #40711.
   154  func TestDaemonConfigurationMergeDefaultAddressPools(t *testing.T) {
   155  	emptyConfigFile := makeConfigFile(t, `{}`)
   156  	configFile := makeConfigFile(t, `{"default-address-pools":[{"base": "10.123.0.0/16", "size": 24 }]}`)
   157  
   158  	expected := []*ipamutils.NetworkToSplit{{Base: "10.123.0.0/16", Size: 24}}
   159  
   160  	t.Run("empty config file", func(t *testing.T) {
   161  		conf := Config{}
   162  		flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   163  		flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "")
   164  		assert.Check(t, flags.Set("default-address-pool", "base=10.123.0.0/16,size=24"))
   165  
   166  		config, err := MergeDaemonConfigurations(&conf, flags, emptyConfigFile)
   167  		assert.NilError(t, err)
   168  		assert.DeepEqual(t, config.DefaultAddressPools.Value(), expected)
   169  	})
   170  
   171  	t.Run("config file", func(t *testing.T) {
   172  		conf := Config{}
   173  		flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   174  		flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "")
   175  
   176  		config, err := MergeDaemonConfigurations(&conf, flags, configFile)
   177  		assert.NilError(t, err)
   178  		assert.DeepEqual(t, config.DefaultAddressPools.Value(), expected)
   179  	})
   180  
   181  	t.Run("with conflicting options", func(t *testing.T) {
   182  		conf := Config{}
   183  		flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   184  		flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "")
   185  		assert.Check(t, flags.Set("default-address-pool", "base=10.123.0.0/16,size=24"))
   186  
   187  		_, err := MergeDaemonConfigurations(&conf, flags, configFile)
   188  		assert.ErrorContains(t, err, "the following directives are specified both as a flag and in the configuration file")
   189  		assert.ErrorContains(t, err, "default-address-pools")
   190  	})
   191  }
   192  
   193  func TestFindConfigurationConflictsWithUnknownKeys(t *testing.T) {
   194  	config := map[string]interface{}{"tls-verify": "true"}
   195  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   196  
   197  	flags.Bool("tlsverify", false, "")
   198  	err := findConfigurationConflicts(config, flags)
   199  	assert.ErrorContains(t, err, "the following directives don't match any configuration option: tls-verify")
   200  }
   201  
   202  func TestFindConfigurationConflictsWithMergedValues(t *testing.T) {
   203  	var hosts []string
   204  	config := map[string]interface{}{"hosts": "tcp://127.0.0.1:2345"}
   205  	flags := pflag.NewFlagSet("base", pflag.ContinueOnError)
   206  	flags.VarP(opts.NewNamedListOptsRef("hosts", &hosts, nil), "host", "H", "")
   207  
   208  	err := findConfigurationConflicts(config, flags)
   209  	assert.NilError(t, err)
   210  
   211  	assert.Check(t, flags.Set("host", "unix:///var/run/docker.sock"))
   212  	err = findConfigurationConflicts(config, flags)
   213  	assert.ErrorContains(t, err, "hosts: (from flag: [unix:///var/run/docker.sock], from file: tcp://127.0.0.1:2345)")
   214  }
   215  
   216  func TestValidateConfigurationErrors(t *testing.T) {
   217  	testCases := []struct {
   218  		name        string
   219  		field       string
   220  		config      *Config
   221  		expectedErr string
   222  	}{
   223  		{
   224  			name: "single label without value",
   225  			config: &Config{
   226  				CommonConfig: CommonConfig{
   227  					Labels: []string{"one"},
   228  				},
   229  			},
   230  			expectedErr: "bad attribute format: one",
   231  		},
   232  		{
   233  			name: "multiple label without value",
   234  			config: &Config{
   235  				CommonConfig: CommonConfig{
   236  					Labels: []string{"foo=bar", "one"},
   237  				},
   238  			},
   239  			expectedErr: "bad attribute format: one",
   240  		},
   241  		{
   242  			name: "single DNS, invalid IP-address",
   243  			config: &Config{
   244  				CommonConfig: CommonConfig{
   245  					DNSConfig: DNSConfig{
   246  						DNS: []string{"1.1.1.1o"},
   247  					},
   248  				},
   249  			},
   250  			expectedErr: "1.1.1.1o is not an ip address",
   251  		},
   252  		{
   253  			name: "multiple DNS, invalid IP-address",
   254  			config: &Config{
   255  				CommonConfig: CommonConfig{
   256  					DNSConfig: DNSConfig{
   257  						DNS: []string{"2.2.2.2", "1.1.1.1o"},
   258  					},
   259  				},
   260  			},
   261  			expectedErr: "1.1.1.1o is not an ip address",
   262  		},
   263  		{
   264  			name: "single DNSSearch",
   265  			config: &Config{
   266  				CommonConfig: CommonConfig{
   267  					DNSConfig: DNSConfig{
   268  						DNSSearch: []string{"123456"},
   269  					},
   270  				},
   271  			},
   272  			expectedErr: "123456 is not a valid domain",
   273  		},
   274  		{
   275  			name: "multiple DNSSearch",
   276  			config: &Config{
   277  				CommonConfig: CommonConfig{
   278  					DNSConfig: DNSConfig{
   279  						DNSSearch: []string{"a.b.c", "123456"},
   280  					},
   281  				},
   282  			},
   283  			expectedErr: "123456 is not a valid domain",
   284  		},
   285  		{
   286  			name: "negative MTU",
   287  			config: &Config{
   288  				CommonConfig: CommonConfig{
   289  					BridgeConfig: BridgeConfig{MTU: -10},
   290  				},
   291  			},
   292  			expectedErr: "invalid default MTU: -10",
   293  		},
   294  		{
   295  			name: "negative max-concurrent-downloads",
   296  			config: &Config{
   297  				CommonConfig: CommonConfig{
   298  					MaxConcurrentDownloads: -10,
   299  				},
   300  			},
   301  			expectedErr: "invalid max concurrent downloads: -10",
   302  		},
   303  		{
   304  			name: "negative max-concurrent-uploads",
   305  			config: &Config{
   306  				CommonConfig: CommonConfig{
   307  					MaxConcurrentUploads: -10,
   308  				},
   309  			},
   310  			expectedErr: "invalid max concurrent uploads: -10",
   311  		},
   312  		{
   313  			name: "negative max-download-attempts",
   314  			config: &Config{
   315  				CommonConfig: CommonConfig{
   316  					MaxDownloadAttempts: -10,
   317  				},
   318  			},
   319  			expectedErr: "invalid max download attempts: -10",
   320  		},
   321  		// TODO(thaJeztah) temporarily excluding this test as it assumes defaults are set before validating and applying updated configs
   322  		/*
   323  			{
   324  				name:  "zero max-download-attempts",
   325  				field: "MaxDownloadAttempts",
   326  				config: &Config{
   327  					CommonConfig: CommonConfig{
   328  						MaxDownloadAttempts: 0,
   329  					},
   330  				},
   331  				expectedErr: "invalid max download attempts: 0",
   332  			},
   333  		*/
   334  		{
   335  			name: "generic resource without =",
   336  			config: &Config{
   337  				CommonConfig: CommonConfig{
   338  					NodeGenericResources: []string{"foo"},
   339  				},
   340  			},
   341  			expectedErr: "could not parse GenericResource: incorrect term foo, missing '=' or malformed expression",
   342  		},
   343  		{
   344  			name: "generic resource mixed named and discrete",
   345  			config: &Config{
   346  				CommonConfig: CommonConfig{
   347  					NodeGenericResources: []string{"foo=bar", "foo=1"},
   348  				},
   349  			},
   350  			expectedErr: "could not parse GenericResource: mixed discrete and named resources in expression 'foo=[bar 1]'",
   351  		},
   352  		{
   353  			name: "with invalid hosts",
   354  			config: &Config{
   355  				CommonConfig: CommonConfig{
   356  					Hosts: []string{"127.0.0.1:2375/path"},
   357  				},
   358  			},
   359  			expectedErr: "invalid bind address (127.0.0.1:2375/path): should not contain a path element",
   360  		},
   361  		{
   362  			name: "with invalid log-level",
   363  			config: &Config{
   364  				CommonConfig: CommonConfig{
   365  					LogLevel: "foobar",
   366  				},
   367  			},
   368  			expectedErr: "invalid logging level: foobar",
   369  		},
   370  	}
   371  	for _, tc := range testCases {
   372  		t.Run(tc.name, func(t *testing.T) {
   373  			cfg, err := New()
   374  			assert.NilError(t, err)
   375  			if tc.field != "" {
   376  				assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride, withForceOverwrite(tc.field)))
   377  			} else {
   378  				assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride))
   379  			}
   380  			err = Validate(cfg)
   381  			assert.Error(t, err, tc.expectedErr)
   382  		})
   383  	}
   384  }
   385  
   386  func withForceOverwrite(fieldName string) func(config *mergo.Config) {
   387  	return mergo.WithTransformers(overwriteTransformer{fieldName: fieldName})
   388  }
   389  
   390  type overwriteTransformer struct {
   391  	fieldName string
   392  }
   393  
   394  func (tf overwriteTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
   395  	if typ == reflect.TypeOf(CommonConfig{}) {
   396  		return func(dst, src reflect.Value) error {
   397  			dst.FieldByName(tf.fieldName).Set(src.FieldByName(tf.fieldName))
   398  			return nil
   399  		}
   400  	}
   401  	return nil
   402  }
   403  
   404  func TestValidateConfiguration(t *testing.T) {
   405  	testCases := []struct {
   406  		name   string
   407  		field  string
   408  		config *Config
   409  	}{
   410  		{
   411  			name:  "with label",
   412  			field: "Labels",
   413  			config: &Config{
   414  				CommonConfig: CommonConfig{
   415  					Labels: []string{"one=two"},
   416  				},
   417  			},
   418  		},
   419  		{
   420  			name:  "with dns",
   421  			field: "DNSConfig",
   422  			config: &Config{
   423  				CommonConfig: CommonConfig{
   424  					DNSConfig: DNSConfig{
   425  						DNS: []string{"1.1.1.1"},
   426  					},
   427  				},
   428  			},
   429  		},
   430  		{
   431  			name:  "with dns-search",
   432  			field: "DNSConfig",
   433  			config: &Config{
   434  				CommonConfig: CommonConfig{
   435  					DNSConfig: DNSConfig{
   436  						DNSSearch: []string{"a.b.c"},
   437  					},
   438  				},
   439  			},
   440  		},
   441  		{
   442  			name:  "with mtu",
   443  			field: "MTU",
   444  			config: &Config{
   445  				CommonConfig: CommonConfig{
   446  					BridgeConfig: BridgeConfig{MTU: 1234},
   447  				},
   448  			},
   449  		},
   450  		{
   451  			name:  "with max-concurrent-downloads",
   452  			field: "MaxConcurrentDownloads",
   453  			config: &Config{
   454  				CommonConfig: CommonConfig{
   455  					MaxConcurrentDownloads: 4,
   456  				},
   457  			},
   458  		},
   459  		{
   460  			name:  "with max-concurrent-uploads",
   461  			field: "MaxConcurrentUploads",
   462  			config: &Config{
   463  				CommonConfig: CommonConfig{
   464  					MaxConcurrentUploads: 4,
   465  				},
   466  			},
   467  		},
   468  		{
   469  			name:  "with max-download-attempts",
   470  			field: "MaxDownloadAttempts",
   471  			config: &Config{
   472  				CommonConfig: CommonConfig{
   473  					MaxDownloadAttempts: 4,
   474  				},
   475  			},
   476  		},
   477  		{
   478  			name:  "with multiple node generic resources",
   479  			field: "NodeGenericResources",
   480  			config: &Config{
   481  				CommonConfig: CommonConfig{
   482  					NodeGenericResources: []string{"foo=bar", "foo=baz"},
   483  				},
   484  			},
   485  		},
   486  		{
   487  			name:  "with node generic resources",
   488  			field: "NodeGenericResources",
   489  			config: &Config{
   490  				CommonConfig: CommonConfig{
   491  					NodeGenericResources: []string{"foo=1"},
   492  				},
   493  			},
   494  		},
   495  		{
   496  			name:  "with hosts",
   497  			field: "Hosts",
   498  			config: &Config{
   499  				CommonConfig: CommonConfig{
   500  					Hosts: []string{"tcp://127.0.0.1:2375"},
   501  				},
   502  			},
   503  		},
   504  		{
   505  			name:  "with log-level warn",
   506  			field: "LogLevel",
   507  			config: &Config{
   508  				CommonConfig: CommonConfig{
   509  					LogLevel: "warn",
   510  				},
   511  			},
   512  		},
   513  	}
   514  	for _, tc := range testCases {
   515  		t.Run(tc.name, func(t *testing.T) {
   516  			// Start with a config with all defaults set, so that we only
   517  			cfg, err := New()
   518  			assert.NilError(t, err)
   519  			assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride))
   520  
   521  			// Check that the override happened :)
   522  			assert.Check(t, is.DeepEqual(cfg, tc.config, field(tc.field)))
   523  			err = Validate(cfg)
   524  			assert.NilError(t, err)
   525  		})
   526  	}
   527  }
   528  
   529  func field(field string) cmp.Option {
   530  	tmp := reflect.TypeOf(Config{})
   531  	ignoreFields := make([]string, 0, tmp.NumField())
   532  	for i := 0; i < tmp.NumField(); i++ {
   533  		if tmp.Field(i).Name != field {
   534  			ignoreFields = append(ignoreFields, tmp.Field(i).Name)
   535  		}
   536  	}
   537  	return cmpopts.IgnoreFields(Config{}, ignoreFields...)
   538  }
   539  
   540  // TestReloadSetConfigFileNotExist tests that when `--config-file` is set, and it doesn't exist the `Reload` function
   541  // returns an error.
   542  func TestReloadSetConfigFileNotExist(t *testing.T) {
   543  	configFile := "/tmp/blabla/not/exists/config.json"
   544  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   545  	flags.String("config-file", "", "")
   546  	assert.Check(t, flags.Set("config-file", configFile))
   547  
   548  	err := Reload(configFile, flags, func(c *Config) {})
   549  	assert.Check(t, is.ErrorContains(err, "unable to configure the Docker daemon with file"))
   550  }
   551  
   552  // TestReloadDefaultConfigNotExist tests that if the default configuration file doesn't exist the daemon still will
   553  // still be reloaded.
   554  func TestReloadDefaultConfigNotExist(t *testing.T) {
   555  	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
   556  	defaultConfigFile := "/tmp/blabla/not/exists/daemon.json"
   557  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   558  	flags.String("config-file", defaultConfigFile, "")
   559  	reloaded := false
   560  	err := Reload(defaultConfigFile, flags, func(c *Config) {
   561  		reloaded = true
   562  	})
   563  	assert.Check(t, err)
   564  	assert.Check(t, reloaded)
   565  }
   566  
   567  // TestReloadBadDefaultConfig tests that when `--config-file` is not set and the default configuration file exists and
   568  // is bad, an error is returned.
   569  func TestReloadBadDefaultConfig(t *testing.T) {
   570  	configFile := makeConfigFile(t, `{wrong: "configuration"}`)
   571  
   572  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   573  	flags.String("config-file", configFile, "")
   574  	reloaded := false
   575  	err := Reload(configFile, flags, func(c *Config) {
   576  		reloaded = true
   577  	})
   578  	assert.Check(t, is.ErrorContains(err, "unable to configure the Docker daemon with file"))
   579  	assert.Check(t, reloaded == false)
   580  }
   581  
   582  func TestReloadWithConflictingLabels(t *testing.T) {
   583  	configFile := makeConfigFile(t, `{"labels": ["foo=bar", "foo=baz"]}`)
   584  
   585  	var lbls []string
   586  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   587  	flags.String("config-file", configFile, "")
   588  	flags.StringSlice("labels", lbls, "")
   589  	reloaded := false
   590  	err := Reload(configFile, flags, func(c *Config) {
   591  		reloaded = true
   592  	})
   593  	assert.Check(t, is.ErrorContains(err, "conflict labels for foo=baz and foo=bar"))
   594  	assert.Check(t, reloaded == false)
   595  }
   596  
   597  func TestReloadWithDuplicateLabels(t *testing.T) {
   598  	configFile := makeConfigFile(t, `{"labels": ["foo=the-same", "foo=the-same"]}`)
   599  
   600  	var lbls []string
   601  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   602  	flags.String("config-file", configFile, "")
   603  	flags.StringSlice("labels", lbls, "")
   604  	reloaded := false
   605  	err := Reload(configFile, flags, func(c *Config) {
   606  		reloaded = true
   607  		assert.Check(t, is.DeepEqual(c.Labels, []string{"foo=the-same"}))
   608  	})
   609  	assert.Check(t, err)
   610  	assert.Check(t, reloaded)
   611  }
   612  
   613  func TestMaskURLCredentials(t *testing.T) {
   614  	tests := []struct {
   615  		rawURL    string
   616  		maskedURL string
   617  	}{
   618  		{
   619  			rawURL:    "",
   620  			maskedURL: "",
   621  		}, {
   622  			rawURL:    "invalidURL",
   623  			maskedURL: "invalidURL",
   624  		}, {
   625  			rawURL:    "http://proxy.example.com:80/",
   626  			maskedURL: "http://proxy.example.com:80/",
   627  		}, {
   628  			rawURL:    "http://USER:PASSWORD@proxy.example.com:80/",
   629  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/",
   630  		}, {
   631  			rawURL:    "http://PASSWORD:PASSWORD@proxy.example.com:80/",
   632  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/",
   633  		}, {
   634  			rawURL:    "http://USER:@proxy.example.com:80/",
   635  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/",
   636  		}, {
   637  			rawURL:    "http://:PASSWORD@proxy.example.com:80/",
   638  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/",
   639  		}, {
   640  			rawURL:    "http://USER@docker:password@proxy.example.com:80/",
   641  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/",
   642  		}, {
   643  			rawURL:    "http://USER%40docker:password@proxy.example.com:80/",
   644  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/",
   645  		}, {
   646  			rawURL:    "http://USER%40docker:pa%3Fsword@proxy.example.com:80/",
   647  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/",
   648  		}, {
   649  			rawURL:    "http://USER%40docker:pa%3Fsword@proxy.example.com:80/hello%20world",
   650  			maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/hello%20world",
   651  		},
   652  	}
   653  	for _, test := range tests {
   654  		maskedURL := MaskCredentials(test.rawURL)
   655  		assert.Equal(t, maskedURL, test.maskedURL)
   656  	}
   657  }