Repository: oras-project/oras
Pull request #549 (2022-09-09 07:13:41)

fix: provide function to support hostname mapping
Resolve: #542 
Signed-off-by: Juncheng Zhu <junczhu@microsoft.com>

qweeah: What if username/password are passed through flags? Replacing host name in cred store only works when credential is empty,  see 
https://github.com/oras-project/oras/blob/main/cmd/oras/internal/option/remote.go#L152
shizhMSFT: @qweeah I've discussed offline with @junczhu. The behavior of `docker.io` in docker is special that it connects to the host of `registry-1.docker.io` using the credential with the reigstry name `https://index.docker.io/v1/`, which is really not a valid name (but it uses).
codecov-commenter: # [Codecov](https://codecov.io/gh/oras-project/oras/pull/549?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) Report
> Merging [#549](https://codecov.io/gh/oras-project/oras/pull/549?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) (362d264) into [main](https://codecov.io/gh/oras-project/oras/commit/9ea7aee84a1a3829a6506d44114424609b546f22?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) (9ea7aee) will **decrease** coverage by `2.04%`.
> The diff coverage is `31.81%`.

```diff
@@            Coverage Diff             @@
##             main     #549      +/-   ##
==========================================
- Coverage   71.36%   69.31%   -2.05%     
==========================================
  Files          11       11              
  Lines         419      440      +21     
==========================================
+ Hits          299      305       +6     
- Misses         98      110      +12     
- Partials       22       25       +3     
```


| [Impacted Files](https://codecov.io/gh/oras-project/oras/pull/549?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) | Coverage Δ | |
|---|---|---|
| [internal/credential/store.go](https://codecov.io/gh/oras-project/oras/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project#diff-aW50ZXJuYWwvY3JlZGVudGlhbC9zdG9yZS5nbw==) | `65.27% <ø> (-0.48%)` | :arrow_down: |
| [cmd/oras/internal/option/remote.go](https://codecov.io/gh/oras-project/oras/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project#diff-Y21kL29yYXMvaW50ZXJuYWwvb3B0aW9uL3JlbW90ZS5nbw==) | `61.53% <31.81%> (-3.98%)` | :arrow_down: |
| [internal/cache/target.go](https://codecov.io/gh/oras-project/oras/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project#diff-aW50ZXJuYWwvY2FjaGUvdGFyZ2V0Lmdv) | `58.44% <0.00%> (-2.43%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project)


Patch:
diff --git a/cmd/oras/internal/option/remote.go b/cmd/oras/internal/option/remote.go
index d15d478..3830ff0 100644
--- a/cmd/oras/internal/option/remote.go
+++ b/cmd/oras/internal/option/remote.go
@@ -110,7 +110,7 @@ func (opts *Remote) tlsConfig() (*tls.Config, error) {
 }
 
 // authClient assembles a oras auth client.
-func (opts *Remote) authClient(debug bool) (client *auth.Client, err error) {
+func (opts *Remote) authClient(registry string, debug bool) (client *auth.Client, err error) {
 	config, err := opts.tlsConfig()
 	if err != nil {
 		return nil, err
@@ -149,7 +149,19 @@ func (opts *Remote) authClient(debug bool) (client *auth.Client, err error) {
 		if err != nil {
 			return nil, err
 		}
-		client.Credential = store.Credential
+		// For a user case with a registry from 'docker.io', the hostname is "registry-1.docker.io"
+		// According to the the behavior of Docker CLI,
+		// credential under key "https://index.docker.io/v1/" should be provided
+		if registry == "docker.io" {
+			client.Credential = func(ctx context.Context, hostname string) (auth.Credential, error) {
+				if hostname == "registry-1.docker.io" {
+					hostname = "https://index.docker.io/v1/"
+				}
+				return store.Credential(ctx, hostname)
+			}
+		} else {
+			client.Credential = store.Credential
+		}
 	}
 	return
 }
@@ -165,8 +177,9 @@ func (opts *Remote) NewRegistry(hostname string, common Common) (reg *remote.Reg
 	if err != nil {
 		return nil, err
 	}
-	reg.PlainHTTP = opts.isPlainHttp(reg.Reference.Registry)
-	if reg.Client, err = opts.authClient(common.Debug); err != nil {
+	hostname = reg.Reference.Registry
+	reg.PlainHTTP = opts.isPlainHttp(hostname)
+	if reg.Client, err = opts.authClient(hostname, common.Debug); err != nil {
 		return nil, err
 	}
 	return
@@ -178,14 +191,15 @@ func (opts *Remote) NewRepository(reference string, common Common) (repo *remote
 	if err != nil {
 		return nil, err
 	}
-	repo.PlainHTTP = opts.isPlainHttp(repo.Reference.Registry)
-	if repo.Client, err = opts.authClient(common.Debug); err != nil {
+	hostname := repo.Reference.Registry
+	repo.PlainHTTP = opts.isPlainHttp(hostname)
+	if repo.Client, err = opts.authClient(hostname, common.Debug); err != nil {
 		return nil, err
 	}
 	return
 }
 
-// isPlainHttp returns the plain http flag for a given regsitry.
+// isPlainHttp returns the plain http flag for a given registry.
 func (opts *Remote) isPlainHttp(registry string) bool {
 	host, _, _ := net.SplitHostPort(registry)
 	if host == "localhost" || registry == "localhost" {
diff --git a/cmd/oras/login.go b/cmd/oras/login.go
index e2ef5be..c7f8b0a 100644
--- a/cmd/oras/login.go
+++ b/cmd/oras/login.go
@@ -116,7 +116,14 @@ func runLogin(opts loginOptions) (err error) {
 	if err != nil {
 		return err
 	}
-	if err := store.Store(opts.Hostname, opts.Credential()); err != nil {
+	// For a user case that login 'docker.io',
+	// According the the behavior of Docker CLI,
+	// credential should be added under key "https://index.docker.io/v1/"
+	hostname := opts.Hostname
+	if hostname == "docker.io" {
+		hostname = "https://index.docker.io/v1/"
+	}
+	if err := store.Store(hostname, opts.Credential()); err != nil {
 		return err
 	}
 	fmt.Println("Login Succeeded")
diff --git a/cmd/oras/logout.go b/cmd/oras/logout.go
index b4ad2fc..0d42fa5 100644
--- a/cmd/oras/logout.go
+++ b/cmd/oras/logout.go
@@ -59,6 +59,12 @@ func runLogout(opts logoutOptions) error {
 	if err != nil {
 		return err
 	}
-
-	return store.Erase(opts.hostname)
+	// For a user case that logout from 'docker.io',
+	// According the the behavior of Docker CLI,
+	// credential under key "https://index.docker.io/v1/" should be removed
+	hostname := opts.hostname
+	if hostname == "docker.io" {
+		hostname = "https://index.docker.io/v1/"
+	}
+	return store.Erase(hostname)
 }
diff --git a/internal/credential/store.go b/internal/credential/store.go
index 5a88727..94660d7 100644
--- a/internal/credential/store.go
+++ b/internal/credential/store.go
@@ -99,7 +99,6 @@ func (s *Store) Store(registry string, cred auth.Credential) error {
 		IdentityToken: cred.RefreshToken,
 		RegistryToken: cred.AccessToken,
 	}
-
 	return s.configs[0].GetCredentialsStore(registry).Store(authConf)
 }
 


Test patch:
diff --git a/cmd/oras/internal/option/remote_test.go b/cmd/oras/internal/option/remote_test.go
index d83e632..ebac9fd 100644
--- a/cmd/oras/internal/option/remote_test.go
+++ b/cmd/oras/internal/option/remote_test.go
@@ -81,7 +81,7 @@ func TestRemote_authClient_RawCredential(t *testing.T) {
 		Username: want.Username,
 		Password: want.Password,
 	}
-	client, err := opts.authClient(false)
+	client, err := opts.authClient("hostname", false)
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
@@ -99,7 +99,7 @@ func TestRemote_authClient_skipTlsVerify(t *testing.T) {
 	opts := Remote{
 		Insecure: true,
 	}
-	client, err := opts.authClient(false)
+	client, err := opts.authClient("hostname", false)
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
@@ -125,7 +125,7 @@ func TestRemote_authClient_CARoots(t *testing.T) {
 	opts := Remote{
 		CACertFilePath: caPath,
 	}
-	client, err := opts.authClient(false)
+	client, err := opts.authClient("hostname", false)
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}