Skip to content

Commit

Permalink
Merge pull request #4 from jamestelfer/list-implementation
Browse files Browse the repository at this point in the history
feat: implement list function
  • Loading branch information
jamestelfer authored Aug 6, 2024
2 parents 37cd9bf + 06cd968 commit a2a1702
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 10 deletions.
71 changes: 65 additions & 6 deletions helper/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ var ErrNotImplemented = errors.New("not implemented")

var nonAlphanumericPattern = regexp.MustCompile(`[^a-zA-Z0-9]`)

const (
variablePrefix = "DOCKER_CREDENTIALS_ENV"
userSuffix = "USER"
passwordSuffix = "PASSWORD"

defaultRegistryUrl = "https://index.docker.io/v1"
)

// must implement the Helper interface
var _ credentials.Helper = EnvHelper{}

Expand Down Expand Up @@ -55,17 +63,16 @@ func (e EnvHelper) Delete(serverURL string) error {
}

func (e EnvHelper) List() (map[string]string, error) {
slog.Error("List action not implemented", "action", "list")
return nil, ErrNotImplemented
return listCredentialsForEnvironment(), nil
}

// credentialsForServer uses the normalized server URL to lookup a pair of environment variables.
func credentialsForServer(serverURL string) (string, string, error) {
normalizedServerName := normalizeServerName(serverURL)

// generate the names, used later in the error message if needed
userEnv := envVarName(normalizedServerName, "USER")
passwordEnv := envVarName(normalizedServerName, "PASSWORD")
userEnv := envVarName(normalizedServerName, userSuffix)
passwordEnv := envVarName(normalizedServerName, passwordSuffix)

// user must have a value, password must be present but may be empty
user, _ := os.LookupEnv(userEnv)
Expand All @@ -89,7 +96,7 @@ func normalizeServerName(serverURL string) string {

// Special case for the default index. This is passed as a URL, where no other
// server is allowed to use this format.
if strings.HasPrefix(serverURL, "https://index.docker.io/v1") {
if strings.HasPrefix(serverURL, defaultRegistryUrl) {
return "INDEX_DOCKER_IO"
}

Expand All @@ -103,5 +110,57 @@ func normalizeServerName(serverURL string) string {

// envVarName looks up the environment variable with the given suffix using the normalized server name.
func envVarName(normalizedServerName string, suffix string) string {
return fmt.Sprintf("DOCKER_CREDENTIALS_ENV_%s_%s", normalizedServerName, suffix)
return fmt.Sprintf("%s_%s_%s", variablePrefix, normalizedServerName, suffix)
}

// listCredentialsForEnvironment returns a map of server URLs to their
// credentials based on the current environment. It will only return credentials
// for servers that have correctly formed environment variables for user and
// password.
func listCredentialsForEnvironment() map[string]string {
// Get all environment variables that start with the prefix
prefix := fmt.Sprintf("%s_", variablePrefix)
suffix := fmt.Sprintf("_%s", userSuffix)

servers := map[string]string{}

vars := os.Environ()
for _, v := range vars {
key, val, found := strings.Cut(v, "=")
if !found {
continue
}

serverURL := key

serverURL, found = strings.CutPrefix(serverURL, prefix)
if !found {
continue
}

serverURL, found = strings.CutSuffix(serverURL, suffix)
if !found {
continue
}

if _, _, err := credentialsForServer(serverURL); err != nil {
continue
}

serverURL = toHostname(serverURL)

// Special case. Docker always uses the full URL for the default registry,
// even when supplied to the CLI in short form.
if serverURL == "index.docker.io" {
serverURL = defaultRegistryUrl
}

servers[serverURL] = val
}

return servers
}

func toHostname(serverURL string) string {
return strings.ToLower(strings.ReplaceAll(serverURL, "_", "."))
}
99 changes: 95 additions & 4 deletions helper/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ import (
"github.com/stretchr/testify/assert"
)

type entry struct {
url string
user string
password string
omitPassword bool
}

func (e entry) Set(t *testing.T) {
normalized := normalizeServerName(e.url)
t.Setenv(envVarName(normalized, userSuffix), e.user)
if !e.omitPassword {
t.Setenv(envVarName(normalized, passwordSuffix), e.password)
}
}

func TestEnvHelper_Get_Success(t *testing.T) {
setTestEnv(t, "DOCKER_CREDENTIALS_ENV_EXAMPLE_COM_USER", "testuser")
setTestEnv(t, "DOCKER_CREDENTIALS_ENV_EXAMPLE_COM_PASSWORD", "testpassword")
Expand Down Expand Up @@ -54,10 +69,86 @@ func TestEnvHelper_Delete_FailsSilently(t *testing.T) {
assert.NoError(t, err)
}

func TestEnvHelper_List_NotImplemented(t *testing.T) {
helper := EnvHelper{}
_, err := helper.List()
assert.ErrorIs(t, err, ErrNotImplemented)
func TestEnvHelper_List(t *testing.T) {
// empty with none
// users where supplied
// skip when password is missing

tests := []struct {
name string
entries []entry
env map[string]string
expected map[string]string
}{
{
name: "normal",
entries: []entry{
{url: "example.com", user: "testuser", password: "testpassword"},
{url: defaultRegistryUrl, user: "hubuser", password: "hubpassword"},
},
expected: map[string]string{
"example.com": "testuser",
defaultRegistryUrl: "hubuser",
},
},
{
name: "default registry",
entries: []entry{
{url: defaultRegistryUrl, user: "hubuser", password: "hubpassword"},
},
expected: map[string]string{
defaultRegistryUrl: "hubuser",
},
},
{
name: "missing password",
entries: []entry{
{url: "nopassword.test", user: "missing", omitPassword: true},
},
expected: map[string]string{},
},
{
name: "empty user",
entries: []entry{
{url: "example.com", user: "", password: "testpassword"},
},
expected: map[string]string{},
},
{
name: "malformed env",
entries: []entry{},
env: map[string]string{
// these env entries are malformed and should be ignored
"DOCKER_CREDENTIALS_ENV_EXAMPLE_COM_USR": "testuser",
"DOCKER_CREDENTIALS_ENV_EXAMPLE_COM_PASSWORD": "testpassword",
"DOCKER_CREDENTIALS_ENV_INDEX_DOCKER_IO_USER": "hubuser",
"DOCKER_CREDENTIALS_ENV_INDEX_DOCKER_IO_PASS": "hubpassword",
"DOCKER_CREDENTIALS_ENV_EXAMPLE2_COM_USERS": "testuser",
"DOCKER_CREDENTIALS_ENV_EXAMPLE2_COM_PASSWORD": "testpassword",
},
expected: map[string]string{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, e := range test.entries {
e.Set(t)
}
if test.env != nil {
for k, v := range test.env {
t.Setenv(k, v)
}
}

helper := EnvHelper{}
r, err := helper.List()

assert.NoError(t, err)
assert.Equal(t, test.expected, r)
})
}

}

func TestCredentialsForServerSuccess(t *testing.T) {
Expand Down

0 comments on commit a2a1702

Please sign in to comment.