diff --git a/registry/remote/credentials/internal/config/config.go b/registry/remote/credentials/internal/config/config.go index ff234291..5bb66a0e 100644 --- a/registry/remote/credentials/internal/config/config.go +++ b/registry/remote/credentials/internal/config/config.go @@ -162,7 +162,20 @@ func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error) authCfgBytes, ok := cfg.authsCache[serverAddress] if !ok { - return auth.EmptyCredential, nil + // NOTE: the auth key for the server address may have been stored with + // a http/https prefix in legacy config files, e.g. "registry.example.com" + // can be stored as "https://registry.example.com/". + var matched bool + for addr, auth := range cfg.authsCache { + if toHostname(addr) == serverAddress { + matched = true + authCfgBytes = auth + break + } + } + if !matched { + return auth.EmptyCredential, nil + } } var authCfg AuthConfig if err := json.Unmarshal(authCfgBytes, &authCfg); err != nil { @@ -300,3 +313,15 @@ func decodeAuth(authStr string) (username string, password string, err error) { } return username, password, nil } + +// toHostname normalizes a server address to just its hostname, removing +// the scheme and the path parts. +// It is used to match keys in the auths map, which may be either stored as +// hostname or as hostname including scheme (in legacy docker config files). +// Reference: https://github.com/docker/cli/blob/v24.0.6/cli/config/credentials/file_store.go#L71 +func toHostname(addr string) string { + addr = strings.TrimPrefix(addr, "http://") + addr = strings.TrimPrefix(addr, "https://") + addr, _, _ = strings.Cut(addr, "/") + return addr +} diff --git a/registry/remote/credentials/internal/config/config_test.go b/registry/remote/credentials/internal/config/config_test.go index 455f23dd..6622108b 100644 --- a/registry/remote/credentials/internal/config/config_test.go +++ b/registry/remote/credentials/internal/config/config_test.go @@ -180,6 +180,94 @@ func TestConfig_GetCredential_validConfig(t *testing.T) { } } +func TestConfig_GetCredential_legacyConfig(t *testing.T) { + cfg, err := Load("../../testdata/legacy_auths_config.json") + if err != nil { + t.Fatal("Load() error =", err) + } + + tests := []struct { + name string + serverAddress string + want auth.Credential + wantErr bool + }{ + { + name: "Regular address matched", + serverAddress: "registry1.example.com", + want: auth.Credential{ + Username: "username1", + Password: "password1", + }, + }, + { + name: "Another entry for the same address matched", + serverAddress: "https://registry1.example.com/", + want: auth.Credential{ + Username: "foo", + Password: "bar", + }, + }, + { + name: "Address with different scheme unmached", + serverAddress: "http://registry1.example.com/", + want: auth.EmptyCredential, + }, + { + name: "Address with http prefix matched", + serverAddress: "registry2.example.com", + want: auth.Credential{ + Username: "username2", + Password: "password2", + }, + }, + { + name: "Address with https prefix matched", + serverAddress: "registry3.example.com", + want: auth.Credential{ + Username: "username3", + Password: "password3", + }, + }, + { + name: "Address with http prefix and / suffix matched", + serverAddress: "registry4.example.com", + want: auth.Credential{ + Username: "username4", + Password: "password4", + }, + }, + { + name: "Address with https prefix and / suffix matched", + serverAddress: "registry5.example.com", + want: auth.Credential{ + Username: "username5", + Password: "password5", + }, + }, + { + name: "Address with https prefix and path suffix matched", + serverAddress: "registry6.example.com", + want: auth.Credential{ + Username: "username6", + Password: "password6", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := cfg.GetCredential(tt.serverAddress) + if (err != nil) != tt.wantErr { + t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want) + } + }) + } +} + func TestConfig_GetCredential_invalidConfig(t *testing.T) { cfg, err := Load("../../testdata/invalid_auths_entry_config.json") if err != nil { @@ -1314,3 +1402,51 @@ func Test_decodeAuth(t *testing.T) { }) } } + +func Test_toHostname(t *testing.T) { + tests := []struct { + name string + addr string + want string + }{ + { + addr: "http://test.example.com", + want: "test.example.com", + }, + { + addr: "http://test.example.com/", + want: "test.example.com", + }, + { + addr: "http://test.example.com/foo/bar", + want: "test.example.com", + }, + { + addr: "https://test.example.com", + want: "test.example.com", + }, + { + addr: "https://test.example.com/", + want: "test.example.com", + }, + { + addr: "http://test.example.com/foo/bar", + want: "test.example.com", + }, + { + addr: "test.example.com", + want: "test.example.com", + }, + { + addr: "test.example.com/foo/bar/", + want: "test.example.com", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toHostname(tt.addr); got != tt.want { + t.Errorf("toHostname() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/registry/remote/credentials/testdata/legacy_auths_config.json b/registry/remote/credentials/testdata/legacy_auths_config.json new file mode 100644 index 00000000..20d6a8d1 --- /dev/null +++ b/registry/remote/credentials/testdata/legacy_auths_config.json @@ -0,0 +1,25 @@ +{ + "auths": { + "registry1.example.com": { + "auth": "dXNlcm5hbWUxOnBhc3N3b3JkMQ==" + }, + "http://registry2.example.com": { + "auth": "dXNlcm5hbWUyOnBhc3N3b3JkMg==" + }, + "https://registry3.example.com": { + "auth": "dXNlcm5hbWUzOnBhc3N3b3JkMw==" + }, + "http://registry4.example.com/": { + "auth": "dXNlcm5hbWU0OnBhc3N3b3JkNA==" + }, + "https://registry5.example.com/": { + "auth": "dXNlcm5hbWU1OnBhc3N3b3JkNQ==" + }, + "https://registry6.example.com/path/": { + "auth": "dXNlcm5hbWU2OnBhc3N3b3JkNg==" + }, + "https://registry1.example.com/": { + "auth": "Zm9vOmJhcg==" + } + } +}