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

feat: support creating store from the default Docker config file #52

Merged
merged 4 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}