github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/daemon/config/config_test.go (about)

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