Skip to content
This repository has been archived by the owner on Apr 7, 2024. It is now read-only.

Commit

Permalink
feat: support creating store from the default Docker config file (#52)
Browse files Browse the repository at this point in the history
Resolves: #34
Signed-off-by: Sylvia Lei <[email protected]>
  • Loading branch information
Wwwsylvia authored Apr 24, 2023
1 parent 5458be0 commit ba6b33c
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 2 deletions.
4 changes: 4 additions & 0 deletions file_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (

// 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.
Expand All @@ -45,6 +47,8 @@ var (
)

// NewFileStore creates a new file credentials store.
//
// Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
func NewFileStore(configPath string) (*FileStore, error) {
cfg, err := config.Load(configPath)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ func (ac AuthConfig) Credential() (auth.Credential, error) {
}

// Config represents a docker configuration file.
// Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L44
// 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
Expand Down
45 changes: 44 additions & 1 deletion store.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ 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"
)

// Store is the interface that any credentials store must implement.
type Store interface {
// Get retrieves credentials from the store for the given server address.
Expand Down Expand Up @@ -69,7 +77,9 @@ type StoreOptions struct {
// - Linux: "pass" or "secretservice"
// - macOS: "osxkeychain"
//
// Reference: https://docs.docker.com/engine/reference/commandline/login/#credentials-store
// 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
func NewStore(configPath string, opts StoreOptions) (Store, error) {
cfg, err := config.Load(configPath)
if err != nil {
Expand All @@ -86,6 +96,24 @@ func NewStore(configPath string, opts StoreOptions) (Store, error) {
return ds, nil
}

// NewStoreFromDocker returns a Store based on the default docker config file.
// - If the $DOCKER_CONFIG environment variable is set,
// $DOCKER_CONFIG/config.json will be used.
// - Otherwise, the default location $HOME/.docker/config.json will be used.
//
// NewStoreFromDocker internally calls [credentials.NewStore].
//
// 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) (Store, 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)
Expand Down Expand Up @@ -140,6 +168,21 @@ func (ds *dynamicStore) getStore(serverAddress string) Store {
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
Expand Down
110 changes: 110 additions & 0 deletions store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,113 @@ func TestStoreWithFallbacks(t *testing.T) {
t.Fatal("incorrect credential after the delete")
}
}

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()
configPath := filepath.Join(tempDir, "config.json")
t.Setenv("DOCKER_CONFIG", tempDir)

serverAddr1 := "test.example.com"
cred1 := auth.Credential{
Username: "foo",
Password: "bar",
}
config := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
serverAddr1: {
Auth: "Zm9vOmJhcg==",
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(config)
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)
}

ctx := context.Background()

ds, err := NewStoreFromDocker(StoreOptions{AllowPlaintextPut: true})
if err != nil {
t.Fatal("NewStoreFromDocker() error =", err)
}

// test getting an existing credential
got, err := ds.Get(ctx, serverAddr1)
if err != nil {
t.Fatal("dynamicStore.Get() error =", err)
}
if want := cred1; got != want {
t.Errorf("dynamicStore.Get() = %v, want %v", got, want)
}

// test putting a new credential
serverAddr2 := "newtest.example.com"
cred2 := auth.Credential{
Username: "username",
Password: "password",
}
if err := ds.Put(ctx, serverAddr2, cred2); err != nil {
t.Fatal("dynamicStore.Get() error =", err)
}

// test getting the new credential
got, err = ds.Get(ctx, serverAddr2)
if err != nil {
t.Fatal("dynamicStore.Get() error =", err)
}
if want := cred2; got != want {
t.Errorf("dynamicStore.Get() = %v, want %v", got, want)
}

// test deleting the old credential
err = ds.Delete(ctx, serverAddr1)
if err != nil {
t.Fatal("dynamicStore.Delete() error =", err)
}

// verify delete
got, err = ds.Get(ctx, serverAddr1)
if err != nil {
t.Fatal("dynamicStore.Get() error =", err)
}
if want := auth.EmptyCredential; got != want {
t.Errorf("dynamicStore.Get() = %v, want %v", got, want)
}
}

0 comments on commit ba6b33c

Please sign in to comment.