diff --git a/README.md b/README.md index 6360a51..efb1a5b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Credential Management for [oras-go](https://github.com/oras-project/oras-go) -> **Warning** This project is currently under initial development. APIs may and will be changed incompatibly from one commit to another. - [![Build Status](https://github.com/oras-project/oras-credentials-go/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/oras-project/oras-credentials-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain) [![codecov](https://codecov.io/gh/oras-project/oras-credentials-go/branch/main/graph/badge.svg)](https://codecov.io/gh/oras-project/oras-credentials-go) [![Go Report Card](https://goreportcard.com/badge/github.com/oras-project/oras-credentials-go)](https://goreportcard.com/report/github.com/oras-project/oras-credentials-go) @@ -13,7 +11,10 @@ `oras-credentials-go` is a credential management library designed for [`oras-go`](https://github.com/oras-project/oras-go). It supports reading, saving, and removing credentials from Docker configuration files and external credential stores that follow the [Docker credential helper protocol](https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol). -Once it reaches a fairly stable version (e.g. `v1.0.0-rc.1`), `oras-credentials-go` will be merged into `oras-go` (See [discussion](https://github.com/oras-project/oras-credentials-go/discussions/80)). After that, this repository will be archived. +> [!IMPORTANT] +> The APIs previously located in this library have been moved to [`oras-go`](https://github.com/oras-project/oras-go). As a result, these APIs are now deprecated and users should use [the packages](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials) in `oras-go` instead. +> +> This repository will now be used for developing experimental features scoped to credentials management. If any of these features are deemed stable and applicable to `oras-go`, they may be moved there in the future. ## Versioning diff --git a/file_store.go b/file_store.go index 40e4192..f710e58 100644 --- a/file_store.go +++ b/file_store.go @@ -16,82 +16,43 @@ limitations under the License. package credentials import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/oras-project/oras-credentials-go/internal/config" - "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/credentials" ) // FileStore implements a credentials store using the docker configuration file // to keep the credentials in plain-text. // // Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties -type FileStore struct { - // DisablePut disables putting credentials in plaintext. - // If DisablePut is set to true, Put() will return ErrPlaintextPutDisabled. - DisablePut bool - - config *config.Config -} +// +// Deprecated: This type is now simply [credentials.FileStore] of oras-go. +// +// [credentials.FileStore]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#FileStore +type FileStore = credentials.FileStore var ( // ErrPlaintextPutDisabled is returned by Put() when DisablePut is set // to true. - ErrPlaintextPutDisabled = errors.New("putting plaintext credentials is disabled") + // + // Deprecated: This type is now simply [credentials.ErrPlaintextPutDisabled] of oras-go. + // + // [credentials.ErrPlaintextPutDisabled]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#ErrPlaintextPutDisabled + ErrPlaintextPutDisabled = credentials.ErrPlaintextPutDisabled // ErrBadCredentialFormat is returned by Put() when the credential format // is bad. - ErrBadCredentialFormat = errors.New("bad credential format") + // + // Deprecated: This type is now simply [credentials.ErrBadCredentialFormat] of oras-go. + // + // [credentials.ErrBadCredentialFormat]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#ErrBadCredentialFormat + ErrBadCredentialFormat = credentials.ErrBadCredentialFormat ) // NewFileStore creates a new file credentials store. // // Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties +// +// Deprecated: This funciton now simply calls [credentials.NewFileStore] of oras-go. +// +// [credentials.NewFileStore]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#NewFileStore func NewFileStore(configPath string) (*FileStore, error) { - cfg, err := config.Load(configPath) - if err != nil { - return nil, err - } - return newFileStore(cfg), nil -} - -// newFileStore creates a file credentials store based on the given config instance. -func newFileStore(cfg *config.Config) *FileStore { - return &FileStore{config: cfg} -} - -// Get retrieves credentials from the store for the given server address. -func (fs *FileStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) { - return fs.config.GetCredential(serverAddress) -} - -// Put saves credentials into the store for the given server address. -// Returns ErrPlaintextPutDisabled if fs.DisablePut is set to true. -func (fs *FileStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error { - if fs.DisablePut { - return ErrPlaintextPutDisabled - } - if err := validateCredentialFormat(cred); err != nil { - return err - } - - return fs.config.PutCredential(serverAddress, cred) -} - -// Delete removes credentials from the store for the given server address. -func (fs *FileStore) Delete(_ context.Context, serverAddress string) error { - return fs.config.DeleteCredential(serverAddress) -} - -// validateCredentialFormat validates the format of cred. -func validateCredentialFormat(cred auth.Credential) error { - if strings.ContainsRune(cred.Username, ':') { - // Username and password will be encoded in the base64(username:password) - // format in the file. The decoded result will be wrong if username - // contains colon(s). - return fmt.Errorf("%w: colons(:) are not allowed in username", ErrBadCredentialFormat) - } - return nil + return credentials.NewFileStore(configPath) } diff --git a/file_store_test.go b/file_store_test.go index a4b78b5..52b0078 100644 --- a/file_store_test.go +++ b/file_store_test.go @@ -877,34 +877,3 @@ func TestFileStore_Delete_notExistConfig(t *testing.T) { t.Errorf("Stat(%s) error = %v, wantErr %v", configPath, err, wantErr) } } - -func Test_validateCredentialFormat(t *testing.T) { - tests := []struct { - name string - cred auth.Credential - wantErr error - }{ - { - name: "Username contains colon", - cred: auth.Credential{ - Username: "x:y", - Password: "z", - }, - wantErr: ErrBadCredentialFormat, - }, - { - name: "Password contains colon", - cred: auth.Credential{ - Username: "x", - Password: "y:z", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := validateCredentialFormat(tt.cred); !errors.Is(err, tt.wantErr) { - t.Errorf("validateCredentialFormat() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/go.mod b/go.mod index 3b9230a..8d51343 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/oras-project/oras-credentials-go go 1.19 -require oras.land/oras-go/v2 v2.3.0 +require oras.land/oras-go/v2 v2.3.1-0.20230925030825-cb8c8bc3075c require ( github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/go.sum b/go.sum index 25b71d2..8d8baf1 100644 --- a/go.sum +++ b/go.sum @@ -4,5 +4,5 @@ github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYB github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -oras.land/oras-go/v2 v2.3.0 h1:lqX1aXdN+DAmDTKjiDyvq85cIaI4RkIKp/PghWlAGIU= -oras.land/oras-go/v2 v2.3.0/go.mod h1:GeAwLuC4G/JpNwkd+bSZ6SkDMGaaYglt6YK2WvZP7uQ= +oras.land/oras-go/v2 v2.3.1-0.20230925030825-cb8c8bc3075c h1:znOn5Gb/vfBI/rvruoeqUHyJEfi9woL6r/IvqnFpqRQ= +oras.land/oras-go/v2 v2.3.1-0.20230925030825-cb8c8bc3075c/go.mod h1:LTHGmKO431CaOWkqgadzYq0sEixlO+EN/zShuWvT1Yg= diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index b00eff3..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,302 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package config - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/oras-project/oras-credentials-go/internal/ioutil" - "oras.land/oras-go/v2/registry/remote/auth" -) - -const ( - // configFieldAuths is the "auths" field in the config file. - // Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L19 - configFieldAuths = "auths" - // configFieldCredentialsStore is the "credsStore" field in the config file. - configFieldCredentialsStore = "credsStore" - // configFieldCredentialHelpers is the "credHelpers" field in the config file. - configFieldCredentialHelpers = "credHelpers" -) - -// ErrInvalidConfigFormat is returned when the config format is invalid. -var ErrInvalidConfigFormat = errors.New("invalid config format") - -// AuthConfig contains authorization information for connecting to a Registry. -// References: -// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L45 -// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/types/authconfig.go#L3-L22 -type AuthConfig struct { - // Auth is a base64-encoded string of "{username}:{password}". - Auth string `json:"auth,omitempty"` - // IdentityToken is used to authenticate the user and get an access token - // for the registry. - IdentityToken string `json:"identitytoken,omitempty"` - // RegistryToken is a bearer token to be sent to a registry. - RegistryToken string `json:"registrytoken,omitempty"` - - Username string `json:"username,omitempty"` // legacy field for compatibility - Password string `json:"password,omitempty"` // legacy field for compatibility -} - -// NewAuthConfig creates an authConfig based on cred. -func NewAuthConfig(cred auth.Credential) AuthConfig { - return AuthConfig{ - Auth: encodeAuth(cred.Username, cred.Password), - IdentityToken: cred.RefreshToken, - RegistryToken: cred.AccessToken, - } -} - -// Credential returns an auth.Credential based on ac. -func (ac AuthConfig) Credential() (auth.Credential, error) { - cred := auth.Credential{ - Username: ac.Username, - Password: ac.Password, - RefreshToken: ac.IdentityToken, - AccessToken: ac.RegistryToken, - } - if ac.Auth != "" { - var err error - // override username and password - cred.Username, cred.Password, err = decodeAuth(ac.Auth) - if err != nil { - return auth.EmptyCredential, fmt.Errorf("failed to decode auth field: %w: %v", ErrInvalidConfigFormat, err) - } - } - return cred, nil -} - -// Config represents a docker configuration file. -// References: -// - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties -// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L44 -type Config struct { - // path is the path to the config file. - path string - // rwLock is a read-write-lock for the file store. - rwLock sync.RWMutex - // content is the content of the config file. - // Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L44 - content map[string]json.RawMessage - // authsCache is a cache of the auths field of the config. - // Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L19 - authsCache map[string]json.RawMessage - // credentialsStore is the credsStore field of the config. - // Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L28 - credentialsStore string - // credentialHelpers is the credHelpers field of the config. - // Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L29 - credentialHelpers map[string]string -} - -// Load loads Config from the given config path. -func Load(configPath string) (*Config, error) { - cfg := &Config{path: configPath} - configFile, err := os.Open(configPath) - if err != nil { - if os.IsNotExist(err) { - // init content and caches if the content file does not exist - cfg.content = make(map[string]json.RawMessage) - cfg.authsCache = make(map[string]json.RawMessage) - return cfg, nil - } - return nil, fmt.Errorf("failed to open config file at %s: %w", configPath, err) - } - defer configFile.Close() - - // decode config content if the config file exists - if err := json.NewDecoder(configFile).Decode(&cfg.content); err != nil { - return nil, fmt.Errorf("failed to decode config file at %s: %w: %v", configPath, ErrInvalidConfigFormat, err) - } - - if credsStoreBytes, ok := cfg.content[configFieldCredentialsStore]; ok { - if err := json.Unmarshal(credsStoreBytes, &cfg.credentialsStore); err != nil { - return nil, fmt.Errorf("failed to unmarshal creds store field: %w: %v", ErrInvalidConfigFormat, err) - } - } - - if credHelpersBytes, ok := cfg.content[configFieldCredentialHelpers]; ok { - if err := json.Unmarshal(credHelpersBytes, &cfg.credentialHelpers); err != nil { - return nil, fmt.Errorf("failed to unmarshal cred helpers field: %w: %v", ErrInvalidConfigFormat, err) - } - } - - if authsBytes, ok := cfg.content[configFieldAuths]; ok { - if err := json.Unmarshal(authsBytes, &cfg.authsCache); err != nil { - return nil, fmt.Errorf("failed to unmarshal auths field: %w: %v", ErrInvalidConfigFormat, err) - } - } - if cfg.authsCache == nil { - cfg.authsCache = make(map[string]json.RawMessage) - } - - return cfg, nil -} - -// GetAuthConfig returns an auth.Credential for serverAddress. -func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error) { - cfg.rwLock.RLock() - defer cfg.rwLock.RUnlock() - - authCfgBytes, ok := cfg.authsCache[serverAddress] - if !ok { - return auth.EmptyCredential, nil - } - var authCfg AuthConfig - if err := json.Unmarshal(authCfgBytes, &authCfg); err != nil { - return auth.EmptyCredential, fmt.Errorf("failed to unmarshal auth field: %w: %v", ErrInvalidConfigFormat, err) - } - return authCfg.Credential() -} - -// PutAuthConfig puts cred for serverAddress. -func (cfg *Config) PutCredential(serverAddress string, cred auth.Credential) error { - cfg.rwLock.Lock() - defer cfg.rwLock.Unlock() - - authCfg := NewAuthConfig(cred) - authCfgBytes, err := json.Marshal(authCfg) - if err != nil { - return fmt.Errorf("failed to marshal auth field: %w", err) - } - cfg.authsCache[serverAddress] = authCfgBytes - return cfg.saveFile() -} - -// DeleteAuthConfig deletes the corresponding credential for serverAddress. -func (cfg *Config) DeleteCredential(serverAddress string) error { - cfg.rwLock.Lock() - defer cfg.rwLock.Unlock() - - if _, ok := cfg.authsCache[serverAddress]; !ok { - // no ops - return nil - } - delete(cfg.authsCache, serverAddress) - return cfg.saveFile() -} - -// GetCredentialHelper returns the credential helpers for serverAddress. -func (cfg *Config) GetCredentialHelper(serverAddress string) string { - return cfg.credentialHelpers[serverAddress] -} - -// CredentialsStore returns the configured credentials store. -func (cfg *Config) CredentialsStore() string { - cfg.rwLock.RLock() - defer cfg.rwLock.RUnlock() - - return cfg.credentialsStore -} - -// SetCredentialsStore puts the configured credentials store. -func (cfg *Config) SetCredentialsStore(credsStore string) error { - cfg.rwLock.Lock() - defer cfg.rwLock.Unlock() - - cfg.credentialsStore = credsStore - return cfg.saveFile() -} - -// IsAuthConfigured returns whether there is authentication configured in this -// config file or not. -func (cfg *Config) IsAuthConfigured() bool { - return cfg.credentialsStore != "" || - len(cfg.credentialHelpers) > 0 || - len(cfg.authsCache) > 0 -} - -// saveFile saves Config into the file. -func (cfg *Config) saveFile() (returnErr error) { - // marshal content - // credentialHelpers is skipped as it's never set - if cfg.credentialsStore != "" { - credsStoreBytes, err := json.Marshal(cfg.credentialsStore) - if err != nil { - return fmt.Errorf("failed to marshal creds store: %w", err) - } - cfg.content[configFieldCredentialsStore] = credsStoreBytes - } else { - // omit empty - delete(cfg.content, configFieldCredentialsStore) - } - authsBytes, err := json.Marshal(cfg.authsCache) - if err != nil { - return fmt.Errorf("failed to marshal credentials: %w", err) - } - cfg.content[configFieldAuths] = authsBytes - jsonBytes, err := json.MarshalIndent(cfg.content, "", "\t") - if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) - } - - // write the content to a ingest file for atomicity - configDir := filepath.Dir(cfg.path) - if err := os.MkdirAll(configDir, 0700); err != nil { - return fmt.Errorf("failed to make directory %s: %w", configDir, err) - } - ingest, err := ioutil.Ingest(configDir, bytes.NewReader(jsonBytes)) - if err != nil { - return fmt.Errorf("failed to save config file: %w", err) - } - defer func() { - if returnErr != nil { - // clean up the ingest file in case of error - os.Remove(ingest) - } - }() - - // overwrite the config file - if err := os.Rename(ingest, cfg.path); err != nil { - return fmt.Errorf("failed to save config file: %w", err) - } - return nil -} - -// encodeAuth base64-encodes username and password into base64(username:password). -func encodeAuth(username, password string) string { - if username == "" && password == "" { - return "" - } - return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) -} - -// decodeAuth decodes a base64 encoded string and returns username and password. -func decodeAuth(authStr string) (username string, password string, err error) { - if authStr == "" { - return "", "", nil - } - - decoded, err := base64.StdEncoding.DecodeString(authStr) - if err != nil { - return "", "", err - } - decodedStr := string(decoded) - username, password, ok := strings.Cut(decodedStr, ":") - if !ok { - return "", "", fmt.Errorf("auth '%s' does not conform the base64(username:password) format", decodedStr) - } - return username, password, nil -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index e49a2e7..0000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,1316 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package config - -import ( - "encoding/json" - "errors" - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/oras-project/oras-credentials-go/internal/config/configtest" - "oras.land/oras-go/v2/registry/remote/auth" -) - -func TestLoad_badPath(t *testing.T) { - tempDir := t.TempDir() - - tests := []struct { - name string - configPath string - wantErr bool - }{ - { - name: "Path is a directory", - configPath: tempDir, - wantErr: true, - }, - { - name: "Empty file name", - configPath: filepath.Join(tempDir, ""), - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := Load(tt.configPath) - if (err != nil) != tt.wantErr { - t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } -} - -func TestLoad_badFormat(t *testing.T) { - tests := []struct { - name string - configPath string - wantErr bool - }{ - { - name: "Bad JSON format", - configPath: "../../testdata/bad_config", - wantErr: true, - }, - { - name: "Invalid auths format", - configPath: "../../testdata/invalid_auths_config.json", - wantErr: true, - }, - { - name: "No auths field", - configPath: "../../testdata/no_auths_config.json", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := Load(tt.configPath) - if (err != nil) != tt.wantErr { - t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } -} - -func TestConfig_GetCredential_validConfig(t *testing.T) { - cfg, err := Load("../../testdata/valid_auths_config.json") - if err != nil { - t.Fatal("Load() error =", err) - } - - tests := []struct { - name string - serverAddress string - want auth.Credential - wantErr bool - }{ - { - name: "Username and password", - serverAddress: "registry1.example.com", - want: auth.Credential{ - Username: "username", - Password: "password", - }, - }, - { - name: "Identity token", - serverAddress: "registry2.example.com", - want: auth.Credential{ - RefreshToken: "identity_token", - }, - }, - { - name: "Registry token", - serverAddress: "registry3.example.com", - want: auth.Credential{ - AccessToken: "registry_token", - }, - }, - { - name: "Username and password, identity token and registry token", - serverAddress: "registry4.example.com", - want: auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "identity_token", - AccessToken: "registry_token", - }, - }, - { - name: "Empty credential", - serverAddress: "registry5.example.com", - want: auth.EmptyCredential, - }, - { - name: "Username and password, no auth", - serverAddress: "registry6.example.com", - want: auth.Credential{ - Username: "username", - Password: "password", - }, - }, - { - name: "Auth overriding Username and password", - serverAddress: "registry7.example.com", - want: auth.Credential{ - Username: "username", - Password: "password", - }, - }, - { - name: "Not in auths", - serverAddress: "foo.example.com", - want: auth.EmptyCredential, - }, - { - name: "No record", - serverAddress: "registry999.example.com", - want: auth.EmptyCredential, - }, - } - 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 { - t.Fatal("Load() error =", err) - } - - tests := []struct { - name string - serverAddress string - want auth.Credential - wantErr bool - }{ - { - name: "Invalid auth encode", - serverAddress: "registry1.example.com", - want: auth.EmptyCredential, - wantErr: true, - }, - { - name: "Invalid auths format", - serverAddress: "registry2.example.com", - want: auth.EmptyCredential, - wantErr: true, - }, - { - name: "Invalid type", - serverAddress: "registry3.example.com", - want: auth.EmptyCredential, - wantErr: true, - }, - } - 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_emptyConfig(t *testing.T) { - cfg, err := Load("../../testdata/empty_config.json") - if err != nil { - t.Fatal("Load() error =", err) - } - - tests := []struct { - name string - serverAddress string - want auth.Credential - wantErr error - }{ - { - name: "Not found", - serverAddress: "registry.example.com", - want: auth.EmptyCredential, - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) - if !errors.Is(err, 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_notExistConfig(t *testing.T) { - cfg, err := Load("whatever") - if err != nil { - t.Fatal("Load() error =", err) - } - - tests := []struct { - name string - serverAddress string - want auth.Credential - wantErr error - }{ - { - name: "Not found", - serverAddress: "registry.example.com", - want: auth.EmptyCredential, - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) - if !errors.Is(err, 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_PutCredential_notExistConfig(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - - server := "test.example.com" - cred := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", - } - - // test put - if err := cfg.PutCredential(server, cred); err != nil { - t.Fatalf("Config.PutCredential() error = %v", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - - var testCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&testCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - want := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server: { - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: "refresh_token", - RegistryToken: "access_token", - }, - }, - } - if !reflect.DeepEqual(testCfg, want) { - t.Errorf("Decoded config = %v, want %v", testCfg, want) - } - - // verify get - got, err := cfg.GetCredential(server) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, want) - } -} - -func TestConfig_PutCredential_addNew(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - // prepare test content - server1 := "registry1.example.com" - cred1 := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", - } - - testCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server1: { - SomeAuthField: "whatever", - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred1.RefreshToken, - RegistryToken: cred1.AccessToken, - }, - }, - SomeConfigField: 123, - } - jsonStr, err := json.Marshal(testCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - - // test put - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - server2 := "registry2.example.com" - cred2 := auth.Credential{ - Username: "username_2", - Password: "password_2", - RefreshToken: "refresh_token_2", - AccessToken: "access_token_2", - } - if err := cfg.PutCredential(server2, cred2); err != nil { - t.Fatalf("Config.PutCredential() error = %v", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - var gotCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - wantTestCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server1: { - SomeAuthField: "whatever", - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred1.RefreshToken, - RegistryToken: cred1.AccessToken, - }, - server2: { - Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y", - IdentityToken: "refresh_token_2", - RegistryToken: "access_token_2", - }, - }, - SomeConfigField: testCfg.SomeConfigField, - } - if !reflect.DeepEqual(gotCfg, wantTestCfg) { - t.Errorf("Decoded config = %v, want %v", gotCfg, wantTestCfg) - } - - // verify get - got, err := cfg.GetCredential(server1) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred1; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server1, got, want) - } - - got, err = cfg.GetCredential(server2) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred2; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server2, got, want) - } -} - -func TestConfig_PutCredential_updateOld(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - - // prepare test content - server := "registry.example.com" - testCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server: { - SomeAuthField: "whatever", - Username: "foo", - Password: "bar", - IdentityToken: "refresh_token", - }, - }, - SomeConfigField: 123, - } - jsonStr, err := json.Marshal(testCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - - // test put - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - cred := auth.Credential{ - Username: "username", - Password: "password", - AccessToken: "access_token", - } - if err := cfg.PutCredential(server, cred); err != nil { - t.Fatalf("Config.PutCredential() error = %v", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - var gotCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - wantCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server: { - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - RegistryToken: "access_token", - }, - }, - SomeConfigField: testCfg.SomeConfigField, - } - if !reflect.DeepEqual(gotCfg, wantCfg) { - t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg) - } - - // verify get - got, err := cfg.GetCredential(server) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) - } -} - -func TestConfig_DeleteCredential(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - - // prepare test content - server1 := "registry1.example.com" - cred1 := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", - } - server2 := "registry2.example.com" - cred2 := auth.Credential{ - Username: "username_2", - Password: "password_2", - RefreshToken: "refresh_token_2", - AccessToken: "access_token_2", - } - - testCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server1: { - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred1.RefreshToken, - RegistryToken: cred1.AccessToken, - }, - server2: { - Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y", - IdentityToken: "refresh_token_2", - RegistryToken: "access_token_2", - }, - }, - SomeConfigField: 123, - } - jsonStr, err := json.Marshal(testCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - // test get - got, err := cfg.GetCredential(server1) - if err != nil { - t.Fatalf("FileStore.GetCredential() error = %v", err) - } - if want := cred1; !reflect.DeepEqual(got, want) { - t.Errorf("FileStore.GetCredential(%s) = %v, want %v", server1, got, want) - } - got, err = cfg.GetCredential(server2) - if err != nil { - t.Fatalf("FileStore.GetCredential() error = %v", err) - } - if want := cred2; !reflect.DeepEqual(got, want) { - t.Errorf("FileStore.Get(%s) = %v, want %v", server2, got, want) - } - - // test delete - if err := cfg.DeleteCredential(server1); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - var gotTestCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotTestCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - wantTestCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server2: testCfg.AuthConfigs[server2], - }, - SomeConfigField: testCfg.SomeConfigField, - } - if !reflect.DeepEqual(gotTestCfg, wantTestCfg) { - t.Errorf("Decoded config = %v, want %v", gotTestCfg, wantTestCfg) - } - - // test get again - got, err = cfg.GetCredential(server1) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server1, got, want) - } - got, err = cfg.GetCredential(server2) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred2; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server2, got, want) - } -} - -func TestConfig_DeleteCredential_lastConfig(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - - // prepare test content - server := "registry1.example.com" - cred := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", - } - - testCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server: { - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred.RefreshToken, - RegistryToken: cred.AccessToken, - }, - }, - SomeConfigField: 123, - } - jsonStr, err := json.Marshal(testCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - // test get - got, err := cfg.GetCredential(server) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) - } - - // test delete - if err := cfg.DeleteCredential(server); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - var gotTestCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotTestCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - wantTestCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{}, - SomeConfigField: testCfg.SomeConfigField, - } - if !reflect.DeepEqual(gotTestCfg, wantTestCfg) { - t.Errorf("Decoded config = %v, want %v", gotTestCfg, wantTestCfg) - } - - // test get again - got, err = cfg.GetCredential(server) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) - } -} - -func TestConfig_DeleteCredential_notExistRecord(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - - // prepare test content - server := "registry1.example.com" - cred := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", - } - testCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server: { - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred.RefreshToken, - RegistryToken: cred.AccessToken, - }, - }, - SomeConfigField: 123, - } - jsonStr, err := json.Marshal(testCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - // test get - got, err := cfg.GetCredential(server) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) - } - - // test delete - if err := cfg.DeleteCredential("test.example.com"); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - var gotTestCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotTestCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - wantTestCfg := configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - server: testCfg.AuthConfigs[server], - }, - SomeConfigField: testCfg.SomeConfigField, - } - if !reflect.DeepEqual(gotTestCfg, wantTestCfg) { - t.Errorf("Decoded config = %v, want %v", gotTestCfg, wantTestCfg) - } - - // test get again - got, err = cfg.GetCredential(server) - if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) - } - if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) - } -} - -func TestConfig_DeleteCredential_notExistConfig(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - - server := "test.example.com" - // test delete - if err := cfg.DeleteCredential(server); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) - } - - // verify config file is not created - _, err = os.Stat(configPath) - if wantErr := os.ErrNotExist; !errors.Is(err, wantErr) { - t.Errorf("Stat(%s) error = %v, wantErr %v", configPath, err, wantErr) - } -} - -func TestConfig_GetCredentialHelper(t *testing.T) { - cfg, err := Load("../../testdata/credHelpers_config.json") - if err != nil { - t.Fatal("Load() error =", err) - } - - tests := []struct { - name string - serverAddress string - want string - }{ - { - name: "Get cred helper: registry_helper1", - serverAddress: "registry1.example.com", - want: "registry1-helper", - }, - { - name: "Get cred helper: registry_helper2", - serverAddress: "registry2.example.com", - want: "registry2-helper", - }, - { - name: "Empty cred helper configured", - serverAddress: "registry3.example.com", - want: "", - }, - { - name: "No cred helper configured", - serverAddress: "whatever.example.com", - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := cfg.GetCredentialHelper(tt.serverAddress); got != tt.want { - t.Errorf("Config.GetCredentialHelper() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConfig_CredentialsStore(t *testing.T) { - tests := []struct { - name string - configPath string - want string - }{ - { - name: "creds store configured", - configPath: "../../testdata/credsStore_config.json", - want: "teststore", - }, - { - name: "No creds store configured", - configPath: "../../testdata/credsHelpers_config.json", - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg, err := Load(tt.configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - if got := cfg.CredentialsStore(); got != tt.want { - t.Errorf("Config.CredentialsStore() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConfig_SetCredentialsStore(t *testing.T) { - // prepare test content - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.json") - testCfg := configtest.Config{ - SomeConfigField: 123, - } - jsonStr, err := json.Marshal(testCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - - // test SetCredentialsStore - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - credsStore := "testStore" - if err := cfg.SetCredentialsStore(credsStore); err != nil { - t.Fatal("Config.SetCredentialsStore() error =", err) - } - - // verify - if got := cfg.credentialsStore; got != credsStore { - t.Errorf("Config.credentialsStore = %v, want %v", got, credsStore) - } - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - var gotTestCfg1 configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotTestCfg1); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - if err := configFile.Close(); err != nil { - t.Fatal("failed to close config file:", err) - } - - wantTestCfg1 := configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - CredentialsStore: credsStore, - SomeConfigField: testCfg.SomeConfigField, - } - if !reflect.DeepEqual(gotTestCfg1, wantTestCfg1) { - t.Errorf("Decoded config = %v, want %v", gotTestCfg1, wantTestCfg1) - } - - // test SetCredentialsStore: set as empty - if err := cfg.SetCredentialsStore(""); err != nil { - t.Fatal("Config.SetCredentialsStore() error =", err) - } - // verify - if got := cfg.credentialsStore; got != "" { - t.Errorf("Config.credentialsStore = %v, want empty", got) - } - // verify config file - configFile, err = os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - var gotTestCfg2 configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotTestCfg2); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - if err := configFile.Close(); err != nil { - t.Fatal("failed to close config file:", err) - } - - wantTestCfg2 := configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - SomeConfigField: testCfg.SomeConfigField, - } - if !reflect.DeepEqual(gotTestCfg2, wantTestCfg2) { - t.Errorf("Decoded config = %v, want %v", gotTestCfg2, wantTestCfg2) - } -} - -func TestConfig_IsAuthConfigured(t *testing.T) { - tempDir := t.TempDir() - - tests := []struct { - name string - fileName string - shouldCreateFile bool - cfg configtest.Config - want bool - }{ - { - name: "not existing file", - fileName: "config.json", - shouldCreateFile: false, - cfg: configtest.Config{}, - want: false, - }, - { - name: "no auth", - fileName: "config.json", - shouldCreateFile: true, - cfg: configtest.Config{ - SomeConfigField: 123, - }, - want: false, - }, - { - name: "empty auths exist", - fileName: "empty_auths.json", - shouldCreateFile: true, - cfg: configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{}, - }, - want: false, - }, - { - name: "auths exist, but no credential", - fileName: "no_cred_auths.json", - shouldCreateFile: true, - cfg: configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - "test.example.com": {}, - }, - }, - want: true, - }, - { - name: "auths exist", - fileName: "auths.json", - shouldCreateFile: true, - cfg: configtest.Config{ - AuthConfigs: map[string]configtest.AuthConfig{ - "test.example.com": { - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - }, - }, - }, - want: true, - }, - { - name: "credsStore exists", - fileName: "credsStore.json", - shouldCreateFile: true, - cfg: configtest.Config{ - CredentialsStore: "teststore", - }, - want: true, - }, - { - name: "empty credHelpers exist", - fileName: "empty_credsStore.json", - shouldCreateFile: true, - cfg: configtest.Config{ - CredentialHelpers: map[string]string{}, - }, - want: false, - }, - { - name: "credHelpers exist", - fileName: "credsStore.json", - shouldCreateFile: true, - cfg: configtest.Config{ - CredentialHelpers: map[string]string{ - "test.example.com": "testhelper", - }, - }, - want: true, - }, - { - name: "all exist", - fileName: "credsStore.json", - shouldCreateFile: true, - cfg: configtest.Config{ - SomeConfigField: 123, - AuthConfigs: map[string]configtest.AuthConfig{ - "test.example.com": {}, - }, - CredentialsStore: "teststore", - CredentialHelpers: map[string]string{ - "test.example.com": "testhelper", - }, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // prepare test content - configPath := filepath.Join(tempDir, tt.fileName) - if tt.shouldCreateFile { - jsonStr, err := json.Marshal(tt.cfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - } - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - if got := cfg.IsAuthConfigured(); got != tt.want { - t.Errorf("IsAuthConfigured() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConfig_saveFile(t *testing.T) { - tempDir := t.TempDir() - tests := []struct { - name string - fileName string - shouldCreateFile bool - oldCfg configtest.Config - newCfg configtest.Config - wantCfg configtest.Config - }{ - { - name: "set credsStore in a non-existing file", - fileName: "config.json", - oldCfg: configtest.Config{}, - newCfg: configtest.Config{ - CredentialsStore: "teststore", - }, - wantCfg: configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - CredentialsStore: "teststore", - }, - shouldCreateFile: false, - }, - { - name: "set credsStore in empty file", - fileName: "empty.json", - oldCfg: configtest.Config{}, - newCfg: configtest.Config{ - CredentialsStore: "teststore", - }, - wantCfg: configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - CredentialsStore: "teststore", - }, - shouldCreateFile: true, - }, - { - name: "set credsStore in a no-auth-configured file", - fileName: "empty.json", - oldCfg: configtest.Config{ - SomeConfigField: 123, - }, - newCfg: configtest.Config{ - CredentialsStore: "teststore", - }, - wantCfg: configtest.Config{ - SomeConfigField: 123, - AuthConfigs: make(map[string]configtest.AuthConfig), - CredentialsStore: "teststore", - }, - shouldCreateFile: true, - }, - { - name: "Set credsStore and credHelpers in an auth-configured file", - fileName: "auth_configured.json", - oldCfg: configtest.Config{ - SomeConfigField: 123, - AuthConfigs: map[string]configtest.AuthConfig{ - "registry1.example.com": { - SomeAuthField: "something", - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - }, - }, - CredentialsStore: "oldstore", - CredentialHelpers: map[string]string{ - "registry2.example.com": "testhelper", - }, - }, - newCfg: configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - SomeConfigField: 123, - CredentialsStore: "newstore", - CredentialHelpers: map[string]string{ - "xxx": "yyy", - }, - }, - wantCfg: configtest.Config{ - SomeConfigField: 123, - AuthConfigs: map[string]configtest.AuthConfig{ - "registry1.example.com": { - SomeAuthField: "something", - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - }, - }, - CredentialsStore: "newstore", - CredentialHelpers: map[string]string{ - "registry2.example.com": "testhelper", // cred helpers will not be updated - }, - }, - shouldCreateFile: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // prepare test content - configPath := filepath.Join(tempDir, tt.fileName) - if tt.shouldCreateFile { - jsonStr, err := json.Marshal(tt.oldCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - } - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - cfg.credentialsStore = tt.newCfg.CredentialsStore - cfg.credentialHelpers = tt.newCfg.CredentialHelpers - if err := cfg.saveFile(); err != nil { - t.Fatal("saveFile() error =", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - var gotCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - if !reflect.DeepEqual(gotCfg, tt.wantCfg) { - t.Errorf("Decoded config = %v, want %v", gotCfg, tt.wantCfg) - } - }) - } -} - -func Test_encodeAuth(t *testing.T) { - tests := []struct { - name string - username string - password string - want string - }{ - { - name: "Username and password", - username: "username", - password: "password", - want: "dXNlcm5hbWU6cGFzc3dvcmQ=", - }, - { - name: "Username only", - username: "username", - password: "", - want: "dXNlcm5hbWU6", - }, - { - name: "Password only", - username: "", - password: "password", - want: "OnBhc3N3b3Jk", - }, - { - name: "Empty username and empty password", - username: "", - password: "", - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := encodeAuth(tt.username, tt.password); got != tt.want { - t.Errorf("encodeAuth() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_decodeAuth(t *testing.T) { - tests := []struct { - name string - authStr string - username string - password string - wantErr bool - }{ - { - name: "Valid base64", - authStr: "dXNlcm5hbWU6cGFzc3dvcmQ=", // username:password - username: "username", - password: "password", - }, - { - name: "Valid base64, username only", - authStr: "dXNlcm5hbWU6", // username: - username: "username", - }, - { - name: "Valid base64, password only", - authStr: "OnBhc3N3b3Jk", // :password - password: "password", - }, - { - name: "Valid base64, bad format", - authStr: "d2hhdGV2ZXI=", // whatever - username: "", - password: "", - wantErr: true, - }, - { - name: "Invalid base64", - authStr: "whatever", - username: "", - password: "", - wantErr: true, - }, - { - name: "Empty string", - authStr: "", - username: "", - password: "", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUsername, gotPassword, err := decodeAuth(tt.authStr) - if (err != nil) != tt.wantErr { - t.Errorf("decodeAuth() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotUsername != tt.username { - t.Errorf("decodeAuth() got = %v, want %v", gotUsername, tt.username) - } - if gotPassword != tt.password { - t.Errorf("decodeAuth() got1 = %v, want %v", gotPassword, tt.password) - } - }) - } -} diff --git a/internal/executer/executer.go b/internal/executer/executer.go deleted file mode 100644 index fc872c9..0000000 --- a/internal/executer/executer.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package executer is an abstraction for the docker credential helper protocol -// binaries. It is used by nativeStore to interact with installed binaries. -package executer - -import ( - "bytes" - "context" - "errors" - "io" - "os" - "os/exec" - - "github.com/oras-project/oras-credentials-go/trace" -) - -// dockerDesktopHelperName is the name of the docker credentials helper -// execuatable. -const dockerDesktopHelperName = "docker-credential-desktop.exe" - -// Executer is an interface that simulates an executable binary. -type Executer interface { - Execute(ctx context.Context, input io.Reader, action string) ([]byte, error) -} - -// executable implements the Executer interface. -type executable struct { - name string -} - -// New returns a new Executer instance. -func New(name string) Executer { - return &executable{ - name: name, - } -} - -// Execute operates on an executable binary and supports context. -func (c *executable) Execute(ctx context.Context, input io.Reader, action string) ([]byte, error) { - cmd := exec.CommandContext(ctx, c.name, action) - cmd.Stdin = input - cmd.Stderr = os.Stderr - trace := trace.ContextExecutableTrace(ctx) - if trace != nil && trace.ExecuteStart != nil { - trace.ExecuteStart(c.name, action) - } - output, err := cmd.Output() - if trace != nil && trace.ExecuteDone != nil { - trace.ExecuteDone(c.name, action, err) - } - if err != nil { - switch execErr := err.(type) { - case *exec.ExitError: - if errMessage := string(bytes.TrimSpace(output)); errMessage != "" { - return nil, errors.New(errMessage) - } - case *exec.Error: - // check if the error is caused by Docker Desktop not running - if execErr.Err == exec.ErrNotFound && c.name == dockerDesktopHelperName { - return nil, errors.New("credentials store is configured to `desktop.exe` but Docker Desktop seems not running") - } - } - return nil, err - } - return output, nil -} diff --git a/internal/ioutil/ioutil.go b/internal/ioutil/ioutil.go deleted file mode 100644 index b2e3179..0000000 --- a/internal/ioutil/ioutil.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package ioutil - -import ( - "fmt" - "io" - "os" -) - -// Ingest writes content into a temporary ingest file with the file name format -// "oras_credstore_temp_{randomString}". -func Ingest(dir string, content io.Reader) (path string, ingestErr error) { - tempFile, err := os.CreateTemp(dir, "oras_credstore_temp_*") - if err != nil { - return "", fmt.Errorf("failed to create ingest file: %w", err) - } - path = tempFile.Name() - defer func() { - if err := tempFile.Close(); err != nil && ingestErr == nil { - ingestErr = fmt.Errorf("failed to close ingest file: %w", err) - } - // remove the temp file in case of error. - if ingestErr != nil { - os.Remove(path) - } - }() - - if err := tempFile.Chmod(0600); err != nil { - return "", fmt.Errorf("failed to ensure permission: %w", err) - } - if _, err := io.Copy(tempFile, content); err != nil { - return "", fmt.Errorf("failed to ingest: %w", err) - } - return -} diff --git a/memory_store.go b/memory_store.go index 34a6ace..092d362 100644 --- a/memory_store.go +++ b/memory_store.go @@ -16,39 +16,14 @@ limitations under the License. package credentials import ( - "context" - "sync" - - "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/credentials" ) -// MemoryStore is a store that keeps credentials in memory. -type MemoryStore struct { - store sync.Map -} - // NewMemoryStore creates a new in-memory credentials store. -func NewMemoryStore() *MemoryStore { - return &MemoryStore{} -} - -// Get retrieves credentials from the store for the given server address. -func (ms *MemoryStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) { - cred, found := ms.store.Load(serverAddress) - if !found { - return auth.EmptyCredential, nil - } - return cred.(auth.Credential), nil -} - -// Put saves credentials into the store for the given server address. -func (ms *MemoryStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error { - ms.store.Store(serverAddress, cred) - return nil -} - -// Delete removes credentials from the store for the given server address. -func (ms *MemoryStore) Delete(_ context.Context, serverAddress string) error { - ms.store.Delete(serverAddress) - return nil +// +// Deprecated: This funciton now simply calls [credentials.NewMemoryStore] of oras-go. +// +// [credentials.NewMemoryStore]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#NewMemoryStore +func NewMemoryStore() Store { + return credentials.NewMemoryStore() } diff --git a/memory_store_test.go b/memory_store_test.go index 8276722..fbb424c 100644 --- a/memory_store_test.go +++ b/memory_store_test.go @@ -49,7 +49,7 @@ func TestMemoryStore_Get_validRecord(t *testing.T) { RefreshToken: "identity_token", AccessToken: "registry_token", } - ms.store.Store(serverAddress, want) + ms.Put(ctx, serverAddress, want) got, err := ms.Get(ctx, serverAddress) if err != nil { diff --git a/native_store.go b/native_store.go index d67bbc1..0c92ac5 100644 --- a/native_store.go +++ b/native_store.go @@ -16,38 +16,9 @@ limitations under the License. package credentials import ( - "bytes" - "context" - "encoding/json" - "os/exec" - "strings" - - "github.com/oras-project/oras-credentials-go/internal/executer" - "oras.land/oras-go/v2/registry/remote/auth" -) - -const ( - remoteCredentialsPrefix = "docker-credential-" - emptyUsername = "" - errCredentialsNotFoundMessage = "credentials not found in native keychain" + "oras.land/oras-go/v2/registry/remote/credentials" ) -// dockerCredentials mimics how docker credential helper binaries store -// credential information. -// Reference: -// - https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol -type dockerCredentials struct { - ServerURL string `json:"ServerURL"` - Username string `json:"Username"` - Secret string `json:"Secret"` -} - -// nativeStore implements a credentials store using native keychain to keep -// credentials secure. -type nativeStore struct { - exec executer.Executer -} - // NewNativeStore creates a new native store that uses a remote helper program to // manage credentials. // @@ -57,10 +28,12 @@ type nativeStore struct { // // Reference: // - https://docs.docker.com/engine/reference/commandline/login#credentials-store +// +// Deprecated: This funciton now simply calls [credentials.NewNativeStore] of oras-go. +// +// [credentials.NewNativeStore]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#NewNativeStore func NewNativeStore(helperSuffix string) Store { - return &nativeStore{ - exec: executer.New(remoteCredentialsPrefix + helperSuffix), - } + return credentials.NewNativeStore(helperSuffix) } // NewDefaultNativeStore returns a native store based on the platform-default @@ -72,68 +45,10 @@ func NewNativeStore(helperSuffix string) Store { // // Reference: // - https://docs.docker.com/engine/reference/commandline/login/#credentials-store +// +// Deprecated: This funciton now simply calls [credentials.NewDefaultNativeStore] of oras-go. +// +// [credentials.NewDefaultNativeStore]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#NewDefaultNativeStore func NewDefaultNativeStore() (Store, bool) { - if helper := getDefaultHelperSuffix(); helper != "" { - return NewNativeStore(helper), true - } - return nil, false -} - -// Get retrieves credentials from the store for the given server. -func (ns *nativeStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { - var cred auth.Credential - out, err := ns.exec.Execute(ctx, strings.NewReader(serverAddress), "get") - if err != nil { - if err.Error() == errCredentialsNotFoundMessage { - // do not return an error if the credentials are not in the keychain. - return auth.EmptyCredential, nil - } - return auth.EmptyCredential, err - } - var dockerCred dockerCredentials - if err := json.Unmarshal(out, &dockerCred); err != nil { - return auth.EmptyCredential, err - } - // bearer auth is used if the username is "" - if dockerCred.Username == emptyUsername { - cred.RefreshToken = dockerCred.Secret - } else { - cred.Username = dockerCred.Username - cred.Password = dockerCred.Secret - } - return cred, nil -} - -// Put saves credentials into the store. -func (ns *nativeStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { - dockerCred := &dockerCredentials{ - ServerURL: serverAddress, - Username: cred.Username, - Secret: cred.Password, - } - if cred.RefreshToken != "" { - dockerCred.Username = emptyUsername - dockerCred.Secret = cred.RefreshToken - } - credJSON, err := json.Marshal(dockerCred) - if err != nil { - return err - } - _, err = ns.exec.Execute(ctx, bytes.NewReader(credJSON), "store") - return err -} - -// Delete removes credentials from the store for the given server. -func (ns *nativeStore) Delete(ctx context.Context, serverAddress string) error { - _, err := ns.exec.Execute(ctx, strings.NewReader(serverAddress), "erase") - return err -} - -// getDefaultHelperSuffix returns the default credential helper suffix. -func getDefaultHelperSuffix() string { - platformDefault := getPlatformDefaultHelperSuffix() - if _, err := exec.LookPath(remoteCredentialsPrefix + platformDefault); err == nil { - return platformDefault - } - return "" + return credentials.NewDefaultNativeStore() } diff --git a/native_store_darwin.go b/native_store_darwin.go deleted file mode 100644 index 1a9aca6..0000000 --- a/native_store_darwin.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package credentials - -// getPlatformDefaultHelperSuffix returns the platform default credential -// helper suffix. -// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior -func getPlatformDefaultHelperSuffix() string { - return "osxkeychain" -} diff --git a/native_store_generic.go b/native_store_generic.go deleted file mode 100644 index 5c7d4a3..0000000 --- a/native_store_generic.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !windows && !darwin && !linux - -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package credentials - -// getPlatformDefaultHelperSuffix returns the platform default credential -// helper suffix. -// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior -func getPlatformDefaultHelperSuffix() string { - return "" -} diff --git a/native_store_linux.go b/native_store_linux.go deleted file mode 100644 index f182923..0000000 --- a/native_store_linux.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package credentials - -import "os/exec" - -// getPlatformDefaultHelperSuffix returns the platform default credential -// helper suffix. -// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior -func getPlatformDefaultHelperSuffix() string { - if _, err := exec.LookPath("pass"); err == nil { - return "pass" - } - - return "secretservice" -} diff --git a/native_store_test.go b/native_store_test.go deleted file mode 100644 index 6ee0059..0000000 --- a/native_store_test.go +++ /dev/null @@ -1,385 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package credentials - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "strings" - "testing" - - "github.com/oras-project/oras-credentials-go/trace" - "oras.land/oras-go/v2/registry/remote/auth" -) - -const ( - basicAuthHost = "localhost:2333" - bearerAuthHost = "localhost:666" - exeErrorHost = "localhost:500/exeError" - jsonErrorHost = "localhost:500/jsonError" - noCredentialsHost = "localhost:404" - traceHost = "localhost:808" - testUsername = "test_username" - testPassword = "test_password" - testRefreshToken = "test_token" -) - -var ( - errCommandExited = fmt.Errorf("exited with error") - errExecute = fmt.Errorf("Execute failed") - errCredentialsNotFound = fmt.Errorf(errCredentialsNotFoundMessage) -) - -// testExecuter implements the Executer interface for testing purpose. -// It simulates interactions between the docker client and a remote -// credentials helper. -type testExecuter struct{} - -// Execute mocks the behavior of a credential helper binary. It returns responses -// and errors based on the input. -func (e *testExecuter) Execute(ctx context.Context, input io.Reader, action string) ([]byte, error) { - in, err := io.ReadAll(input) - if err != nil { - return nil, err - } - inS := string(in) - switch action { - case "get": - switch inS { - case basicAuthHost: - return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil - case bearerAuthHost: - return []byte(`{"Username": "", "Secret": "test_token"}`), nil - case exeErrorHost: - return []byte("Execute failed"), errExecute - case jsonErrorHost: - return []byte("json.Unmarshal failed"), nil - case noCredentialsHost: - return []byte("credentials not found"), errCredentialsNotFound - case traceHost: - traceHook := trace.ContextExecutableTrace(ctx) - if traceHook != nil { - if traceHook.ExecuteStart != nil { - traceHook.ExecuteStart("testExecuter", "get") - } - if traceHook.ExecuteDone != nil { - traceHook.ExecuteDone("testExecuter", "get", nil) - } - } - return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil - default: - return []byte("program failed"), errCommandExited - } - case "store": - var c dockerCredentials - err := json.NewDecoder(strings.NewReader(inS)).Decode(&c) - if err != nil { - return []byte("program failed"), errCommandExited - } - switch c.ServerURL { - case basicAuthHost, bearerAuthHost, exeErrorHost: - return nil, nil - case traceHost: - traceHook := trace.ContextExecutableTrace(ctx) - if traceHook != nil { - if traceHook.ExecuteStart != nil { - traceHook.ExecuteStart("testExecuter", "store") - } - if traceHook.ExecuteDone != nil { - traceHook.ExecuteDone("testExecuter", "store", nil) - } - } - return nil, nil - default: - return []byte("program failed"), errCommandExited - } - case "erase": - switch inS { - case basicAuthHost, bearerAuthHost: - return nil, nil - case traceHost: - traceHook := trace.ContextExecutableTrace(ctx) - if traceHook != nil { - if traceHook.ExecuteStart != nil { - traceHook.ExecuteStart("testExecuter", "erase") - } - if traceHook.ExecuteDone != nil { - traceHook.ExecuteDone("testExecuter", "erase", nil) - } - } - return nil, nil - default: - return []byte("program failed"), errCommandExited - } - } - return []byte(fmt.Sprintf("unknown argument %q with %q", action, inS)), errCommandExited -} - -func TestNativeStore_interface(t *testing.T) { - var ns interface{} = &nativeStore{} - if _, ok := ns.(Store); !ok { - t.Error("&NativeStore{} does not conform Store") - } -} - -func TestNativeStore_basicAuth(t *testing.T) { - ns := &nativeStore{ - &testExecuter{}, - } - // Put - err := ns.Put(context.Background(), basicAuthHost, auth.Credential{Username: testUsername, Password: testPassword}) - if err != nil { - t.Fatalf("basic auth test ns.Put fails: %v", err) - } - // Get - cred, err := ns.Get(context.Background(), basicAuthHost) - if err != nil { - t.Fatalf("basic auth test ns.Get fails: %v", err) - } - if cred.Username != testUsername { - t.Fatal("incorrect username") - } - if cred.Password != testPassword { - t.Fatal("incorrect password") - } - // Delete - err = ns.Delete(context.Background(), basicAuthHost) - if err != nil { - t.Fatalf("basic auth test ns.Delete fails: %v", err) - } -} - -func TestNativeStore_refreshToken(t *testing.T) { - ns := &nativeStore{ - &testExecuter{}, - } - // Put - err := ns.Put(context.Background(), bearerAuthHost, auth.Credential{RefreshToken: testRefreshToken}) - if err != nil { - t.Fatalf("refresh token test ns.Put fails: %v", err) - } - // Get - cred, err := ns.Get(context.Background(), bearerAuthHost) - if err != nil { - t.Fatalf("refresh token test ns.Get fails: %v", err) - } - if cred.Username != "" { - t.Fatalf("expect username to be empty, got %s", cred.Username) - } - if cred.RefreshToken != testRefreshToken { - t.Fatal("incorrect refresh token") - } - // Delete - err = ns.Delete(context.Background(), basicAuthHost) - if err != nil { - t.Fatalf("refresh token test ns.Delete fails: %v", err) - } -} - -func TestNativeStore_errorHandling(t *testing.T) { - ns := &nativeStore{ - &testExecuter{}, - } - // Get Error: Execute error - _, err := ns.Get(context.Background(), exeErrorHost) - if err != errExecute { - t.Fatalf("got error: %v, should get exeErr", err) - } - // Get Error: json.Unmarshal - _, err = ns.Get(context.Background(), jsonErrorHost) - if err == nil { - t.Fatalf("should get error from json.Unmarshal") - } - // Get: Should not return error when credentials are not found - _, err = ns.Get(context.Background(), noCredentialsHost) - if err != nil { - t.Fatalf("should not get error when no credentials are found") - } -} - -func TestNewDefaultNativeStore(t *testing.T) { - defaultHelper := getDefaultHelperSuffix() - wantOK := (defaultHelper != "") - - if _, ok := NewDefaultNativeStore(); ok != wantOK { - t.Errorf("NewDefaultNativeStore() = %v, want %v", ok, wantOK) - } -} - -func TestNativeStore_trace(t *testing.T) { - ns := &nativeStore{ - &testExecuter{}, - } - // create trace hooks that write to buffer - buffer := bytes.Buffer{} - traceHook := &trace.ExecutableTrace{ - ExecuteStart: func(executableName string, action string) { - buffer.WriteString(fmt.Sprintf("test trace, start the execution of executable %s with action %s ", executableName, action)) - }, - ExecuteDone: func(executableName string, action string, err error) { - buffer.WriteString(fmt.Sprintf("test trace, completed the execution of executable %s with action %s and got err %v", executableName, action, err)) - }, - } - ctx := trace.WithExecutableTrace(context.Background(), traceHook) - // Test ns.Put trace - err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword}) - if err != nil { - t.Fatalf("trace test ns.Put fails: %v", err) - } - bufferContent := buffer.String() - if bufferContent != "test trace, start the execution of executable testExecuter with action store test trace, completed the execution of executable testExecuter with action store and got err " { - t.Fatalf("incorrect buffer content: %s", bufferContent) - } - buffer.Reset() - // Test ns.Get trace - _, err = ns.Get(ctx, traceHost) - if err != nil { - t.Fatalf("trace test ns.Get fails: %v", err) - } - bufferContent = buffer.String() - if bufferContent != "test trace, start the execution of executable testExecuter with action get test trace, completed the execution of executable testExecuter with action get and got err " { - t.Fatalf("incorrect buffer content: %s", bufferContent) - } - buffer.Reset() - // Test ns.Delete trace - err = ns.Delete(ctx, traceHost) - if err != nil { - t.Fatalf("trace test ns.Delete fails: %v", err) - } - bufferContent = buffer.String() - if bufferContent != "test trace, start the execution of executable testExecuter with action erase test trace, completed the execution of executable testExecuter with action erase and got err " { - t.Fatalf("incorrect buffer content: %s", bufferContent) - } -} - -// This test ensures that a nil trace will not cause an error. -func TestNativeStore_noTrace(t *testing.T) { - ns := &nativeStore{ - &testExecuter{}, - } - // Put - err := ns.Put(context.Background(), traceHost, auth.Credential{Username: testUsername, Password: testPassword}) - if err != nil { - t.Fatalf("basic auth test ns.Put fails: %v", err) - } - // Get - cred, err := ns.Get(context.Background(), traceHost) - if err != nil { - t.Fatalf("basic auth test ns.Get fails: %v", err) - } - if cred.Username != testUsername { - t.Fatal("incorrect username") - } - if cred.Password != testPassword { - t.Fatal("incorrect password") - } - // Delete - err = ns.Delete(context.Background(), traceHost) - if err != nil { - t.Fatalf("basic auth test ns.Delete fails: %v", err) - } -} - -// This test ensures that an empty trace will not cause an error. -func TestNativeStore_emptyTrace(t *testing.T) { - ns := &nativeStore{ - &testExecuter{}, - } - traceHook := &trace.ExecutableTrace{} - ctx := trace.WithExecutableTrace(context.Background(), traceHook) - // Put - err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword}) - if err != nil { - t.Fatalf("basic auth test ns.Put fails: %v", err) - } - // Get - cred, err := ns.Get(ctx, traceHost) - if err != nil { - t.Fatalf("basic auth test ns.Get fails: %v", err) - } - if cred.Username != testUsername { - t.Fatal("incorrect username") - } - if cred.Password != testPassword { - t.Fatal("incorrect password") - } - // Delete - err = ns.Delete(ctx, traceHost) - if err != nil { - t.Fatalf("basic auth test ns.Delete fails: %v", err) - } -} - -func TestNativeStore_multipleTrace(t *testing.T) { - ns := &nativeStore{ - &testExecuter{}, - } - // create trace hooks that write to buffer - buffer := bytes.Buffer{} - trace1 := &trace.ExecutableTrace{ - ExecuteStart: func(executableName string, action string) { - buffer.WriteString(fmt.Sprintf("trace 1 start %s, %s ", executableName, action)) - }, - ExecuteDone: func(executableName string, action string, err error) { - buffer.WriteString(fmt.Sprintf("trace 1 done %s, %s, %v ", executableName, action, err)) - }, - } - ctx := context.Background() - ctx = trace.WithExecutableTrace(ctx, trace1) - trace2 := &trace.ExecutableTrace{ - ExecuteStart: func(executableName string, action string) { - buffer.WriteString(fmt.Sprintf("trace 2 start %s, %s ", executableName, action)) - }, - ExecuteDone: func(executableName string, action string, err error) { - buffer.WriteString(fmt.Sprintf("trace 2 done %s, %s, %v ", executableName, action, err)) - }, - } - ctx = trace.WithExecutableTrace(ctx, trace2) - trace3 := &trace.ExecutableTrace{} - ctx = trace.WithExecutableTrace(ctx, trace3) - // Test ns.Put trace - err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword}) - if err != nil { - t.Fatalf("trace test ns.Put fails: %v", err) - } - bufferContent := buffer.String() - if bufferContent != "trace 2 start testExecuter, store trace 1 start testExecuter, store trace 2 done testExecuter, store, trace 1 done testExecuter, store, " { - t.Fatalf("incorrect buffer content: %s", bufferContent) - } - buffer.Reset() - // Test ns.Get trace - _, err = ns.Get(ctx, traceHost) - if err != nil { - t.Fatalf("trace test ns.Get fails: %v", err) - } - bufferContent = buffer.String() - if bufferContent != "trace 2 start testExecuter, get trace 1 start testExecuter, get trace 2 done testExecuter, get, trace 1 done testExecuter, get, " { - t.Fatalf("incorrect buffer content: %s", bufferContent) - } - buffer.Reset() - // Test ns.Delete trace - err = ns.Delete(ctx, traceHost) - if err != nil { - t.Fatalf("trace test ns.Delete fails: %v", err) - } - bufferContent = buffer.String() - if bufferContent != "trace 2 start testExecuter, erase trace 1 start testExecuter, erase trace 2 done testExecuter, erase, trace 1 done testExecuter, erase, " { - t.Fatalf("incorrect buffer content: %s", bufferContent) - } -} diff --git a/native_store_windows.go b/native_store_windows.go deleted file mode 100644 index e334cc7..0000000 --- a/native_store_windows.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package credentials - -// getPlatformDefaultHelperSuffix returns the platform default credential -// helper suffix. -// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior -func getPlatformDefaultHelperSuffix() string { - return "wincred" -} diff --git a/registry.go b/registry.go index 8b82b0d..6a3088b 100644 --- a/registry.go +++ b/registry.go @@ -17,86 +17,70 @@ package credentials import ( "context" - "errors" - "fmt" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/credentials" ) // ErrClientTypeUnsupported is thrown by Login() when the registry's client type // is not supported. -var ErrClientTypeUnsupported = errors.New("client type not supported") +// +// Deprecated: This type is now simply [credentials.ErrClientTypeUnsupported] of oras-go. +// +// [credentials.ErrClientTypeUnsupported]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#ErrClientTypeUnsupported +var ErrClientTypeUnsupported = credentials.ErrClientTypeUnsupported // Login provides the login functionality with the given credentials. The target // registry's client should be nil or of type *auth.Client. Login uses // a client local to the function and will not modify the original client of // the registry. +// +// Deprecated: This funciton now simply calls [credentials.Login] of oras-go. +// +// [credentials.Login]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#Login func Login(ctx context.Context, store Store, reg *remote.Registry, cred auth.Credential) error { - // create a clone of the original registry for login purpose - regClone := *reg - // we use the original client if applicable, otherwise use a default client - var authClient auth.Client - if reg.Client == nil { - authClient = *auth.DefaultClient - authClient.Cache = nil // no cache - } else if client, ok := reg.Client.(*auth.Client); ok { - authClient = *client - } else { - return ErrClientTypeUnsupported - } - regClone.Client = &authClient - // update credentials with the client - authClient.Credential = auth.StaticCredential(reg.Reference.Registry, cred) - // validate and store the credential - if err := regClone.Ping(ctx); err != nil { - return fmt.Errorf("failed to validate the credentials for %s: %w", regClone.Reference.Registry, err) - } - hostname := ServerAddressFromRegistry(regClone.Reference.Registry) - if err := store.Put(ctx, hostname, cred); err != nil { - return fmt.Errorf("failed to store the credentials for %s: %w", hostname, err) - } - return nil + return credentials.Login(ctx, store, reg, cred) } // Logout provides the logout functionality given the registry name. +// +// Deprecated: This funciton now simply calls [credentials.Logout] of oras-go. +// +// [credentials.Logout]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#Logout func Logout(ctx context.Context, store Store, registryName string) error { - registryName = ServerAddressFromRegistry(registryName) - if err := store.Delete(ctx, registryName); err != nil { - return fmt.Errorf("failed to delete the credential for %s: %w", registryName, err) - } - return nil + return credentials.Logout(ctx, store, registryName) } // Credential returns a Credential() function that can be used by auth.Client. +// +// Deprecated: This funciton now simply calls [credentials.Credential] of oras-go. +// +// [credentials.Credential]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#Credential func Credential(store Store) func(context.Context, string) (auth.Credential, error) { - return func(ctx context.Context, reg string) (auth.Credential, error) { - reg = ServerAddressFromHostname(reg) - if reg == "" { - return auth.EmptyCredential, nil - } - return store.Get(ctx, reg) - } + return credentials.Credential(store) } // ServerAddressFromRegistry maps a registry to a server address, which is used as // a key for credentials store. The Docker CLI expects that the credentials of // the registry 'docker.io' will be added under the key "https://index.docker.io/v1/". // See: https://github.com/moby/moby/blob/v24.0.2/registry/config.go#L25-L48 +// +// Deprecated: This funciton now simply calls [credentials.ServerAddressFromRegistry] of oras-go. +// +// [credentials.ServerAddressFromRegistry]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#ServerAddressFromRegistry func ServerAddressFromRegistry(registry string) string { - if registry == "docker.io" { - return "https://index.docker.io/v1/" - } - return registry + return credentials.ServerAddressFromRegistry(registry) } // ServerAddressFromHostname maps a hostname to a server address, which is used as // a key for credentials store. It is expected that the traffic targetting the // host "registry-1.docker.io" will be redirected to "https://index.docker.io/v1/". // See: https://github.com/moby/moby/blob/v24.0.2/registry/config.go#L25-L48 +// +// Deprecated: This funciton now simply calls [credentials.ServerAddressFromHostname] of oras-go. +// +// [credentials.ServerAddressFromHostname]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#ServerAddressFromHostname func ServerAddressFromHostname(hostname string) string { - if hostname == "registry-1.docker.io" { - return "https://index.docker.io/v1/" - } - return hostname + return credentials.ServerAddressFromHostname(hostname) } diff --git a/registry_test.go b/registry_test.go index 1b5dce3..687b09f 100644 --- a/registry_test.go +++ b/registry_test.go @@ -53,6 +53,8 @@ func (t *testStore) Delete(ctx context.Context, serverAddress string) error { func TestLogin(t *testing.T) { // create a test registry + testUsername := "test_username" + testPassword := "test_password" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wantedAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(testUsername+":"+testPassword)) authHeader := r.Header.Get("Authorization") diff --git a/store.go b/store.go index fcb367f..da061a7 100644 --- a/store.go +++ b/store.go @@ -13,69 +13,37 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Deprecated: This package is deprecated. +// The same functionality is now provided by [oras.land/oras-go/v2/registry/remote/credentials]. +// +// [oras.land/oras-go/v2/registry/remote/credentials]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials package credentials import ( - "context" - "fmt" - "os" - "path/filepath" - "sync" - - "github.com/oras-project/oras-credentials-go/internal/config" - "oras.land/oras-go/v2/registry/remote/auth" -) - -const ( - dockerConfigDirEnv = "DOCKER_CONFIG" - dockerConfigFileDir = ".docker" - dockerConfigFileName = "config.json" + "oras.land/oras-go/v2/registry/remote/credentials" ) // Store is the interface that any credentials store must implement. -type Store interface { - // Get retrieves credentials from the store for the given server address. - Get(ctx context.Context, serverAddress string) (auth.Credential, error) - // Put saves credentials into the store for the given server address. - Put(ctx context.Context, serverAddress string, cred auth.Credential) error - // Delete removes credentials from the store for the given server address. - Delete(ctx context.Context, serverAddress string) error -} +// +// Deprecated: This type is now simply [credentials.Store] of oras-go. +// +// [credentials.Store]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#Store +type Store = credentials.Store // DynamicStore dynamically determines which store to use based on the settings // in the config file. -type DynamicStore struct { - config *config.Config - options StoreOptions - detectedCredsStore string - setCredsStoreOnce sync.Once -} +// +// Deprecated: This type is now simply [credentials.DynamicStore] of oras-go. +// +// [credentials.DynamicStore]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#DynamicStore +type DynamicStore = credentials.DynamicStore // StoreOptions provides options for NewStore. -type StoreOptions struct { - // AllowPlaintextPut allows saving credentials in plaintext in the config - // file. - // - If AllowPlaintextPut is set to false (default value), Put() will - // return an error when native store is not available. - // - If AllowPlaintextPut is set to true, Put() will save credentials in - // plaintext in the config file when native store is not available. - AllowPlaintextPut bool - - // DetectDefaultNativeStore enables detecting the platform-default native - // credentials store when the config file has no authentication information. - // - // If DetectDefaultNativeStore is set to true, the store will detect and set - // the default native credentials store in the "credsStore" field of the - // config file. - // - Windows: "wincred" - // - Linux: "pass" or "secretservice" - // - macOS: "osxkeychain" - // - // References: - // - https://docs.docker.com/engine/reference/commandline/login/#credentials-store - // - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties - DetectDefaultNativeStore bool -} +// +// Deprecated: This type is now simply [credentials.StoreOptions] of oras-go. +// +// [credentials.StoreOptions]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#StoreOptions +type StoreOptions = credentials.StoreOptions // NewStore returns a Store based on the given configuration file. // @@ -89,20 +57,12 @@ type StoreOptions struct { // References: // - https://docs.docker.com/engine/reference/commandline/login/#credentials-store // - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties +// +// Deprecated: This funciton now simply calls [credentials.NewStore] of oras-go. +// +// [credentials.NewStore]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#NewStore func NewStore(configPath string, opts StoreOptions) (*DynamicStore, error) { - cfg, err := config.Load(configPath) - if err != nil { - return nil, err - } - ds := &DynamicStore{ - config: cfg, - options: opts, - } - if opts.DetectDefaultNativeStore && !cfg.IsAuthConfigured() { - // no authentication configured, detect the default credentials store - ds.detectedCredsStore = getDefaultHelperSuffix() - } - return ds, nil + return credentials.NewStore(configPath, opts) } // NewStoreFromDocker returns a Store based on the default docker config file. @@ -115,97 +75,12 @@ func NewStore(configPath string, opts StoreOptions) (*DynamicStore, error) { // References: // - https://docs.docker.com/engine/reference/commandline/cli/#configuration-files // - https://docs.docker.com/engine/reference/commandline/cli/#change-the-docker-directory -func NewStoreFromDocker(opt StoreOptions) (*DynamicStore, error) { - configPath, err := getDockerConfigPath() - if err != nil { - return nil, err - } - return NewStore(configPath, opt) -} - -// Get retrieves credentials from the store for the given server address. -func (ds *DynamicStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { - return ds.getStore(serverAddress).Get(ctx, serverAddress) -} - -// Put saves credentials into the store for the given server address. -// Put returns ErrPlaintextPutDisabled if native store is not available and -// [StoreOptions].AllowPlaintextPut is set to false. -func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) (returnErr error) { - if err := ds.getStore(serverAddress).Put(ctx, serverAddress, cred); err != nil { - return err - } - // save the detected creds store back to the config file on first put - ds.setCredsStoreOnce.Do(func() { - if ds.detectedCredsStore != "" { - if err := ds.config.SetCredentialsStore(ds.detectedCredsStore); err != nil { - returnErr = fmt.Errorf("failed to set credsStore: %w", err) - } - } - }) - return returnErr -} - -// Delete removes credentials from the store for the given server address. -func (ds *DynamicStore) Delete(ctx context.Context, serverAddress string) error { - return ds.getStore(serverAddress).Delete(ctx, serverAddress) -} - -// IsAuthConfigured returns whether there is authentication configured in the -// config file or not. -// -// IsAuthConfigured returns true when: -// - The "credsStore" field is not empty -// - Or the "credHelpers" field is not empty -// - Or there is any entry in the "auths" field -func (ds *DynamicStore) IsAuthConfigured() bool { - return ds.config.IsAuthConfigured() -} - -// getHelperSuffix returns the credential helper suffix for the given server -// address. -func (ds *DynamicStore) getHelperSuffix(serverAddress string) string { - // 1. Look for a server-specific credential helper first - if helper := ds.config.GetCredentialHelper(serverAddress); helper != "" { - return helper - } - // 2. Then look for the configured native store - if credsStore := ds.config.CredentialsStore(); credsStore != "" { - return credsStore - } - // 3. Use the detected default store - return ds.detectedCredsStore -} - -// getStore returns a store for the given server address. -func (ds *DynamicStore) getStore(serverAddress string) Store { - if helper := ds.getHelperSuffix(serverAddress); helper != "" { - return NewNativeStore(helper) - } - - fs := newFileStore(ds.config) - fs.DisablePut = !ds.options.AllowPlaintextPut - return fs -} - -// getDockerConfigPath returns the path to the default docker config file. -func getDockerConfigPath() (string, error) { - // first try the environment variable - configDir := os.Getenv(dockerConfigDirEnv) - if configDir == "" { - // then try home directory - homeDir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get user home directory: %w", err) - } - configDir = filepath.Join(homeDir, dockerConfigFileDir) - } - return filepath.Join(configDir, dockerConfigFileName), nil -} - -// storeWithFallbacks is a store that has multiple fallback stores. -type storeWithFallbacks struct { - stores []Store +// +// Deprecated: This funciton now simply calls [credentials.NewStoreFromDocker] of oras-go. +// +// [credentials.NewStoreFromDocker]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#NewStoreFromDocker +func NewStoreFromDocker(opts StoreOptions) (*DynamicStore, error) { + return credentials.NewStoreFromDocker(opts) } // NewStoreWithFallbacks returns a new store based on the given stores. @@ -214,39 +89,10 @@ type storeWithFallbacks struct { // credentials in any of the stores. // - Put() saves the credentials into the primary store. // - Delete() deletes the credentials from the primary store. +// +// Deprecated: This funciton now simply calls [credentials.NewStoreWithFallbacks] of oras-go. +// +// [credentials.NewStoreWithFallbacks]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#NewStoreWithFallbacks func NewStoreWithFallbacks(primary Store, fallbacks ...Store) Store { - if len(fallbacks) == 0 { - return primary - } - return &storeWithFallbacks{ - stores: append([]Store{primary}, fallbacks...), - } -} - -// Get retrieves credentials from the StoreWithFallbacks for the given server. -// It searches the primary and the fallback stores for the credentials of serverAddress -// and returns when it finds the credentials in any of the stores. -func (sf *storeWithFallbacks) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { - for _, s := range sf.stores { - cred, err := s.Get(ctx, serverAddress) - if err != nil { - return auth.EmptyCredential, err - } - if cred != auth.EmptyCredential { - return cred, nil - } - } - return auth.EmptyCredential, nil -} - -// Put saves credentials into the StoreWithFallbacks. It puts -// the credentials into the primary store. -func (sf *storeWithFallbacks) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { - return sf.stores[0].Put(ctx, serverAddress, cred) -} - -// Delete removes credentials from the StoreWithFallbacks for the given server. -// It deletes the credentials from the primary store. -func (sf *storeWithFallbacks) Delete(ctx context.Context, serverAddress string) error { - return sf.stores[0].Delete(ctx, serverAddress) + return credentials.NewStoreWithFallbacks(primary, fallbacks...) } diff --git a/store_test.go b/store_test.go index d18b559..f9a7bf1 100644 --- a/store_test.go +++ b/store_test.go @@ -217,13 +217,6 @@ func Test_DynamicStore_authConfigured(t *testing.T) { if err := ds.Put(ctx, serverAddr, cred); err != nil { t.Fatal("DynamicStore.Get() error =", err) } - // Put() should not set detected store back to config - if got := ds.detectedCredsStore; got != "" { - t.Errorf("ds.detectedCredsStore = %v, want empty", got) - } - if got := ds.config.CredentialsStore(); got != "" { - t.Errorf("ds.config.CredentialsStore() = %v, want empty", got) - } // test get got, err := ds.Get(ctx, serverAddr) @@ -294,13 +287,6 @@ func Test_DynamicStore_authConfigured_DetectDefaultNativeStore(t *testing.T) { if err := ds.Put(ctx, serverAddr, cred); err != nil { t.Fatal("DynamicStore.Get() error =", err) } - // Put() should not set detected store back to config - if got := ds.detectedCredsStore; got != "" { - t.Errorf("ds.detectedCredsStore = %v, want empty", got) - } - if got := ds.config.CredentialsStore(); got != "" { - t.Errorf("ds.config.CredentialsStore() = %v, want empty", got) - } // test get got, err := ds.Get(ctx, serverAddr) @@ -369,13 +355,6 @@ func Test_DynamicStore_noAuthConfigured(t *testing.T) { if err := ds.Put(ctx, serverAddr, cred); err != nil { t.Fatal("DynamicStore.Put() error =", err) } - // Put() should not set detected store back to config - if got := ds.detectedCredsStore; got != "" { - t.Errorf("ds.detectedCredsStore = %v, want empty", got) - } - if got := ds.config.CredentialsStore(); got != "" { - t.Errorf("ds.config.CredentialsStore() = %v, want empty", got) - } // test get got, err := ds.Get(ctx, serverAddr) @@ -443,25 +422,12 @@ func Test_DynamicStore_noAuthConfigured_DetectDefaultNativeStore(t *testing.T) { if _, err := ds.Get(ctx, serverAddr); err != nil { t.Fatal("DynamicStore.Get() error =", err) } - if defaultStore := getDefaultHelperSuffix(); defaultStore != "" { - if got := ds.detectedCredsStore; got != defaultStore { - t.Errorf("ds.detectedCredsStore = %v, want %v", got, defaultStore) - } - } - if got := ds.config.CredentialsStore(); got != "" { - t.Errorf("ds.config.CredentialsStore() = %v, want empty", got) - } // test put if err := ds.Put(ctx, serverAddr, cred); err != nil { t.Fatal("DynamicStore.Put() error =", err) } - // Put() should set the detected store back to config - if got := ds.config.CredentialsStore(); got != ds.detectedCredsStore { - t.Errorf("ds.config.CredentialsStore() = %v, want %v", got, ds.detectedCredsStore) - } - // test get got, err := ds.Get(ctx, serverAddr) if err != nil { @@ -555,146 +521,6 @@ func Test_DynamicStore_fileStore_AllowPlainTextPut(t *testing.T) { } } -func Test_DynamicStore_getHelperSuffix(t *testing.T) { - tests := []struct { - name string - configPath string - serverAddress string - want string - }{ - { - name: "Get cred helper: registry_helper1", - configPath: "testdata/credHelpers_config.json", - serverAddress: "registry1.example.com", - want: "registry1-helper", - }, - { - name: "Get cred helper: registry_helper2", - configPath: "testdata/credHelpers_config.json", - serverAddress: "registry2.example.com", - want: "registry2-helper", - }, - { - name: "Empty cred helper configured", - configPath: "testdata/credHelpers_config.json", - serverAddress: "registry3.example.com", - want: "", - }, - { - name: "No cred helper and creds store configured", - configPath: "testdata/credHelpers_config.json", - serverAddress: "whatever.example.com", - want: "", - }, - { - name: "Choose cred helper over creds store", - configPath: "testdata/credsStore_config.json", - serverAddress: "test.example.com", - want: "test-helper", - }, - { - name: "No cred helper configured, choose cred store", - configPath: "testdata/credsStore_config.json", - serverAddress: "whatever.example.com", - want: "teststore", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ds, err := NewStore(tt.configPath, StoreOptions{}) - if err != nil { - t.Fatal("NewStore() error =", err) - } - if got := ds.getHelperSuffix(tt.serverAddress); got != tt.want { - t.Errorf("DynamicStore.getHelperSuffix() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_DynamicStore_getStore_nativeStore(t *testing.T) { - tests := []struct { - name string - configPath string - serverAddress string - }{ - { - name: "Cred helper configured for registry1.example.com", - configPath: "testdata/credHelpers_config.json", - serverAddress: "registry1.example.com", - }, - { - name: "Cred helper configured for registry2.example.com", - configPath: "testdata/credHelpers_config.json", - serverAddress: "registry2.example.com", - }, - { - name: "Cred helper configured for test.example.com", - configPath: "testdata/credsStore_config.json", - serverAddress: "test.example.com", - }, - { - name: "No cred helper configured, use creds store", - configPath: "testdata/credsStore_config.json", - serverAddress: "whaterver.example.com", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ds, err := NewStore(tt.configPath, StoreOptions{}) - if err != nil { - t.Fatal("NewStore() error =", err) - } - gotStore := ds.getStore(tt.serverAddress) - if _, ok := gotStore.(*nativeStore); !ok { - t.Errorf("gotStore is not a native store") - } - }) - } -} - -func Test_DynamicStore_getStore_fileStore(t *testing.T) { - tests := []struct { - name string - configPath string - serverAddress string - }{ - { - name: "Empty cred helper configured for registry3.example.com", - configPath: "testdata/credHelpers_config.json", - serverAddress: "registry3.example.com", - }, - { - name: "No cred helper configured", - configPath: "testdata/credHelpers_config.json", - serverAddress: "whatever.example.com", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ds, err := NewStore(tt.configPath, StoreOptions{}) - if err != nil { - t.Fatal("NewStore() error =", err) - } - gotStore := ds.getStore(tt.serverAddress) - gotFS1, ok := gotStore.(*FileStore) - if !ok { - t.Errorf("gotStore is not a file store") - } - - // get again, the two file stores should be based on the same config instance - gotStore = ds.getStore(tt.serverAddress) - gotFS2, ok := gotStore.(*FileStore) - if !ok { - t.Errorf("gotStore is not a file store") - } - if gotFS1.config != gotFS2.config { - t.Errorf("gotFS1 and gotFS2 are not based on the same config") - } - }) - } -} - func Test_storeWithFallbacks_Get(t *testing.T) { // prepare test content server1 := "foo.registry.com" @@ -871,38 +697,6 @@ func Test_storeWithFallbacks_Delete_throwError(t *testing.T) { } } -func Test_getDockerConfigPath_env(t *testing.T) { - dir, err := os.Getwd() - if err != nil { - t.Fatal("os.Getwd() error =", err) - } - t.Setenv("DOCKER_CONFIG", dir) - - got, err := getDockerConfigPath() - if err != nil { - t.Fatal("getDockerConfigPath() error =", err) - } - if want := filepath.Join(dir, "config.json"); got != want { - t.Errorf("getDockerConfigPath() = %v, want %v", got, want) - } -} - -func Test_getDockerConfigPath_homeDir(t *testing.T) { - t.Setenv("DOCKER_CONFIG", "") - - got, err := getDockerConfigPath() - if err != nil { - t.Fatal("getDockerConfigPath() error =", err) - } - homeDir, err := os.UserHomeDir() - if err != nil { - t.Fatal("os.UserHomeDir()") - } - if want := filepath.Join(homeDir, ".docker", "config.json"); got != want { - t.Errorf("getDockerConfigPath() = %v, want %v", got, want) - } -} - func TestNewStoreFromDocker(t *testing.T) { // prepare test content tempDir := t.TempDir() diff --git a/trace/trace.go b/trace/trace.go index 244a3f1..9caa52f 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -13,84 +13,44 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Deprecated: This package is deprecated. +// The same functionality is now provided by [oras.land/oras-go/v2/registry/remote/credentials/trace]. +// +// [oras.land/oras-go/v2/registry/remote/credentials/trace]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials/trace package trace import ( "context" -) -// executableTraceContextKey is a value key used to retrieve the ExecutableTrace -// from Context. -type executableTraceContextKey struct{} + "oras.land/oras-go/v2/registry/remote/credentials/trace" +) // ExecutableTrace is a set of hooks used to trace the execution of binary // executables. Any particular hook may be nil. -type ExecutableTrace struct { - // ExecuteStart is called before the execution of the executable. The - // executableName parameter is the name of the credential helper executable - // used with NativeStore. The action parameter is one of "store", "get" and - // "erase". - // - // Reference: - // - https://docs.docker.com/engine/reference/commandline/login#credentials-store - ExecuteStart func(executableName string, action string) - - // ExecuteDone is called after the execution of an executable completes. - // The executableName parameter is the name of the credential helper - // executable used with NativeStore. The action parameter is one of "store", - // "get" and "erase". The err parameter is the error (if any) returned from - // the execution. - // - // Reference: - // - https://docs.docker.com/engine/reference/commandline/login#credentials-store - ExecuteDone func(executableName string, action string, err error) -} +// +// Deprecated: This type is now simply [trace.ExecutableTrace] of oras-go. +// +// [trace.ExecutableTrace]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials/trace#ExecutableTrace +type ExecutableTrace = trace.ExecutableTrace // ContextExecutableTrace returns the ExecutableTrace associated with the // context. If none, it returns nil. +// +// Deprecated: This function now simply calls [trace.ContextExecutableTrace] of oras-go. +// +// [trace.ContextExecutableTrace]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials/trace#ContextExecutableTrace func ContextExecutableTrace(ctx context.Context) *ExecutableTrace { - trace, _ := ctx.Value(executableTraceContextKey{}).(*ExecutableTrace) - return trace + return trace.ContextExecutableTrace(ctx) } // WithExecutableTrace takes a Context and an ExecutableTrace, and returns a // Context with the ExecutableTrace added as a Value. If the Context has a // previously added trace, the hooks defined in the new trace will be added // in addition to the previous ones. The recent hooks will be called first. -func WithExecutableTrace(ctx context.Context, trace *ExecutableTrace) context.Context { - if trace == nil { - return ctx - } - if oldTrace := ContextExecutableTrace(ctx); oldTrace != nil { - trace.compose(oldTrace) - } - return context.WithValue(ctx, executableTraceContextKey{}, trace) -} - -// compose takes an oldTrace and modifies the existing trace to include -// the hooks defined in the oldTrace. The hooks in the existing trace will -// be called first. -func (trace *ExecutableTrace) compose(oldTrace *ExecutableTrace) { - if oldStart := oldTrace.ExecuteStart; oldStart != nil { - start := trace.ExecuteStart - if start != nil { - trace.ExecuteStart = func(executableName, action string) { - start(executableName, action) - oldStart(executableName, action) - } - } else { - trace.ExecuteStart = oldStart - } - } - if oldDone := oldTrace.ExecuteDone; oldDone != nil { - done := trace.ExecuteDone - if done != nil { - trace.ExecuteDone = func(executableName, action string, err error) { - done(executableName, action, err) - oldDone(executableName, action, err) - } - } else { - trace.ExecuteDone = oldDone - } - } +// +// Deprecated: This function now simply calls [trace.WithExecutableTrace] of oras-go. +// +// [trace.WithExecutableTrace]: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials/trace#WithExecutableTrace +func WithExecutableTrace(ctx context.Context, et *ExecutableTrace) context.Context { + return trace.WithExecutableTrace(ctx, et) }