Skip to content

Commit 30c63cf

Browse files
authored
Merge pull request containerd#3293 from apostasie/dev-login-1
Login & authentication rework, part 1: credentials management and URL parsing
2 parents 51258bd + 04cc74b commit 30c63cf

File tree

13 files changed

+1397
-385
lines changed

13 files changed

+1397
-385
lines changed

cmd/nerdctl/login/login_linux_test.go

Lines changed: 324 additions & 113 deletions
Large diffs are not rendered by default.

cmd/nerdctl/login/logout.go

Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@
1717
package login
1818

1919
import (
20-
"fmt"
21-
22-
dockercliconfig "github.com/docker/cli/cli/config"
2320
"github.com/spf13/cobra"
2421

25-
"github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
22+
"github.com/containerd/log"
23+
24+
"github.com/containerd/nerdctl/v2/pkg/cmd/logout"
2625
)
2726

2827
func NewLogoutCommand() *cobra.Command {
29-
var logoutCommand = &cobra.Command{
28+
return &cobra.Command{
3029
Use: "logout [flags] [SERVER]",
3130
Args: cobra.MaximumNArgs(1),
3231
Short: "Log out from a container registry",
@@ -35,62 +34,33 @@ func NewLogoutCommand() *cobra.Command {
3534
SilenceUsage: true,
3635
SilenceErrors: true,
3736
}
38-
return logoutCommand
3937
}
4038

41-
// code inspired from XXX
4239
func logoutAction(cmd *cobra.Command, args []string) error {
43-
serverAddress := dockerconfigresolver.IndexServer
44-
isDefaultRegistry := true
45-
if len(args) >= 1 {
46-
serverAddress = args[0]
47-
isDefaultRegistry = false
48-
}
49-
50-
var (
51-
regsToLogout = []string{serverAddress}
52-
hostnameAddress = serverAddress
53-
)
54-
55-
if !isDefaultRegistry {
56-
hostnameAddress = dockerconfigresolver.ConvertToHostname(serverAddress)
57-
// the tries below are kept for backward compatibility where a user could have
58-
// saved the registry in one of the following format.
59-
regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress)
40+
logoutServer := ""
41+
if len(args) > 0 {
42+
logoutServer = args[0]
6043
}
6144

62-
fmt.Fprintf(cmd.OutOrStdout(), "Removing login credentials for %s\n", hostnameAddress)
63-
64-
dockerConfigFile, err := dockercliconfig.Load("")
45+
errGroup, err := logout.Logout(cmd.Context(), logoutServer)
6546
if err != nil {
66-
return err
47+
log.L.WithError(err).Errorf("Failed to erase credentials for: %s", logoutServer)
6748
}
68-
errs := make(map[string]error)
69-
for _, r := range regsToLogout {
70-
if err := dockerConfigFile.GetCredentialsStore(r).Erase(r); err != nil {
71-
errs[r] = err
49+
if errGroup != nil {
50+
log.L.Error("None of the following entries could be found")
51+
for _, v := range errGroup {
52+
log.L.Errorf("%s", v)
7253
}
7354
}
7455

75-
// if at least one removal succeeded, report success. Otherwise report errors
76-
if len(errs) == len(regsToLogout) {
77-
fmt.Fprintln(cmd.ErrOrStderr(), "WARNING: could not erase credentials:")
78-
for k, v := range errs {
79-
fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", k, v)
80-
}
81-
}
82-
83-
return nil
56+
return err
8457
}
8558

8659
func logoutShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
87-
dockerConfigFile, err := dockercliconfig.Load("")
60+
candidates, err := logout.ShellCompletion()
8861
if err != nil {
8962
return nil, cobra.ShellCompDirectiveError
9063
}
91-
candidates := []string{}
92-
for key := range dockerConfigFile.AuthConfigs {
93-
candidates = append(candidates, key)
94-
}
64+
9565
return candidates, cobra.ShellCompDirectiveNoFileComp
9666
}

pkg/cmd/login/login.go

Lines changed: 33 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ import (
2727
"os"
2828
"strings"
2929

30-
dockercliconfig "github.com/docker/cli/cli/config"
31-
dockercliconfigtypes "github.com/docker/cli/cli/config/types"
32-
"github.com/docker/docker/api/types/registry"
3330
"golang.org/x/net/context/ctxhttp"
3431
"golang.org/x/term"
3532

@@ -48,106 +45,64 @@ Configure a credential helper to remove this warning. See
4845
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
4946
`
5047

51-
type isFileStore interface {
52-
IsFileStore() bool
53-
GetFilename() string
54-
}
55-
5648
func Login(ctx context.Context, options types.LoginCommandOptions, stdout io.Writer) error {
57-
var serverAddress string
58-
if options.ServerAddress == "" || options.ServerAddress == "docker.io" || options.ServerAddress == "index.docker.io" || options.ServerAddress == "registry-1.docker.io" {
59-
serverAddress = dockerconfigresolver.IndexServer
60-
} else {
61-
serverAddress = options.ServerAddress
49+
registryURL, err := dockerconfigresolver.Parse(options.ServerAddress)
50+
if err != nil {
51+
return err
52+
}
53+
54+
credStore, err := dockerconfigresolver.NewCredentialsStore("")
55+
if err != nil {
56+
return err
6257
}
6358

6459
var responseIdentityToken string
65-
isDefaultRegistry := serverAddress == dockerconfigresolver.IndexServer
6660

67-
authConfig, err := GetDefaultAuthConfig(options.Username == "" && options.Password == "", serverAddress, isDefaultRegistry)
68-
if authConfig == nil {
69-
authConfig = &registry.AuthConfig{ServerAddress: serverAddress}
70-
}
71-
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
72-
// login With StoreCreds
73-
responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig)
61+
credentials, err := credStore.Retrieve(registryURL, options.Username == "" && options.Password == "")
62+
credentials.IdentityToken = ""
63+
64+
if err == nil && credentials.Username != "" && credentials.Password != "" {
65+
responseIdentityToken, err = loginClientSide(ctx, options.GOptions, registryURL, credentials)
7466
}
7567

76-
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
77-
err = ConfigureAuthentication(authConfig, options.Username, options.Password)
68+
if err != nil || credentials.Username == "" || credentials.Password == "" {
69+
err = configureAuthentication(credentials, options.Username, options.Password)
7870
if err != nil {
7971
return err
8072
}
8173

82-
responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig)
74+
responseIdentityToken, err = loginClientSide(ctx, options.GOptions, registryURL, credentials)
8375
if err != nil {
8476
return err
8577
}
8678
}
8779

8880
if responseIdentityToken != "" {
89-
authConfig.Password = ""
90-
authConfig.IdentityToken = responseIdentityToken
91-
}
92-
93-
dockerConfigFile, err := dockercliconfig.Load("")
94-
if err != nil {
95-
return err
81+
credentials.Password = ""
82+
credentials.IdentityToken = responseIdentityToken
9683
}
9784

98-
creds := dockerConfigFile.GetCredentialsStore(serverAddress)
99-
100-
store, isFile := creds.(isFileStore)
10185
// Display a warning if we're storing the users password (not a token) and credentials store type is file.
102-
if isFile && authConfig.Password != "" {
103-
_, err = fmt.Fprintln(stdout, fmt.Sprintf(unencryptedPasswordWarning, store.GetFilename()))
86+
storageFileLocation := credStore.FileStorageLocation(registryURL)
87+
if storageFileLocation != "" && credentials.Password != "" {
88+
_, err = fmt.Fprintln(stdout, fmt.Sprintf(unencryptedPasswordWarning, storageFileLocation))
10489
if err != nil {
10590
return err
10691
}
10792
}
10893

109-
if err := creds.Store(dockercliconfigtypes.AuthConfig(*(authConfig))); err != nil {
94+
err = credStore.Store(registryURL, credentials)
95+
if err != nil {
11096
return fmt.Errorf("error saving credentials: %w", err)
11197
}
11298

113-
fmt.Fprintln(stdout, "Login Succeeded")
114-
115-
return nil
116-
}
99+
_, err = fmt.Fprintln(stdout, "Login Succeeded")
117100

118-
// GetDefaultAuthConfig gets the default auth config given a serverAddress.
119-
// If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it.
120-
// Code from github.com/docker/cli/cli/command (v20.10.3).
121-
func GetDefaultAuthConfig(checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*registry.AuthConfig, error) {
122-
if !isDefaultRegistry {
123-
var err error
124-
serverAddress, err = convertToHostname(serverAddress)
125-
if err != nil {
126-
return nil, err
127-
}
128-
}
129-
authconfig := dockercliconfigtypes.AuthConfig{}
130-
if checkCredStore {
131-
dockerConfigFile, err := dockercliconfig.Load("")
132-
if err != nil {
133-
return nil, err
134-
}
135-
authconfig, err = dockerConfigFile.GetAuthConfig(serverAddress)
136-
if err != nil {
137-
return nil, err
138-
}
139-
}
140-
authconfig.ServerAddress = serverAddress
141-
authconfig.IdentityToken = ""
142-
res := registry.AuthConfig(authconfig)
143-
return &res, nil
101+
return err
144102
}
145103

146-
func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptions, auth registry.AuthConfig) (string, error) {
147-
host, err := convertToHostname(auth.ServerAddress)
148-
if err != nil {
149-
return "", err
150-
}
104+
func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptions, registryURL *dockerconfigresolver.RegistryURL, credentials *dockerconfigresolver.Credentials) (string, error) {
105+
host := registryURL.Host
151106
var dOpts []dockerconfigresolver.Opt
152107
if globalOptions.InsecureRegistry {
153108
log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", host)
@@ -157,12 +112,12 @@ func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptio
157112

158113
authCreds := func(acArg string) (string, string, error) {
159114
if acArg == host {
160-
if auth.RegistryToken != "" {
115+
if credentials.RegistryToken != "" {
161116
// Even containerd/CRI does not support RegistryToken as of v1.4.3,
162117
// so, nobody is actually using RegistryToken?
163118
log.G(ctx).Warnf("RegistryToken (for %q) is not supported yet (FIXME)", host)
164119
}
165-
return auth.Username, auth.Password, nil
120+
return credentials.Username, credentials.Password, nil
166121
}
167122
return "", "", fmt.Errorf("expected acArg to be %q, got %q", host, acArg)
168123
}
@@ -251,10 +206,9 @@ func tryLoginWithRegHost(ctx context.Context, rh docker.RegistryHost) error {
251206
return errors.New("too many 401 (probably)")
252207
}
253208

254-
func ConfigureAuthentication(authConfig *registry.AuthConfig, username, password string) error {
255-
authConfig.Username = strings.TrimSpace(authConfig.Username)
209+
func configureAuthentication(credentials *dockerconfigresolver.Credentials, username, password string) error {
256210
if username = strings.TrimSpace(username); username == "" {
257-
username = authConfig.Username
211+
username = credentials.Username
258212
}
259213
if username == "" {
260214
fmt.Print("Enter Username: ")
@@ -281,8 +235,8 @@ func ConfigureAuthentication(authConfig *registry.AuthConfig, username, password
281235
return fmt.Errorf("error: Password is Required")
282236
}
283237

284-
authConfig.Username = username
285-
authConfig.Password = password
238+
credentials.Username = username
239+
credentials.Password = password
286240

287241
return nil
288242
}
@@ -304,22 +258,3 @@ func readUsername() (string, error) {
304258

305259
return username, nil
306260
}
307-
308-
func convertToHostname(serverAddress string) (string, error) {
309-
// Ensure that URL contains scheme for a good parsing process
310-
if strings.Contains(serverAddress, "://") {
311-
u, err := url.Parse(serverAddress)
312-
if err != nil {
313-
return "", err
314-
}
315-
serverAddress = u.Host
316-
} else {
317-
u, err := url.Parse("https://" + serverAddress)
318-
if err != nil {
319-
return "", err
320-
}
321-
serverAddress = u.Host
322-
}
323-
324-
return serverAddress, nil
325-
}

pkg/cmd/logout/logout.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package logout
18+
19+
import (
20+
"context"
21+
22+
"github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
23+
)
24+
25+
func Logout(ctx context.Context, logoutServer string) (map[string]error, error) {
26+
reg, err := dockerconfigresolver.Parse(logoutServer)
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
credentialsStore, err := dockerconfigresolver.NewCredentialsStore("")
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
return credentialsStore.Erase(reg)
37+
}
38+
39+
func ShellCompletion() ([]string, error) {
40+
credentialsStore, err := dockerconfigresolver.NewCredentialsStore("")
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
return credentialsStore.ShellCompletion(), nil
46+
}

0 commit comments

Comments
 (0)