Files
opentf/internal/command/cliconfig/oci_credentials_test.go
2025-10-29 12:03:55 -05:00

804 lines
30 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package cliconfig
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/opentofu/opentofu/internal/command/cliconfig/ociauthconfig"
)
func TestLoadConfig_ociDefaultCredentials(t *testing.T) {
// Due to path handling differences between Windows and Unix-like
// systems, `/foo/bar` is an absolute path on Unix but is converted
// to `\foo\bar`, a relative path on Windows. `filepath.Abs` is used to
// convert the path to an absolute path on Windows.
authJsonPath := "/foo/bar/auth.json"
if runtime.GOOS == "windows" {
authJsonPath, _ = filepath.Abs(filepath.FromSlash("testdata/foo/bar/auth.json"))
}
// The keys in this map correspond to fixture names under
// the "testdata" directory.
tests := map[string]struct {
want *OCIDefaultCredentials
wantErr string
}{
"oci-default-credentials": {
&OCIDefaultCredentials{
DiscoverAmbientCredentials: true,
DockerStyleConfigFiles: []string{
authJsonPath,
},
DefaultDockerCredentialHelper: "osxkeychain",
},
``,
},
"oci-default-credentials.json": {
&OCIDefaultCredentials{
DiscoverAmbientCredentials: true,
DockerStyleConfigFiles: []string{
authJsonPath,
},
DefaultDockerCredentialHelper: "osxkeychain",
},
``,
},
"oci-default-credentials-defaults": {
&OCIDefaultCredentials{
DiscoverAmbientCredentials: true,
DockerStyleConfigFiles: nil, // represents "use the default search paths"
DefaultDockerCredentialHelper: "", // represents no default credential helper at all
},
``,
},
"oci-default-credentials-no-docker": {
&OCIDefaultCredentials{
DiscoverAmbientCredentials: true,
DockerStyleConfigFiles: []string{
// Must be non-nil empty, because nil represents
// "use the default search paths".
},
DefaultDockerCredentialHelper: "", // represents no default credential helper at all
},
``,
},
"oci-default-credentials-inconsistent": {
&OCIDefaultCredentials{
// The following is just a best-effort approximation of the
// configuration despite the errors, so it's not super important
// that it stay consistent in future releases but tested just
// so that if it _does_ change we can review and make sure that
// the change is reasonable.
DiscoverAmbientCredentials: false,
DockerStyleConfigFiles: []string{},
DefaultDockerCredentialHelper: "",
},
`disables discovery of ambient credentials, but also sets docker_style_config_files`,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
fixtureFile := filepath.Join("testdata", name)
gotConfig, diags := loadConfigFile(fixtureFile)
if diags.HasErrors() {
errStr := diags.Err().Error()
if test.wantErr == "" {
t.Errorf("unexpected errors: %s", errStr)
}
if !strings.Contains(errStr, test.wantErr) {
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", test.wantErr, errStr)
}
} else if test.wantErr != "" {
t.Errorf("unexpected success\nwant error with substring: %s", test.wantErr)
}
var got *OCIDefaultCredentials
if len(gotConfig.OCIDefaultCredentials) > 0 {
got = gotConfig.OCIDefaultCredentials[0]
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Error("unexpected result\n" + diff)
}
})
}
t.Run("oci-default-credentials-duplicate", func(t *testing.T) {
// This one is different than all of the others because it
// only gets detected as invalid during the validation step,
// so that (in the normal case) we can check it only after
// we've merged all of the separate CLI config files together.
fixtureFile := filepath.Join("testdata", "oci-default-credentials-duplicate")
gotConfig, loadDiags := loadConfigFile(fixtureFile)
if loadDiags.HasErrors() {
t.Errorf("unexpected errors from loadConfigFile: %s", loadDiags.Err().Error())
}
validateDiags := gotConfig.Validate()
wantErr := `No more than one oci_default_credentials block may be specified`
if !validateDiags.HasErrors() {
t.Fatalf("unexpected success\nwant error with substring: %s", wantErr)
}
if errStr := validateDiags.Err().Error(); !strings.Contains(errStr, wantErr) {
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", wantErr, errStr)
}
})
}
func TestLoadConfig_ociCredentials(t *testing.T) {
// The keys in this map correspond to fixture names under
// the "testdata" directory.
tests := map[string]struct {
want []*OCIRepositoryCredentials
wantErr string
}{
"oci-credentials-basic": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
Username: "foo",
Password: "bar",
},
},
``,
},
"oci-credentials-basic.json": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
Username: "foo",
Password: "bar",
},
},
``,
},
"oci-credentials-oauth": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
AccessToken: "foo",
RefreshToken: "bar",
},
},
``,
},
"oci-credentials-oauth.json": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
AccessToken: "foo",
RefreshToken: "bar",
},
},
``,
},
"oci-credentials-credhelper": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
DockerCredentialHelper: "osxkeychain",
},
},
``,
},
"oci-credentials-credhelper.json": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
DockerCredentialHelper: "osxkeychain",
},
},
``,
},
"oci-credentials-multi": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
Username: "foo",
Password: "bar",
},
{
RepositoryPrefix: "example.net",
Username: "baz",
Password: "beep",
},
},
``,
},
"oci-credentials-multi.json": {
[]*OCIRepositoryCredentials{
{
RepositoryPrefix: "example.com",
Username: "foo",
Password: "bar",
},
{
RepositoryPrefix: "example.net",
Username: "baz",
Password: "beep",
},
},
``,
},
"oci-credentials-empty": {
nil,
`must set either username+password, access_token+refresh_token, or docker_credentials_helper`,
},
"oci-credentials-mixedstyles": {
nil,
`must set only one group out of username+password, access_token+refresh_token, or docker_credentials_helper`,
},
"oci-credentials-basic-nopassword": {
nil,
`must set both username and password together when using static credentials`,
},
"oci-credentials-basic-nousername": {
nil,
`must set both username and password together when using static credentials`,
},
"oci-credentials-oauth-noaccess": {
nil,
`must set both access_token and refresh_token together when using OAuth-style credentials`,
},
"oci-credentials-oauth-norefresh": {
nil,
`must set both access_token and refresh_token together when using OAuth-style credentials`,
},
"oci-credentials-credhelper-badsyntax": {
nil,
`specifies the invalid Docker credential helper name "not/valid"`,
},
"oci-credentials-credhelper-repopath": {
nil,
`cannot set docker_credentials_helper with a repository path`,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
fixtureFile := filepath.Join("testdata", name)
gotConfig, diags := loadConfigFile(fixtureFile)
if diags.HasErrors() {
errStr := diags.Err().Error()
if test.wantErr == "" {
t.Errorf("unexpected errors: %s", errStr)
}
if !strings.Contains(errStr, test.wantErr) {
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", test.wantErr, errStr)
}
} else if test.wantErr != "" {
t.Errorf("unexpected success\nwant error with substring: %s", test.wantErr)
}
got := gotConfig.OCIRepositoryCredentials
if diff := cmp.Diff(test.want, got); diff != "" {
t.Error("unexpected result\n" + diff)
}
})
}
t.Run("oci-credentials-duplicate", func(t *testing.T) {
// This one is different than all of the others because it
// only gets detected as invalid during the validation step,
// so that (in the normal case) we can check it only after
// we've merged all of the separate CLI config files together.
fixtureFile := filepath.Join("testdata", "oci-credentials-duplicate")
gotConfig, loadDiags := loadConfigFile(fixtureFile)
if loadDiags.HasErrors() {
t.Errorf("unexpected errors from loadConfigFile: %s", loadDiags.Err().Error())
}
validateDiags := gotConfig.Validate()
wantErr := `Duplicate oci_credentials block for "example.com"`
if !validateDiags.HasErrors() {
t.Fatalf("unexpected success\nwant error with substring: %s", wantErr)
}
if errStr := validateDiags.Err().Error(); !strings.Contains(errStr, wantErr) {
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", wantErr, errStr)
}
})
}
func TestConfigOCICredentialsPolicy(t *testing.T) {
// This test exercises various different combinations of OCI credentials
// policy configuration, verifying that the correct set of credentials gets
// selected in each case as a limited-scope integration test.
//
// This is the most comprehensive and complicated test of the OCI credentials
// policy functionality, and there are various more-focused unit tests both
// elsewhere in this package and in package ociauthconfig, so if you can test
// whatever you are trying to test in a more direct way using one of those
// other tests that would probably give us more specific feedback if
// something gets regressed in future.
//
// This test also intentionally focuses only on valid configuration cases,
// since the Config.OCICredentialsPolicy method is documented as allowed only
// after having successfully loaded and validated a CLI configuration. To
// test the handling of invalid configuration input, add cases to
// TestLoadConfig_ociDefaultCredentials and TestLoadConfig_ociCredentials instead.
type Subtest struct {
wantSpecificity ociauthconfig.CredentialsSpecificity
wantCredentials *ociauthconfig.Credentials
}
fixturesDir := filepath.Join("testdata", "oci-credentials-policy")
// The keys of the following map correspond to directories under
// testdata/oci-credentials-policy. Each directory should include
// zero or more files named with the ".tfrc" or ".tfrc.json" suffix,
// and can optionally include the following subdirectories:
// - home: represents a fake home directory to use when performing
// discovery of ambient credentials. This directory will be provided
// to the discovery code regardless of whether it exists, with the
// assumption that any test fixture that needs it will include it.
// - xdgconfig: if present, the absolute path to this directory is
// provided to the ambient config discovery code as the
// XDG_CONFIG_HOME environment variable.
// - xdgrun: if present, the absolute path to this directory is
// provided to the ambient config discovery code as the
// XDG_RUNTIME_DIR environment variable.
// Each directory name must end with a dash followed by a GOOS-style
// operating system name, which will be used to influence the
// platform-specific rules in the ambient config discovery logic.
tests := map[string]struct {
// wantConfigLocations is the set of expected config location names as reported
// by the CredentialsConfigLocationForUI method of each CredentialsConfig object.
//
// This is a baseline check to make sure we even discovered what we expected to
// discover, to give more explicit feedback about inconsistencies than we'd get
// just from the subtests, which only _indirectly_ test that the expected config
// locations are present.
wantConfigLocations []string
// The keys of this map are OCI repository addresses like "domainname/repository"
// which should be looked up against the credentials policy represented by the
// configuration of the parent test, and should produce the given results.
subtests map[string]Subtest
}{
"empty-linux": {
wantConfigLocations: nil,
subtests: map[string]Subtest{
"example.com": {
wantCredentials: nil,
},
"example.com/foo/bar": {
wantCredentials: nil,
},
},
},
"mixed-darwin": {
wantConfigLocations: []string{
`explicit oci_credentials "example.com" block`,
`explicit oci_credentials "example.com/foo" block`,
`oci_default_credentials block`,
filepath.FromSlash(`home/.config/containers/auth.json`),
},
subtests: map[string]Subtest{
"example.org": {
// We have no blocks for this domain, so the global credential helper "wins".
// If this fails trying to use a credhelper called "superseded-by-explicit-config"
// then we incorrectly preferred the ambient config's setting for this.
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.org")),
},
"example.com": {
// This domain has a conflicting entry in the ambient Docker-style config,
// but the explicit configuration should "win".
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
},
"example.com/foo": {
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo user", "example.com/foo password")),
},
"example.com/bar": {
// These credentials come from the base64-encoded "auth" string in the
// ambient Docker-style config file.
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("ambient-example.com-user", "ambient-password")),
},
"example.com/not-foo": {
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
},
"example.com/not-bar": {
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
},
},
},
"explicit-specificity-linux": {
wantConfigLocations: []string{
`explicit oci_credentials "example.com" block`,
`explicit oci_credentials "example.com/foo" block`,
`explicit oci_credentials "example.com/foo/bar" block`,
`explicit oci_credentials "example.net" block`,
`explicit oci_credentials "example.net/foo" block`,
},
subtests: map[string]Subtest{
"example.org": {
wantCredentials: nil, // no credentials blocks for this domain at all
},
"example.com": {
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
},
"example.com/foo": {
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo user", "example.com/foo password")),
},
"example.com/foo/bar": {
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(2),
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo/bar user", "example.com/foo/bar password")),
},
"example.com/foo/not-bar": {
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo user", "example.com/foo password")),
},
"example.com/not-foo/not-bar": {
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
},
"example.net": {
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
"example.net/foo": {
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
wantCredentials: ptrTo(ociauthconfig.NewOAuthCredentials("example.net/foo access", "example.net/foo refresh")),
},
"example.net/not-foo": {
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"explicit-global-credhelper-linux": {
wantConfigLocations: []string{
"oci_default_credentials block",
},
subtests: map[string]Subtest{
"example.com/foo/bar": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.com")),
},
"example.net": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
// These ambient-global-credhelper- tests are the main way we're exercising
// the code that deals with finding Docker-style configuration files, since
// a global credentials helper gives us a straightforward signal about whether
// we found the file or not. We don't need to duplicate all of these different
// search cases for other kinds of credentials source because we use the
// same discovery code regardless of what kind of configuration we might
// find in the files we find.
//
// Note that the logic for deciding which paths to search for Docker-like
// configuration files has its own unit test in package ociauthconfig, and so
// we don't necessarily need to cover all of the combinations of different file
// paths here too.
"ambient-global-credhelper-xdgconfig-linux": {
wantConfigLocations: []string{
filepath.Join("xdgconfig", "containers", "auth.json"),
},
subtests: map[string]Subtest{
"example.com/foo/bar": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.com")),
},
"example.net": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-xdgrun-linux": {
wantConfigLocations: []string{
filepath.Join("xdgrun", "containers", "auth.json"),
},
subtests: map[string]Subtest{
"example.net": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-xdgdefault-linux": {
wantConfigLocations: []string{
filepath.Join("home", ".config", "containers", "auth.json"),
},
subtests: map[string]Subtest{
"example.net": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-docker-linux": {
wantConfigLocations: []string{
filepath.Join("home", ".docker", "config.json"),
},
subtests: map[string]Subtest{
"example.net": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-dockerlegacy-linux": {
wantConfigLocations: []string{
filepath.Join("home", ".dockercfg"),
},
subtests: map[string]Subtest{
"example.net": {
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-explicitpath-linux": {
wantConfigLocations: []string{
"explicitly-named.json",
},
subtests: map[string]Subtest{
"example.net": {
// This should select the "fake" credential helper from the first
// location above, and thus ignore all of the other credential
// helper names configured in the other configuration files.
// If this fails trying to use a different credentials helper
// then that suggests that we're not respecting file preference order.
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-various-linux": {
wantConfigLocations: []string{
filepath.Join("xdgrun", "containers", "auth.json"),
// home/.config/containers/auth.json is ignored on Linux whenever XDG_CONFIG_HOME is set
filepath.Join("xdgconfig", "containers", "auth.json"),
filepath.Join("home", ".docker", "config.json"),
filepath.Join("home", ".dockercfg"),
},
subtests: map[string]Subtest{
"example.net": {
// This should select the "fake" credential helper from the first
// location above, and thus ignore all of the other credential
// helper names configured in the other configuration files.
// If this fails trying to use a different credentials helper
// then that suggests that we're not respecting file preference order.
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-various-windows": {
wantConfigLocations: []string{
// the xdgrun/containers/auth.json file is ignored on Windows
filepath.Join("home", ".config", "containers", "auth.json"),
filepath.Join("xdgconfig", "containers", "auth.json"),
filepath.Join("home", ".docker", "config.json"),
filepath.Join("home", ".dockercfg"),
},
subtests: map[string]Subtest{
"example.net": {
// This should select the "fake" credential helper from the first
// location above, and thus ignore all of the other credential
// helper names configured in the other configuration files.
// If this fails trying to use a different credentials helper
// then that suggests that we're not respecting file preference order.
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-global-credhelper-various-darwin": {
wantConfigLocations: []string{
// the xdgrun/containers/auth.json file is ignored on macOS
filepath.Join("home", ".config", "containers", "auth.json"),
filepath.Join("xdgconfig", "containers", "auth.json"),
filepath.Join("home", ".docker", "config.json"),
filepath.Join("home", ".dockercfg"),
},
subtests: map[string]Subtest{
"example.net": {
// This should select the "fake" credential helper from the first
// location above, and thus ignore all of the other credential
// helper names configured in the other configuration files.
// If this fails trying to use a different credentials helper
// then that suggests that we're not respecting file preference order.
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
},
},
},
"ambient-totally-disabled-linux": {
wantConfigLocations: nil,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var osName string
if lastDashIdx := strings.LastIndexByte(name, '-'); lastDashIdx == -1 {
t.Fatalf("test name does not include -osname suffix")
} else {
osName = name[lastDashIdx+1:]
}
configDir := filepath.Join(fixturesDir, name)
absConfigDir, err := filepath.Abs(configDir)
if err != nil {
t.Fatalf("can't get absolute path for %s", configDir)
}
cfg, diags := standardConfigLoader().loadConfigDir(configDir)
if diags.HasErrors() {
t.Fatalf("errors loading config: %s", diags.Err().Error())
}
diags = cfg.Validate()
if diags.HasErrors() {
t.Fatalf("invalid config:\n%s", diags.Err().Error())
}
baseDir, err := filepath.Abs(configDir)
if err != nil {
t.Fatalf("cannot make path %q absolute: %s", configDir, err)
}
discoEnv := &fakeOCIConfigDiscoveryEnvironment{
osName: osName,
homePath: filepath.Join(baseDir, "home"),
xdgConfigHome: filepath.Join(baseDir, "xdgconfig"),
xdgRuntimeDir: filepath.Join(baseDir, "xdgrun"),
}
if _, err = os.Stat(discoEnv.xdgConfigHome); os.IsNotExist(err) {
discoEnv.xdgConfigHome = ""
}
if _, err = os.Stat(discoEnv.xdgRuntimeDir); os.IsNotExist(err) {
discoEnv.xdgRuntimeDir = ""
}
policy, err := cfg.ociCredentialsPolicy(t.Context(), discoEnv)
if err != nil {
t.Fatalf("error building credentials policy: %s", err)
}
var gotConfigLocations []string
for _, credCfg := range policy.AllConfigs() {
loc := credCfg.CredentialsConfigLocationForUI()
// Sometimes the locations are absolute file paths, so we'll make a best effort
// to re-relativize them so that our test table doesn't need to deal with variations
// in base directory between different dev environments. If this doesn't work then
// we'll assume it's a non-filepath-based location string, which is fine.
if relLoc, err := filepath.Rel(absConfigDir, loc); err == nil {
loc = relLoc
}
gotConfigLocations = append(gotConfigLocations, loc)
}
if diff := cmp.Diff(test.wantConfigLocations, gotConfigLocations); diff != "" {
t.Error("wrong configuration locations\n" + diff)
}
for subname, subtest := range test.subtests {
t.Run(subname, func(t *testing.T) {
registryDomain, repositoryPath, err := ociauthconfig.ParseRepositoryAddressPrefix(subname)
if err != nil {
t.Fatalf("subtest has invalid repository address: %s", err)
}
credSrc, err := policy.CredentialsSourceForRepository(t.Context(), registryDomain, repositoryPath)
if ociauthconfig.IsCredentialsNotFoundError(err) {
t.Logf("no credentials found: %s", err.Error())
if subtest.wantCredentials != nil {
t.Errorf("no credentials returned, but want %s", spew.Sdump(subtest.wantCredentials))
}
return // successfully found no credentials, as expected
} else if err != nil {
// This test is only for valid cases, so any other error is a test failure.
t.Fatalf("failed to get credentials source: %s", err)
}
if got, want := credSrc.CredentialsSpecificity(), subtest.wantSpecificity; got != want {
t.Errorf("wrong specificity\ngot: %#v\nwant: %#v", got, want)
}
t.Logf("found credentials for %s/%s at specificity %#v", registryDomain, repositoryPath, credSrc.CredentialsSpecificity())
lookupEnv := &fakeOCICredLookupEnvironment{
regDomain: registryDomain,
}
creds, err := credSrc.Credentials(t.Context(), lookupEnv)
if ociauthconfig.IsCredentialsNotFoundError(err) {
t.Logf("no credentials found: %s", err.Error())
} else if err != nil && !ociauthconfig.IsCredentialsNotFoundError(err) {
// This test is only for valid cases, so any other error is a test failure.
t.Fatalf("failed to get credentials source: %s", err)
}
if diff := cmp.Diff(subtest.wantCredentials, &creds, cmpopts.EquateComparable(ociauthconfig.Credentials{})); diff != "" {
t.Error("wrong credentials\n" + diff)
}
})
}
})
}
}
type fakeOCIConfigDiscoveryEnvironment struct {
osName string
homePath string
xdgConfigHome string
xdgRuntimeDir string
}
var _ ociauthconfig.ConfigDiscoveryEnvironment = (*fakeOCIConfigDiscoveryEnvironment)(nil)
// EnvironmentVariableVal implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e *fakeOCIConfigDiscoveryEnvironment) EnvironmentVariableVal(name string) string {
switch name {
case "XDG_CONFIG_HOME":
return e.xdgConfigHome
case "XDG_RUNTIME_DIR":
return e.xdgRuntimeDir
default:
return ""
}
}
// OperatingSystemName implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e *fakeOCIConfigDiscoveryEnvironment) OperatingSystemName() string {
return e.osName
}
// ReadFile implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e *fakeOCIConfigDiscoveryEnvironment) ReadFile(_ context.Context, path string) ([]byte, error) {
// We don't fake out the actual file reads, because we assume that the
// tests will ensure that all of the paths reported by other methods
// are absolute paths pointing into a test fixture directory.
return os.ReadFile(path)
}
// UserHomeDirPath implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e *fakeOCIConfigDiscoveryEnvironment) UserHomeDirPath() string {
return e.homePath
}
type fakeOCICredLookupEnvironment struct {
regDomain string
}
// QueryDockerCredentialHelper implements ociauthconfig.CredentialsLookupEnvironment.
func (f *fakeOCICredLookupEnvironment) QueryDockerCredentialHelper(ctx context.Context, helperName string, serverURL string) (ociauthconfig.DockerCredentialHelperGetResult, error) {
if helperName != "fake" {
return ociauthconfig.DockerCredentialHelperGetResult{}, ociauthconfig.NewCredentialsNotFoundError(fmt.Errorf("only the 'fake' credential helper is available in this testing environment"))
}
if got, want := serverURL, "https://"+f.regDomain; got != want {
return ociauthconfig.DockerCredentialHelperGetResult{}, ociauthconfig.NewCredentialsNotFoundError(fmt.Errorf("fake credentials helper only has credentials for %s", want))
}
return ociauthconfig.DockerCredentialHelperGetResult{
ServerURL: serverURL,
Username: "from-cred-helper",
Secret: "for " + serverURL,
}, nil
}
// ptrTo is a helper to compensate for the fact that Go doesn't allow
// using the '&' operator unless the operand is directly addressable.
//
// Instead then, this function returns a pointer to a copy of the given
// value.
func ptrTo[T any](v T) *T {
return &v
}