Skip to content

Commit

Permalink
feat: allow loading config from io.Reader
Browse files Browse the repository at this point in the history
Signed-off-by: Jakub Warczarek <[email protected]>
  • Loading branch information
programmer04 committed Nov 27, 2024
1 parent a80f219 commit 52b94a7
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 17 deletions.
6 changes: 3 additions & 3 deletions registry/remote/credentials/file_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand Down
44 changes: 34 additions & 10 deletions registry/remote/credentials/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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 != "" {
Expand Down
58 changes: 55 additions & 3 deletions registry/remote/credentials/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion registry/remote/credentials/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 52b94a7

Please sign in to comment.