diff --git a/registry/remote/credentials/file_store.go b/registry/remote/credentials/file_store.go index 7664cc2a..b2ae7274 100644 --- a/registry/remote/credentials/file_store.go +++ b/registry/remote/credentials/file_store.go @@ -54,11 +54,11 @@ func NewFileStore(configPath string) (*FileStore, error) { if err != nil { return nil, err } - return newFileStore(cfg), nil + return NewFileStoreFromConfig(cfg), nil } -// newFileStore creates a file credentials store based on the given config instance. -func newFileStore(cfg *config.Config) *FileStore { +// NewFileStoreFromConfig creates a file credentials store based on the given config instance. +func NewFileStoreFromConfig(cfg *config.Config) *FileStore { return &FileStore{config: cfg} } diff --git a/registry/remote/credentials/internal/config/config.go b/registry/remote/credentials/internal/config/config.go index 20ee0743..0f5755b9 100644 --- a/registry/remote/credentials/internal/config/config.go +++ b/registry/remote/credentials/internal/config/config.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "strings" @@ -93,7 +94,7 @@ func (ac AuthConfig) Credential() (auth.Credential, error) { // - 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 is the path to the config file. When config was loaded from memory, path is empty. path string // rwLock is a read-write-lock for the file store. rwLock sync.RWMutex @@ -113,11 +114,11 @@ type Config struct { // 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 := &Config{path: configPath} cfg.content = make(map[string]json.RawMessage) cfg.authsCache = make(map[string]json.RawMessage) return cfg, nil @@ -126,9 +127,21 @@ func Load(configPath string) (*Config, error) { } 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) + cfg, err := LoadFromReader(configFile) + if err != nil { + return nil, err + } + cfg.path = configPath + return cfg, nil +} + +// LoadFromReader loads Config from the given io.Reader. +// Path can be set by SetPath to save the config, +// otherwise all changes happen in memory. +func LoadFromReader(r io.Reader) (*Config, error) { + cfg := &Config{} + if err := json.NewDecoder(r).Decode(&cfg.content); err != nil { + return nil, fmt.Errorf("failed to decode config file: %w: %v", ErrInvalidConfigFormat, err) } if credsStoreBytes, ok := cfg.content[configFieldCredentialsStore]; ok { @@ -195,7 +208,7 @@ func (cfg *Config) PutCredential(serverAddress string, cred auth.Credential) err return fmt.Errorf("failed to marshal auth field: %w", err) } cfg.authsCache[serverAddress] = authCfgBytes - return cfg.saveFile() + return cfg.SaveFile() } // DeleteAuthConfig deletes the corresponding credential for serverAddress. @@ -208,7 +221,7 @@ func (cfg *Config) DeleteCredential(serverAddress string) error { return nil } delete(cfg.authsCache, serverAddress) - return cfg.saveFile() + return cfg.SaveFile() } // GetCredentialHelper returns the credential helpers for serverAddress. @@ -225,17 +238,23 @@ func (cfg *Config) CredentialsStore() string { } // Path returns the path to the config file. +// It's empty if the config was loaded from memory. func (cfg *Config) Path() string { return cfg.path } +// SetPath sets the path to the config file. +func (cfg *Config) SetPath(path string) { + cfg.path = path +} + // 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() + return cfg.SaveFile() } // IsAuthConfigured returns whether there is authentication configured in this @@ -246,8 +265,13 @@ func (cfg *Config) IsAuthConfigured() bool { len(cfg.authsCache) > 0 } -// saveFile saves Config into the file. -func (cfg *Config) saveFile() (returnErr error) { +// SaveFile saves Config into the file. +// In case when the Path() returns empty, it does nothing. +func (cfg *Config) SaveFile() (returnErr error) { + if cfg.path == "" { + return nil + } + // marshal content // credentialHelpers is skipped as it's never set if cfg.credentialsStore != "" { diff --git a/registry/remote/credentials/internal/config/config_test.go b/registry/remote/credentials/internal/config/config_test.go index a18eccac..a662d994 100644 --- a/registry/remote/credentials/internal/config/config_test.go +++ b/registry/remote/credentials/internal/config/config_test.go @@ -18,6 +18,7 @@ package config import ( "encoding/json" "errors" + "fmt" "os" "path/filepath" "reflect" @@ -80,13 +81,64 @@ func TestLoad_badFormat(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(fmt.Sprintf("%s-from-file", 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 } }) + t.Run(fmt.Sprintf("%s-from-reader", tt.name), func(t *testing.T) { + r, err := os.Open(tt.configPath) + if err != nil { + t.Fatal("failed to open test file:", err) + } + defer r.Close() + _, err = LoadFromReader(r) + if (err != nil) != tt.wantErr { + t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestLoadFromReader_setPathAndSave(t *testing.T) { + const testCfgPath = "../../testdata/valid_auths_config.json" + r, err := os.Open(testCfgPath) + if err != nil { + t.Fatal("failed to open test file:", err) + } + defer r.Close() + cfg, err := LoadFromReader(r) + if err != nil { + t.Fatal("LoadFromReader() error =", err) + } + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "config.json") + cfg.SetPath(configPath) + cfg.SaveFile() + if cfg.Path() != configPath { + t.Errorf("Config.Path() = %s, want %s", cfg.Path(), configPath) + } + + // Verify content. + orgContent, err := os.ReadFile(testCfgPath) + if err != nil { + t.Fatalf("failed to read original config file: %v", err) + } + var orgCfg configtest.Config + json.Unmarshal(orgContent, &orgCfg) + + savedContent, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("failed to read saved config file: %v", err) + } + var savedCfg configtest.Config + json.Unmarshal(savedContent, &savedCfg) + + if !reflect.DeepEqual(orgCfg, savedCfg) { + t.Errorf("Saved config = %v, want %v", savedCfg, orgCfg) } } @@ -1278,8 +1330,8 @@ func TestConfig_saveFile(t *testing.T) { } cfg.credentialsStore = tt.newCfg.CredentialsStore cfg.credentialHelpers = tt.newCfg.CredentialHelpers - if err := cfg.saveFile(); err != nil { - t.Fatal("saveFile() error =", err) + if err := cfg.SaveFile(); err != nil { + t.Fatal("SaveFile() error =", err) } // verify config file diff --git a/registry/remote/credentials/store.go b/registry/remote/credentials/store.go index e26a98ae..2367901f 100644 --- a/registry/remote/credentials/store.go +++ b/registry/remote/credentials/store.go @@ -193,7 +193,7 @@ func (ds *DynamicStore) getStore(serverAddress string) Store { return NewNativeStore(helper) } - fs := newFileStore(ds.config) + fs := NewFileStoreFromConfig(ds.config) fs.DisablePut = !ds.options.AllowPlaintextPut return fs }