mirror of https://github.com/containers/image.git
Merge pull request #1288 from saschagrunert/login-logout-tests
Support updating registry credentials scoped to namespaces/repos
This commit is contained in:
commit
d695b98f83
|
|
@ -21,14 +21,15 @@ Except the primary (read/write) file, other files are read-only, unless the user
|
||||||
The auth.json file stores encrypted authentication information for the
|
The auth.json file stores encrypted authentication information for the
|
||||||
user to container image registries. The file can have zero to many entries and
|
user to container image registries. The file can have zero to many entries and
|
||||||
is created by a `login` command from a container tool such as `podman login`,
|
is created by a `login` command from a container tool such as `podman login`,
|
||||||
`buildah login` or `skopeo login`. Each entry includes the name of the registry and then an auth
|
`buildah login` or `skopeo login`. Each entry either contains a single
|
||||||
token in the form of a base64 encoded string from the concatenation of the
|
hostname (e.g. `docker.io`) or a namespace (e.g. `quay.io/user/image`) as a key
|
||||||
username, a colon, and the password. The registry name can additionally contain
|
and an auth token in the form of a base64 encoded string as value of `auth`. The
|
||||||
a path or repository name (an image name without tag or digest). The path (or
|
token is built from the concatenation of the username, a colon, and the
|
||||||
namespace) is matched in its hierarchical order when checking for available
|
password. The registry name can additionally contain a repository name (an image
|
||||||
authentications. For example, an image pull for
|
name without tag or digest) and namespaces. The path (or namespace) is matched
|
||||||
`my-registry.local/namespace/user/image:latest` will result in a lookup in
|
in its hierarchical order when checking for available authentications. For
|
||||||
`auth.json` in the following order:
|
example, an image pull for `my-registry.local/namespace/user/image:latest` will
|
||||||
|
result in a lookup in `auth.json` in the following order:
|
||||||
|
|
||||||
- `my-registry.local/namespace/user/image`
|
- `my-registry.local/namespace/user/image`
|
||||||
- `my-registry.local/namespace/user`
|
- `my-registry.local/namespace/user`
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,17 @@ var (
|
||||||
|
|
||||||
// SetCredentials stores the username and password in a location
|
// SetCredentials stores the username and password in a location
|
||||||
// appropriate for sys and the users’ configuration.
|
// appropriate for sys and the users’ configuration.
|
||||||
|
// A valid key can be either a registry hostname or additionally a namespace if
|
||||||
|
// the AuthenticationFileHelper is being unsed.
|
||||||
// Returns a human-redable description of the location that was updated.
|
// Returns a human-redable description of the location that was updated.
|
||||||
// NOTE: The return value is only intended to be read by humans; its form is not an API,
|
// NOTE: The return value is only intended to be read by humans; its form is not an API,
|
||||||
// it may change (or new forms can be added) any time.
|
// it may change (or new forms can be added) any time.
|
||||||
func SetCredentials(sys *types.SystemContext, registry, username, password string) (string, error) {
|
func SetCredentials(sys *types.SystemContext, key, username, password string) (string, error) {
|
||||||
|
isNamespaced, err := validateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
helpers, err := sysregistriesv2.CredentialHelpers(sys)
|
helpers, err := sysregistriesv2.CredentialHelpers(sys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -72,33 +79,45 @@ func SetCredentials(sys *types.SystemContext, registry, username, password strin
|
||||||
// Special-case the built-in helpers for auth files.
|
// Special-case the built-in helpers for auth files.
|
||||||
case sysregistriesv2.AuthenticationFileHelper:
|
case sysregistriesv2.AuthenticationFileHelper:
|
||||||
desc, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
|
desc, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
|
||||||
if ch, exists := auths.CredHelpers[registry]; exists {
|
if ch, exists := auths.CredHelpers[key]; exists {
|
||||||
return false, setAuthToCredHelper(ch, registry, username, password)
|
if isNamespaced {
|
||||||
|
return false, unsupportedNamespaceErr(ch)
|
||||||
|
}
|
||||||
|
return false, setAuthToCredHelper(ch, key, username, password)
|
||||||
}
|
}
|
||||||
creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||||
newCreds := dockerAuthConfig{Auth: creds}
|
newCreds := dockerAuthConfig{Auth: creds}
|
||||||
auths.AuthConfigs[registry] = newCreds
|
auths.AuthConfigs[key] = newCreds
|
||||||
return true, nil
|
return true, nil
|
||||||
})
|
})
|
||||||
// External helpers.
|
// External helpers.
|
||||||
default:
|
default:
|
||||||
desc = fmt.Sprintf("credential helper: %s", helper)
|
if isNamespaced {
|
||||||
err = setAuthToCredHelper(helper, registry, username, password)
|
err = unsupportedNamespaceErr(helper)
|
||||||
|
} else {
|
||||||
|
desc = fmt.Sprintf("credential helper: %s", helper)
|
||||||
|
err = setAuthToCredHelper(helper, key, username, password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
multiErr = multierror.Append(multiErr, err)
|
multiErr = multierror.Append(multiErr, err)
|
||||||
logrus.Debugf("Error storing credentials for %s in credential helper %s: %v", registry, helper, err)
|
logrus.Debugf("Error storing credentials for %s in credential helper %s: %v", key, helper, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logrus.Debugf("Stored credentials for %s in credential helper %s", registry, helper)
|
logrus.Debugf("Stored credentials for %s in credential helper %s", key, helper)
|
||||||
return desc, nil
|
return desc, nil
|
||||||
}
|
}
|
||||||
return "", multiErr
|
return "", multiErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unsupportedNamespaceErr(helper string) error {
|
||||||
|
return errors.Errorf("namespaced key is not supported for credential helper %s", helper)
|
||||||
|
}
|
||||||
|
|
||||||
// SetAuthentication stores the username and password in the credential helper or file
|
// SetAuthentication stores the username and password in the credential helper or file
|
||||||
func SetAuthentication(sys *types.SystemContext, registry, username, password string) error {
|
// See the documentation of SetCredentials for format of "key"
|
||||||
_, err := SetCredentials(sys, registry, username, password)
|
func SetAuthentication(sys *types.SystemContext, key, username, password string) error {
|
||||||
|
_, err := SetCredentials(sys, key, username, password)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,9 +345,16 @@ func getAuthenticationWithHomeDir(sys *types.SystemContext, registry, homeDir st
|
||||||
return auth.Username, auth.Password, nil
|
return auth.Username, auth.Password, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveAuthentication removes credentials for `registry` from all possible
|
// RemoveAuthentication removes credentials for `key` from all possible
|
||||||
// sources such as credential helpers and auth files.
|
// sources such as credential helpers and auth files.
|
||||||
func RemoveAuthentication(sys *types.SystemContext, registry string) error {
|
// A valid key can be either a registry hostname or additionally a namespace if
|
||||||
|
// the AuthenticationFileHelper is being unsed.
|
||||||
|
func RemoveAuthentication(sys *types.SystemContext, key string) error {
|
||||||
|
isNamespaced, err := validateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
helpers, err := sysregistriesv2.CredentialHelpers(sys)
|
helpers, err := sysregistriesv2.CredentialHelpers(sys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -338,17 +364,22 @@ func RemoveAuthentication(sys *types.SystemContext, registry string) error {
|
||||||
isLoggedIn := false
|
isLoggedIn := false
|
||||||
|
|
||||||
removeFromCredHelper := func(helper string) {
|
removeFromCredHelper := func(helper string) {
|
||||||
err := deleteAuthFromCredHelper(helper, registry)
|
if isNamespaced {
|
||||||
if err == nil {
|
logrus.Debugf("Not removing credentials because namespaced keys are not supported for the credential helper: %s", helper)
|
||||||
logrus.Debugf("Credentials for %q were deleted from credential helper %s", registry, helper)
|
|
||||||
isLoggedIn = true
|
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
err := deleteAuthFromCredHelper(helper, key)
|
||||||
|
if err == nil {
|
||||||
|
logrus.Debugf("Credentials for %q were deleted from credential helper %s", key, helper)
|
||||||
|
isLoggedIn = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
|
||||||
|
logrus.Debugf("Not logged in to %s with credential helper %s", key, helper)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
|
multiErr = multierror.Append(multiErr, errors.Wrapf(err, "removing credentials for %s from credential helper %s", key, helper))
|
||||||
logrus.Debugf("Not logged in to %s with credential helper %s", registry, helper)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
multiErr = multierror.Append(multiErr, errors.Wrapf(err, "removing credentials for %s from credential helper %s", registry, helper))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, helper := range helpers {
|
for _, helper := range helpers {
|
||||||
|
|
@ -357,15 +388,12 @@ func RemoveAuthentication(sys *types.SystemContext, registry string) error {
|
||||||
// Special-case the built-in helper for auth files.
|
// Special-case the built-in helper for auth files.
|
||||||
case sysregistriesv2.AuthenticationFileHelper:
|
case sysregistriesv2.AuthenticationFileHelper:
|
||||||
_, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
|
_, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
|
||||||
if innerHelper, exists := auths.CredHelpers[registry]; exists {
|
if innerHelper, exists := auths.CredHelpers[key]; exists {
|
||||||
removeFromCredHelper(innerHelper)
|
removeFromCredHelper(innerHelper)
|
||||||
}
|
}
|
||||||
if _, ok := auths.AuthConfigs[registry]; ok {
|
if _, ok := auths.AuthConfigs[key]; ok {
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
delete(auths.AuthConfigs, registry)
|
delete(auths.AuthConfigs, key)
|
||||||
} else if _, ok := auths.AuthConfigs[normalizeRegistry(registry)]; ok {
|
|
||||||
isLoggedIn = true
|
|
||||||
delete(auths.AuthConfigs, normalizeRegistry(registry))
|
|
||||||
}
|
}
|
||||||
return true, multiErr
|
return true, multiErr
|
||||||
})
|
})
|
||||||
|
|
@ -699,18 +727,18 @@ func decodeDockerAuth(conf dockerAuthConfig) (types.DockerAuthConfig, error) {
|
||||||
// to just an hostname.
|
// to just an hostname.
|
||||||
// Copied from github.com/docker/docker/registry/auth.go
|
// Copied from github.com/docker/docker/registry/auth.go
|
||||||
func convertToHostname(url string) string {
|
func convertToHostname(url string) string {
|
||||||
stripped := url
|
stripped := stripScheme(url)
|
||||||
if strings.HasPrefix(url, "http://") {
|
|
||||||
stripped = strings.TrimPrefix(url, "http://")
|
|
||||||
} else if strings.HasPrefix(url, "https://") {
|
|
||||||
stripped = strings.TrimPrefix(url, "https://")
|
|
||||||
}
|
|
||||||
|
|
||||||
nameParts := strings.SplitN(stripped, "/", 2)
|
nameParts := strings.SplitN(stripped, "/", 2)
|
||||||
|
|
||||||
return nameParts[0]
|
return nameParts[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stripScheme striped the http|https scheme from the provided URL.
|
||||||
|
func stripScheme(url string) string {
|
||||||
|
stripped := strings.TrimPrefix(url, "http://")
|
||||||
|
stripped = strings.TrimPrefix(stripped, "https://")
|
||||||
|
return stripped
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeRegistry(registry string) string {
|
func normalizeRegistry(registry string) string {
|
||||||
normalized := convertToHostname(registry)
|
normalized := convertToHostname(registry)
|
||||||
switch normalized {
|
switch normalized {
|
||||||
|
|
@ -719,3 +747,14 @@ func normalizeRegistry(registry string) string {
|
||||||
}
|
}
|
||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateKey verifies that the input key does not have a prefix that is not
|
||||||
|
// allowed and returns an indicator if the key is namespaced.
|
||||||
|
func validateKey(key string) (isNamespaced bool, err error) {
|
||||||
|
if strings.HasPrefix(key, "http://") || strings.HasPrefix(key, "https://") {
|
||||||
|
return isNamespaced, errors.Errorf("key %s contains http[s]:// prefix", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the provided key contains one or more subpaths.
|
||||||
|
return strings.ContainsRune(key, '/'), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -675,3 +676,213 @@ func TestAuthKeysForRef(t *testing.T) {
|
||||||
require.Equal(t, tc.expected, result, tc.name)
|
require.Equal(t, tc.expected, result, tc.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetCredentials(t *testing.T) {
|
||||||
|
const (
|
||||||
|
usernamePrefix = "username-"
|
||||||
|
passwordPrefix = "password-"
|
||||||
|
)
|
||||||
|
getAuth := func(sys *types.SystemContext, input string) types.DockerAuthConfig {
|
||||||
|
ref, err := reference.ParseNamed(input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
auth, err := GetCredentialsForRef(sys, ref)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
input []string
|
||||||
|
assert func(*types.SystemContext, dockerConfigFile)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: []string{"quay.io"},
|
||||||
|
assert: func(sys *types.SystemContext, auth dockerConfigFile) {
|
||||||
|
assert.Len(t, auth.AuthConfigs, 1)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io"].Auth)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []string{"quay.io/a/b/c/d/image"},
|
||||||
|
assert: func(sys *types.SystemContext, auth dockerConfigFile) {
|
||||||
|
assert.Len(t, auth.AuthConfigs, 1)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io/a/b/c/d/image"].Auth)
|
||||||
|
|
||||||
|
ta := getAuth(sys, "quay.io/a/b/c/d/image")
|
||||||
|
assert.Equal(t, usernamePrefix+"0", ta.Username)
|
||||||
|
assert.Equal(t, passwordPrefix+"0", ta.Password)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []string{
|
||||||
|
"quay.io/a/b/c",
|
||||||
|
"quay.io/a/b",
|
||||||
|
"quay.io/a",
|
||||||
|
"quay.io",
|
||||||
|
"my-registry.local",
|
||||||
|
"my-registry.local",
|
||||||
|
},
|
||||||
|
assert: func(sys *types.SystemContext, auth dockerConfigFile) {
|
||||||
|
assert.Len(t, auth.AuthConfigs, 5)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io/a/b/c"].Auth)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io/a/b"].Auth)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io/a"].Auth)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io"].Auth)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["my-registry.local"].Auth)
|
||||||
|
|
||||||
|
ta0 := getAuth(sys, "quay.io/a/b/c")
|
||||||
|
assert.Equal(t, usernamePrefix+"0", ta0.Username)
|
||||||
|
assert.Equal(t, passwordPrefix+"0", ta0.Password)
|
||||||
|
|
||||||
|
ta1 := getAuth(sys, "quay.io/a/b")
|
||||||
|
assert.Equal(t, usernamePrefix+"1", ta1.Username)
|
||||||
|
assert.Equal(t, passwordPrefix+"1", ta1.Password)
|
||||||
|
|
||||||
|
ta2 := getAuth(sys, "quay.io/a")
|
||||||
|
assert.Equal(t, usernamePrefix+"2", ta2.Username)
|
||||||
|
assert.Equal(t, passwordPrefix+"2", ta2.Password)
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tmpFile, err := ioutil.TempFile("", "auth.json.set")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpFile.Name())
|
||||||
|
|
||||||
|
_, err = tmpFile.WriteString("{}")
|
||||||
|
require.NoError(t, err)
|
||||||
|
sys := &types.SystemContext{AuthFilePath: tmpFile.Name()}
|
||||||
|
|
||||||
|
for i, input := range tc.input {
|
||||||
|
_, err := SetCredentials(
|
||||||
|
sys,
|
||||||
|
input,
|
||||||
|
usernamePrefix+fmt.Sprint(i),
|
||||||
|
passwordPrefix+fmt.Sprint(i),
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := readJSONFile(tmpFile.Name(), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tc.assert(sys, auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAuthentication(t *testing.T) {
|
||||||
|
testAuth := dockerAuthConfig{Auth: "ZXhhbXBsZTpvcmc="}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
config dockerConfigFile
|
||||||
|
inputs []string
|
||||||
|
shouldError bool
|
||||||
|
assert func(dockerConfigFile)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config: dockerConfigFile{
|
||||||
|
AuthConfigs: map[string]dockerAuthConfig{
|
||||||
|
"quay.io": testAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputs: []string{"quay.io"},
|
||||||
|
assert: func(auth dockerConfigFile) {
|
||||||
|
assert.Len(t, auth.AuthConfigs, 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: dockerConfigFile{
|
||||||
|
AuthConfigs: map[string]dockerAuthConfig{
|
||||||
|
"quay.io": testAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputs: []string{"quay.io/user/image"},
|
||||||
|
shouldError: true, // not logged in
|
||||||
|
assert: func(auth dockerConfigFile) {
|
||||||
|
assert.Len(t, auth.AuthConfigs, 1)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io"].Auth)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: dockerConfigFile{
|
||||||
|
AuthConfigs: map[string]dockerAuthConfig{
|
||||||
|
"quay.io": testAuth,
|
||||||
|
"my-registry.local": testAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputs: []string{"my-registry.local"},
|
||||||
|
assert: func(auth dockerConfigFile) {
|
||||||
|
assert.Len(t, auth.AuthConfigs, 1)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io"].Auth)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: dockerConfigFile{
|
||||||
|
AuthConfigs: map[string]dockerAuthConfig{
|
||||||
|
"quay.io/a/b/c": testAuth,
|
||||||
|
"quay.io/a/b": testAuth,
|
||||||
|
"quay.io/a": testAuth,
|
||||||
|
"quay.io": testAuth,
|
||||||
|
"my-registry.local": testAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputs: []string{
|
||||||
|
"quay.io/a/b",
|
||||||
|
"quay.io",
|
||||||
|
"my-registry.local",
|
||||||
|
},
|
||||||
|
assert: func(auth dockerConfigFile) {
|
||||||
|
assert.Len(t, auth.AuthConfigs, 2)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io/a/b/c"].Auth)
|
||||||
|
assert.NotEmpty(t, auth.AuthConfigs["quay.io/a"].Auth)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
|
||||||
|
content, err := json.Marshal(&tc.config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tmpFile, err := ioutil.TempFile("", "auth.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpFile.Name())
|
||||||
|
|
||||||
|
_, err = tmpFile.Write(content)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sys := &types.SystemContext{AuthFilePath: tmpFile.Name()}
|
||||||
|
|
||||||
|
for _, input := range tc.inputs {
|
||||||
|
err := RemoveAuthentication(sys, input)
|
||||||
|
if tc.shouldError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := readJSONFile(tmpFile.Name(), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tc.assert(auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateKey(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
key string
|
||||||
|
shouldError bool
|
||||||
|
isNamespaced bool
|
||||||
|
}{
|
||||||
|
{"my-registry.local", false, false},
|
||||||
|
{"https://my-registry.local", true, false},
|
||||||
|
{"my-registry.local/path", false, true},
|
||||||
|
{"quay.io/a/b/c/d", false, true},
|
||||||
|
} {
|
||||||
|
|
||||||
|
isNamespaced, err := validateKey(tc.key)
|
||||||
|
if tc.shouldError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.isNamespaced, isNamespaced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue