github.com/rumpl/bof@v23.0.0-rc.2+incompatible/daemon/config/config_test.go (about)

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