Files
opentf/internal/command/cliconfig/oci_credentials.go
Martin Atkins 2b56c06447 cliconfig: Config.OCICredentialsPolicy
This new method collects all of the various different settings that
describe the operator's chosen OCI credentials policy and returns a single
object that encapsulates that policy.

This is the method that will, in future commits, be used by package main
to provide the credentials policy to any OCI-registry-related subsystems
using dependency-inversion style.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-03-13 08:19:57 -07:00

783 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"
"iter"
"log"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/hashicorp/hcl"
hclast "github.com/hashicorp/hcl/hcl/ast"
"github.com/opentofu/opentofu/internal/command/cliconfig/ociauthconfig"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// OCICredentialsPolicy returns an object that encapsulates the operator's configured
// OCI credentials policy, including both the explicitly-configured credentials and
// any automatically-discovered "ambient" credentials.
//
// This should be called only on a [Config] where [Config.Validate] was already called
// and returned no error diagnostics. Calling this on an unvalidated or invalid
// configuration produces unspecified results, possibly including panics.
func (c *Config) OCICredentialsPolicy(ctx context.Context) (ociauthconfig.CredentialsConfigs, error) {
return c.ociCredentialsPolicy(ctx, ociCredentialsEnv{})
}
// ociCredentialsPolicy is the implementation of [Config.OCICredentialsPolicy], exposing
// the discovery environment as an argument so that we can fake it for testing purposes.
func (c *Config) ociCredentialsPolicy(ctx context.Context, discoEnv ociauthconfig.ConfigDiscoveryEnvironment) (ociauthconfig.CredentialsConfigs, error) {
var cfgs []ociauthconfig.CredentialsConfig
// If there are any explicitly-configured oci_credentials blocks then they always
// go first so that they can supersede any other configurations that cover the
// same repository paths with equal same precedence.
for _, block := range c.OCIRepositoryCredentials {
cfgs = append(cfgs, ociRepositoryCredentialsConfig{block})
}
var defaultCredentialsBlock *OCIDefaultCredentials
if len(c.OCIDefaultCredentials) > 0 {
if len(c.OCIDefaultCredentials) != 1 {
// [Config.Validate] requires that there must always be either zero or one
// blocks of this type, and this function is documented as only being callable
// on valid configs, so we should not get here.
panic("too many oci_default_credentials blocks")
}
defaultCredentialsBlock = c.OCIDefaultCredentials[0]
} else {
defaultCredentialsBlock = newDefaultOCIDefaultCredentials()
}
discoverAmbientCredentials := defaultCredentialsBlock.DiscoverAmbientCredentials
dockerStyleConfigFiles := defaultCredentialsBlock.DockerStyleConfigFiles
if defaultCredentialsBlock.DefaultDockerCredentialHelper != "" {
cfgs = append(cfgs, ociauthconfig.NewGlobalDockerCredentialHelperCredentialsConfig(
"oci_default_credentials block",
defaultCredentialsBlock.DefaultDockerCredentialHelper,
))
}
if discoverAmbientCredentials {
ambientCfgs, err := discoverAmbientOCICredentials(ctx, dockerStyleConfigFiles, discoEnv)
if err != nil {
return ociauthconfig.CredentialsConfigs{}, fmt.Errorf("discovering ambient OCI registry credentials: %w", err)
}
cfgs = append(cfgs, ambientCfgs...)
}
return ociauthconfig.NewCredentialsConfigs(cfgs), nil
}
func discoverAmbientOCICredentials(ctx context.Context, dockerStyleConfigFiles []string, discoEnv ociauthconfig.ConfigDiscoveryEnvironment) ([]ociauthconfig.CredentialsConfig, error) {
if dockerStyleConfigFiles != nil {
// explicit config file locations
// (non-nil but empty represents completely disabling our search for
// Docker-style configuration files.)
cfgs, err := ociauthconfig.FixedDockerCLIStyleCredentialsConfigs(
ctx,
dockerStyleConfigFiles,
discoEnv,
)
if err != nil {
// If the user explicitly configured search paths then we treat a failure to
// load any of them as an error.
return nil, fmt.Errorf("failed to read Docker-style config files: %w", err)
}
return cfgs, nil
}
// automatic discovery using default search locations
cfgs, err := ociauthconfig.FindDockerCLIStyleCredentialsConfigs(
ctx,
discoEnv,
)
if err != nil {
// If we were just probing in standard locations then we ignore errors because
// these files are not used exclusively by OpenTofu and an anomaly in one of them
// should never prevent someone from using OpenTofu.
log.Printf("[WARN] Problems during OCI registry ambient credentials discovery:\n%s", err.Error())
return nil, nil
}
return cfgs, nil
}
// OCIDefaultCredentials corresponds to one oci_default_credentials block in
// the CLI configuration.
//
// This represents just one part of the overall OCI credentials policy, and so needs
// to be considered in conjunction with all of the OCICredentials objects across
// the CLI configuration too.
type OCIDefaultCredentials struct {
// DiscoverAmbientCredentials decides whether OpenTofu will attempt to find
// credentials "ambiently" in the environment where OpenTofu is running, such
// as searching the conventional locations for Docker-style configuration files.
//
// This defaults to true, but operators can set it to false to completely opt out
// of OpenTofu using credentials from anywhere other than elsewhere in the
// OpenTofu CLI configuration.
DiscoverAmbientCredentials bool
// DockerStyleConfigFiles forces a specific set of filenames to try to use as
// sources of OCI credentials, interpreting them as Docker CLI-style configuration
// files.
//
// If this is nil, OpenTofu uses a default set of search locations mimicking the
// behavior of other tools in the ecosystem such as Podman, Buildah, etc.
//
// If this is non-nil but zero length, it effectively disables using any Docker CLI-style
// configuration files at all, but if DiscoverAmbientCredentials is also true then
// future versions of OpenTofu might try to use other sources of ambient credentials.
//
// This field is always nil if DiscoverAmbientCredentials is false, because this field
// exists only to customize one aspect of the "ambient credentials" discovery behavior.
DockerStyleConfigFiles []string
// The name of a Docker-style credential helper program to use for any domain
// that doesn't have its own specific credential helper configured.
//
// If this is not set then a default credential helper might still be discovered
// from the ambient credentials sources, unless such discovery is disabled using
// the other fields in this struct.
DefaultDockerCredentialHelper string
}
// newDefaultOCIDefaultCredentials returns an [OCIDefaultCredentials] object representing
// the default settings used when no oci_default_credentials blocks are present.
//
// Each call to this function returns a distinct object, so it's safe for the caller
// to modify the result to reflect any customizations.
func newDefaultOCIDefaultCredentials() *OCIDefaultCredentials {
return &OCIDefaultCredentials{
DiscoverAmbientCredentials: true,
DockerStyleConfigFiles: nil,
DefaultDockerCredentialHelper: "",
}
}
// decodeOCIDefaultCredentialsFromConfig uses the HCL AST API directly to
// decode "oci_default_credentials" blocks from the given file.
//
// The overall CLI configuration is only allowed to contain one
// oci_default_credentials block, but the caller deals with that constraint
// separately after searching all of the CLI configuration files.
//
// This uses the HCL AST directly, rather than HCL's decoder, to continue
// our precedent of trying to constrain new features only to what could be
// supported compatibly in a hypothetical future HCL 2-based implementation
// of the CLI configuration language.
//
// Note that this function wants the top-level file object which might or
// might not contain oci_default_credentials blocks, not an oci_default_credentials
// block directly itself.
func decodeOCIDefaultCredentialsFromConfig(hclFile *hclast.File, filename string) ([]*OCIDefaultCredentials, tfdiags.Diagnostics) {
var ret []*OCIDefaultCredentials
var diags tfdiags.Diagnostics
root, ok := hclFile.Node.(*hclast.ObjectList)
if !ok {
// A HCL file that doesn't have an object list at its root is weird, but
// dealing with that is outside the scope of this function.
// (In practice both the native syntax and JSON parsers for HCL force
// the root to be an ObjectList, so we should not get here for any real file.)
return ret, diags
}
for _, block := range root.Items {
if block.Keys[0].Token.Value() != "oci_default_credentials" {
continue
}
// HCL only tracks whether the input was JSON or native syntax inside
// individual tokens, so we'll use our block type token to decide
// and assume that the rest of the block must be written in the same
// syntax, because syntax is a whole-file idea.
const errInvalidSummary = "Invalid oci_default_credentials block"
isJSON := block.Keys[0].Token.JSON
if block.Assign.Line != 0 && !isJSON {
// Seems to be an attribute rather than a block
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("The oci_default_credentials block at %s must not be introduced with an equals sign.", block.Pos()),
))
continue
}
if len(block.Keys) > 1 && !isJSON {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("The oci_default_credentials block at %s must not have any labels.", block.Pos()),
))
continue
}
body, ok := block.Val.(*hclast.ObjectType)
if !ok {
// We can't get in here with native HCL syntax because we
// already checked above that we're using block syntax, but
// if we're reading JSON then our value could potentially be
// anything.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("The oci_default_credentials block at %s must be represented by a JSON object.", block.Pos()),
))
continue
}
result, blockDiags := decodeOCIDefaultCredentialsBlockBody(body, filename)
diags = diags.Append(blockDiags)
if result != nil {
ret = append(ret, result)
}
}
return ret, diags
}
func decodeOCIDefaultCredentialsBlockBody(body *hclast.ObjectType, filename string) (*OCIDefaultCredentials, tfdiags.Diagnostics) {
const errInvalidSummary = "Invalid oci_default_credentials block"
var diags tfdiags.Diagnostics
// Any relative file paths in this block are resolved relative to the directory
// containing the file where this block came from.
baseDir := filepath.Dir(filename)
// Although decodeOCIDefaultCredentialsFromConfig did some lower-level decoding
// to try to force HCL 2-compatible syntax, the _content_ of this block is all
// just relatively-simple arguments and so we can use HCL 1's decoder here.
type BodyContent struct {
DiscoverAmbientCredentials *bool `hcl:"discover_ambient_credentials"`
DockerStyleConfigFiles *[]string `hcl:"docker_style_config_files"`
DefaultDockerCredentialsHelper *string `hcl:"docker_credentials_helper"`
}
var bodyContent BodyContent
err := hcl.DecodeObject(&bodyContent, body)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("Invalid oci_default_credentials block at %s: %s.", body.Pos(), err),
))
return nil, diags
}
// We'll start with the default values and then override based on what was
// specified in the configuration block.
ret := newDefaultOCIDefaultCredentials()
if bodyContent.DiscoverAmbientCredentials != nil {
ret.DiscoverAmbientCredentials = *bodyContent.DiscoverAmbientCredentials
}
if bodyContent.DockerStyleConfigFiles != nil {
// NOTE: non-nil but zero length represents explicitly nothing, rather that the default locations
ret.DockerStyleConfigFiles = make([]string, len(*bodyContent.DockerStyleConfigFiles))
for i, configPath := range *bodyContent.DockerStyleConfigFiles {
if !filepath.IsAbs(configPath) {
configPath = filepath.Join(baseDir, configPath)
}
// We'll also make a best effort to "absolute-ize" the path
// so that it won't get reinterpreted differently if the process
// switches to a different working directory after loading the
// CLI config (as happens with the -chdir global option). In
// the unlikely event that this fails we'll just accept it
// because we'll report any inaccessible files in a better way later.
if absPath, err := filepath.Abs(configPath); err == nil {
configPath = absPath
}
ret.DockerStyleConfigFiles[i] = configPath
}
}
if bodyContent.DefaultDockerCredentialsHelper != nil {
ret.DefaultDockerCredentialHelper = *bodyContent.DefaultDockerCredentialsHelper
if !validDockerCredentialHelperName(ret.DefaultDockerCredentialHelper) {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_default_credentials block at %s specifies the invalid Docker credential helper name %q. Must be a non-empty string that could be used as part of an executable filename.",
body.Pos(), ret.DefaultDockerCredentialHelper,
),
))
}
}
if !ret.DiscoverAmbientCredentials && ret.DockerStyleConfigFiles != nil {
// docker_style_config_files is a modifier for the discover_ambient_credentials
// behavior, so can't be used if discovery is totally disabled.
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_default_credentials block at %s disables discovery of ambient credentials, but also sets docker_style_config_files which is relevant only when ambient credentials discovery is enabled.",
body.Pos(),
),
))
}
return ret, diags
}
// OCIRepositoryCredentials corresponds directly to a single oci_credentials block
// in the CLI configuration, decoded in isolation. It represents the credentials
// configuration for a set of OCI repositories with a specific registry domain and
// optional repository path prefix.
//
// This represents just one part of the overall OCI credentials policy, and so needs
// to be considered in conjunction with all of the other OCICredentials objects across
// the CLI configuration, and the OCIDefaultCredentials object too.
type OCIRepositoryCredentials struct {
// A repository address prefix, in the form "domain/path", that describes which
// repositories these credentials are to be used for.
//
// This string is treated in the same way as the properties of the "auths" object
// in a container engine configuration file.
RepositoryPrefix string
// Username and Password are credentials to use for a "Basic"-style
// authentication method. These are mutually-exclusive with AccessToken
// and RefreshToken.
Username, Password string
// AccessToken and RefreshToken are credentials for an OAuth-style
// authentication method. These are mutually-exclusive with Username
// and Password.
AccessToken, RefreshToken string
// DockerCredentialsHelper is the name of a Docker-style credential helper program
// to use.
//
// Docker-style config only allows credential helpers to be configured at
// whole-registry-domain granularity, so for consistency we only allow this to be
// set when RepositoryPathPrefix isn't set.
DockerCredentialHelper string
}
// decodeOCIRepositoryCredentialsFromConfig uses the HCL AST API directly
// to decode "oci_credentials" blocks from the given file.
//
// The overall CLI configuration can contain zero or more blocks of this
// type. We require that each one describes a distinct OCI repository
// address prefix, but that constraint must be enforced by the caller of
// this function because it must be checked across all of the CLI
// configuration files together, rather than just one file at a time.
//
// This uses the HCL AST directly, rather than HCL's decoder, to continue
// our precedent of trying to constrain new features only to what could be
// supported compatibly in a hypothetical future HCL 2-based implementation
// of the CLI configuration language.
//
// Note that this function wants the top-level file object which might or
// might not contain oci_credentials blocks, not an oci_credentials block
// directly itself.
func decodeOCIRepositoryCredentialsFromConfig(hclFile *hclast.File) ([]*OCIRepositoryCredentials, tfdiags.Diagnostics) {
var ret []*OCIRepositoryCredentials
var diags tfdiags.Diagnostics
root, ok := hclFile.Node.(*hclast.ObjectList)
if !ok {
// A HCL file that doesn't have an object list at its root is weird, but
// dealing with that is outside the scope of this function.
// (In practice both the native syntax and JSON parsers for HCL force
// the root to be an ObjectList, so we should not get here for any real file.)
return ret, diags
}
for _, block := range root.Items {
const errInvalidSummary = "Invalid oci_credentials block"
if block.Keys[0].Token.Value() != "oci_credentials" {
continue
}
// This helper function compensates for HCL 1's inability to automatically
// resolve the block label vs. block argument ambiguity in its JSON syntax.
// (This is why HCL 2 requires explicit schema!)
const TWO = 2 // To quiet the "mnd" linter
unwrapHCLObjectKeysFromJSON(block, TWO)
if len(block.Keys) != TWO {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("The oci_credentials block at %s must have one label, giving an OCI repository address prefix.", block.Pos()),
))
continue
}
// HCL only tracks whether the input was JSON or native syntax inside
// individual tokens, so we'll use our block type token to decide
// and assume that the rest of the block must be written in the same
// syntax, because syntax is a whole-file idea.
isJSON := block.Keys[0].Token.JSON
if block.Assign.Line != 0 && !isJSON {
// Seems to be an attribute rather than a block
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("The oci_credentials block at %s must not be introduced with an equals sign.", block.Pos()),
))
continue
}
body, ok := block.Val.(*hclast.ObjectType)
if !ok {
// We can't get in here with native HCL syntax because we
// already checked above that we're using block syntax, but
// if we're reading JSON then our value could potentially be
// anything.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("The oci_credentials block at %s must be represented by a JSON object.", block.Pos()),
))
continue
}
label, ok := block.Keys[1].Token.Value().(string)
if !ok {
// HCL grammar doesn't allow anything other than string in the key position,
// so we should not get here.
panic(fmt.Sprintf("HCL returned non-string label %#v for oci_credentials block", block.Keys[1].Token))
}
result, blockDiags := decodeOCICredentialsBlockBody(label, body)
diags = diags.Append(blockDiags)
if result != nil {
ret = append(ret, result)
}
}
return ret, diags
}
func decodeOCICredentialsBlockBody(label string, body *hclast.ObjectType) (*OCIRepositoryCredentials, tfdiags.Diagnostics) {
const errInvalidSummary = "Invalid oci_credentials block"
var diags tfdiags.Diagnostics
// We only validate here, since the repository-matching function in ociauthconfig
// wants the unparsed string and performs its own parsing step for consistency
// with the handling of container-engine-style config files.
_, repositoryPath, labelErr := ociauthconfig.ParseRepositoryAddressPrefix(label)
if labelErr != nil {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_credentials block at %s has an invalid block label: %s.",
body.Pos(), labelErr,
),
))
return nil, diags
}
repositoryPrefix := label
// Although decodeOCICredentialsFromConfig did some lower-level decoding
// to try to force HCL 2-compatible syntax, the _content_ of this block is all
// just relatively-simple arguments and so we can use HCL 1's decoder here.
type BodyContent struct {
// The following three groups of arguments are mutually-exclusive.
// Basic-auth-style credentials, statically configured
Username *string `hcl:"username"`
Password *string `hcl:"password"`
// OAuth style credentials
AccessToken *string `hcl:"access_token"`
RefreshToken *string `hcl:"refresh_token"`
// Docker-style credentials helper providing Basic-auth-style credentials
// indirectly through an external program
DockerCredentialsHelper *string `hcl:"docker_credentials_helper"`
}
var bodyContent BodyContent
err := hcl.DecodeObject(&bodyContent, body)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf("Invalid oci_credentials block at %s: %s.", body.Pos(), err),
))
return nil, diags
}
staticBasicAuth := bodyContent.Username != nil || bodyContent.Password != nil
oauth := bodyContent.AccessToken != nil || bodyContent.RefreshToken != nil
dockerCredHelper := bodyContent.DockerCredentialsHelper != nil
stylesConfigured := trueCount(staticBasicAuth, oauth, dockerCredHelper)
if stylesConfigured == 0 {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_credentials block at %s must set either username+password, access_token+refresh_token, or docker_credentials_helper.",
body.Pos(),
),
))
return nil, diags
}
if stylesConfigured > 1 {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_credentials block at %s must set only one group out of username+password, access_token+refresh_token, or docker_credentials_helper.",
body.Pos(),
),
))
return nil, diags
}
ret := &OCIRepositoryCredentials{
RepositoryPrefix: repositoryPrefix,
}
switch {
case staticBasicAuth:
if bodyContent.Username == nil || bodyContent.Password == nil {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_credentials block at %s must set both username and password together when using static credentials.",
body.Pos(),
),
))
return nil, diags
}
ret.Username = *bodyContent.Username
ret.Password = *bodyContent.Password
case oauth:
// FIXME: Is refresh_roken actually required? We could potentially allow setting
// only access_token and let the request just immediately fail if the token has expired.
if bodyContent.AccessToken == nil || bodyContent.RefreshToken == nil {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_credentials block at %s must set both access_token and refresh_token together when using OAuth-style credentials.",
body.Pos(),
),
))
return nil, diags
}
ret.AccessToken = *bodyContent.AccessToken
ret.RefreshToken = *bodyContent.RefreshToken
case dockerCredHelper:
ret.DockerCredentialHelper = *bodyContent.DockerCredentialsHelper
if repositoryPath != "" {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_credentials block at %s cannot set docker_credentials_helper with a repository path: credential helpers only support credentials for whole domains.",
body.Pos(),
),
))
}
if !validDockerCredentialHelperName(ret.DockerCredentialHelper) {
diags = append(diags, tfdiags.Sourceless(
tfdiags.Error,
errInvalidSummary,
fmt.Sprintf(
"The oci_credentials block at %s specifies the invalid Docker credential helper name %q. Must be a non-empty string that could be used as part of an executable filename.",
body.Pos(), ret.DockerCredentialHelper,
),
))
}
if diags.HasErrors() {
return nil, diags
}
}
return ret, diags
}
// ociRepositoryCredentialsConfig is an unexported wrapper around an
// [OCIRepositoryCredentials] object that implements [ociauthconfig.CredentialsConfig]
// for use in [Config.OCICredentialsPolicy].
//
// For explicitly-configured blocks we treat each individual block as a separate
// CredentialsConfig, because that makes the config location more explicit. (Other
// implementations potentially use this interface to represent a whole file with
// potentially-many credentials in it.)
type ociRepositoryCredentialsConfig struct {
block *OCIRepositoryCredentials
}
var _ ociauthconfig.CredentialsConfig = ociRepositoryCredentialsConfig{}
// CredentialsConfigLocationForUI implements ociauthconfig.CredentialsConfig.
func (o ociRepositoryCredentialsConfig) CredentialsConfigLocationForUI() string {
// HCL 1 doesn't retain particularly useful source location information, and so
// we'll refer to the block by its name and label rather than by its physical
// location on disk.
return fmt.Sprintf("explicit oci_credentials %q block", o.block.RepositoryPrefix)
}
// CredentialsSourcesForRepository implements ociauthconfig.CredentialsConfig.
func (o ociRepositoryCredentialsConfig) CredentialsSourcesForRepository(_ context.Context, registryDomain string, repositoryPath string) iter.Seq2[ociauthconfig.CredentialsSource, error] {
spec := ociauthconfig.ContainersAuthPropertyNameMatch(o.block.RepositoryPrefix, registryDomain, repositoryPath)
if spec == ociauthconfig.NoCredentialsSpecificity {
// Does not match, so we'll return an empty sequence.
// TODO: Hopefully in future we can replace this with a stdlib helper function https://github.com/golang/go/issues/68947
return func(_ func(ociauthconfig.CredentialsSource, error) bool) {}
}
// If we get here then we have a match, so we'll return exactly one credentials source,
// which is really just this same object again implementing a different interface.
// TODO: Hopefully in future we can implement these 1-element sequences with a stdlib helper function https://github.com/golang/go/issues/68947
return func(yield func(ociauthconfig.CredentialsSource, error) bool) {
switch {
case o.block.Username != "":
// Validation checks that Username is always used with Password, so
// we can safely assume that here.
yield(ociauthconfig.NewStaticCredentialsSource(
ociauthconfig.NewBasicAuthCredentials(o.block.Username, o.block.Password),
spec,
), nil)
case o.block.AccessToken != "":
// Validation checks that AccessToken is always used with RefreshToken, so
// we can safely assume that here.
yield(ociauthconfig.NewStaticCredentialsSource(
ociauthconfig.NewOAuthCredentials(o.block.AccessToken, o.block.RefreshToken),
spec,
), nil)
case o.block.DockerCredentialHelper != "":
yield(ociauthconfig.NewDockerCredentialHelperCredentialsSource(
o.block.DockerCredentialHelper, "https://"+registryDomain, spec,
), nil)
default:
// There are no other credentials styles, so we should not get here.
yield(nil, fmt.Errorf("%s has no supported credentials arguments", o.CredentialsConfigLocationForUI()))
}
}
}
// ociCredentialsEnv implements ociauthconfig.ConfigDiscoveryEnvironment against the
// real execution environment provided by the host operating system.
type ociCredentialsEnv struct{}
var _ ociauthconfig.ConfigDiscoveryEnvironment = ociCredentialsEnv{}
// EnvironmentVariableVal implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e ociCredentialsEnv) EnvironmentVariableVal(name string) string {
return os.Getenv(name)
}
// OperatingSystemName implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e ociCredentialsEnv) OperatingSystemName() string {
return runtime.GOOS
}
// ReadFile implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e ociCredentialsEnv) ReadFile(_ context.Context, path string) ([]byte, error) {
log.Printf("[TRACE] OCI credentials discovery reading %s", path)
return os.ReadFile(path)
}
// UserHomeDirPath implements ociauthconfig.ConfigDiscoveryEnvironment.
func (e ociCredentialsEnv) UserHomeDirPath() string {
// This is intentionally slightly different from the homeDir function
// in this package, because it's designed to better match the behavior
// of Podman/etc.
switch runtime.GOOS {
case "windows":
if envHome := os.Getenv("USERPROFILE"); envHome != "" {
return envHome
}
homeDir, err := os.UserHomeDir()
if err == nil {
return homeDir
}
// If we can't find information about the current user then we'll
// just use the nul device as a placeholder, since we're using this
// result only as a basedir to probe for configuration files anyway.
return "nul:"
default:
if envHome := os.Getenv("HOME"); envHome != "" {
return envHome
}
u, err := user.Current()
if err == nil {
return u.HomeDir
}
// If we can't find information about the current user then we'll
// just use the root directory as a placeholder, since we're using
// this result only as a basedir to probe for configuration files anyway.
return "/"
}
}
func validDockerCredentialHelperName(n string) bool {
switch {
case n == "":
// It definitely can't be an empty string.
return false
case strings.Contains(filepath.ToSlash(n), `/`):
// The exact details of what's valid here seem OS-specific and so we'll defer
// the most detailed validation until we know we're actually going to try to
// run the credentials helper, but at this point we do at least know that
// the given name is going to be used as part of the filename of an executable
// and so it definitely can't contain path separators accepted by the current
// platform.
return false
default:
return true
}
}
func trueCount(flags ...bool) int {
ret := 0
for _, flag := range flags {
if flag {
ret++
}
}
return ret
}
// unwrapHCLObjectKeysFromJSON cleans up an edge case that can occur when
// parsing JSON as input using the old HCL 1 parser: if we're parsing JSON
// then directly nested items will show up as additional "keys".
//
// For objects that expect a fixed number of keys, this breaks the
// decoding process. This function unwraps the object into what it would've
// looked like if it came directly from HCL 1 by specifying the number of keys
// you expect.
//
// Example:
//
// { "foo": { "baz": {} } }
//
// Will show up with Keys being: []string{"foo", "baz"}
// when we really just want the first one. This function will fix this.
//
// This function is a fun old helper cribbed from a much older version before
// HCL 2, where the main language was also implemented using HCL 1:
// https://github.com/opentofu/opentofu/blob/e0fd3ddd704b230897723a7ca251f36b71c2b67a/config/loader_hcl.go#L1215-L1237
func unwrapHCLObjectKeysFromJSON(item *hclast.ObjectItem, depth int) {
if len(item.Keys) > depth && item.Keys[0].Token.JSON {
for len(item.Keys) > depth {
// Pop off the last key
n := len(item.Keys)
key := item.Keys[n-1]
item.Keys[n-1] = nil
item.Keys = item.Keys[:n-1]
// Wrap our value in a list
item.Val = &hclast.ObjectType{
List: &hclast.ObjectList{
Items: []*hclast.ObjectItem{
{
Keys: []*hclast.ObjectKey{key},
Val: item.Val,
},
},
},
}
}
}
}