ca57012072
Co-authored-by: Anton Averchenkov <84287187+averche@users.noreply.github.com>
1314 lines
35 KiB
Go
1314 lines
35 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
ctconfig "github.com/hashicorp/consul-template/config"
|
|
ctsignals "github.com/hashicorp/consul-template/signals"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
"github.com/mitchellh/mapstructure"
|
|
"k8s.io/utils/strings/slices"
|
|
|
|
"github.com/hashicorp/vault/command/agentproxyshared"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/internalshared/configutil"
|
|
"github.com/hashicorp/vault/sdk/helper/pointerutil"
|
|
)
|
|
|
|
// Config is the configuration for Vault Agent.
|
|
type Config struct {
|
|
*configutil.SharedConfig `hcl:"-"`
|
|
|
|
AutoAuth *AutoAuth `hcl:"auto_auth"`
|
|
ExitAfterAuth bool `hcl:"exit_after_auth"`
|
|
Cache *Cache `hcl:"cache"`
|
|
APIProxy *APIProxy `hcl:"api_proxy"`
|
|
Vault *Vault `hcl:"vault"`
|
|
TemplateConfig *TemplateConfig `hcl:"template_config"`
|
|
Templates []*ctconfig.TemplateConfig `hcl:"templates"`
|
|
DisableIdleConns []string `hcl:"disable_idle_connections"`
|
|
DisableIdleConnsAPIProxy bool `hcl:"-"`
|
|
DisableIdleConnsTemplating bool `hcl:"-"`
|
|
DisableIdleConnsAutoAuth bool `hcl:"-"`
|
|
DisableKeepAlives []string `hcl:"disable_keep_alives"`
|
|
DisableKeepAlivesAPIProxy bool `hcl:"-"`
|
|
DisableKeepAlivesTemplating bool `hcl:"-"`
|
|
DisableKeepAlivesAutoAuth bool `hcl:"-"`
|
|
Exec *ExecConfig `hcl:"exec,optional"`
|
|
EnvTemplates []*ctconfig.TemplateConfig `hcl:"env_template,optional"`
|
|
}
|
|
|
|
const (
|
|
DisableIdleConnsEnv = "VAULT_AGENT_DISABLE_IDLE_CONNECTIONS"
|
|
DisableKeepAlivesEnv = "VAULT_AGENT_DISABLE_KEEP_ALIVES"
|
|
)
|
|
|
|
func (c *Config) Prune() {
|
|
for _, l := range c.Listeners {
|
|
l.RawConfig = nil
|
|
l.Profiling.UnusedKeys = nil
|
|
l.Telemetry.UnusedKeys = nil
|
|
l.CustomResponseHeaders = nil
|
|
}
|
|
c.FoundKeys = nil
|
|
c.UnusedKeys = nil
|
|
c.SharedConfig.FoundKeys = nil
|
|
c.SharedConfig.UnusedKeys = nil
|
|
if c.Telemetry != nil {
|
|
c.Telemetry.FoundKeys = nil
|
|
c.Telemetry.UnusedKeys = nil
|
|
}
|
|
}
|
|
|
|
type Retry struct {
|
|
NumRetries int `hcl:"num_retries"`
|
|
}
|
|
|
|
// Vault contains configuration for connecting to Vault servers
|
|
type Vault struct {
|
|
Address string `hcl:"address"`
|
|
CACert string `hcl:"ca_cert"`
|
|
CAPath string `hcl:"ca_path"`
|
|
TLSSkipVerify bool `hcl:"-"`
|
|
TLSSkipVerifyRaw interface{} `hcl:"tls_skip_verify"`
|
|
ClientCert string `hcl:"client_cert"`
|
|
ClientKey string `hcl:"client_key"`
|
|
TLSServerName string `hcl:"tls_server_name"`
|
|
Retry *Retry `hcl:"retry"`
|
|
}
|
|
|
|
// transportDialer is an interface that allows passing a custom dialer function
|
|
// to an HTTP client's transport config
|
|
type transportDialer interface {
|
|
// Dial is intended to match https://pkg.go.dev/net#Dialer.Dial
|
|
Dial(network, address string) (net.Conn, error)
|
|
|
|
// DialContext is intended to match https://pkg.go.dev/net#Dialer.DialContext
|
|
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
}
|
|
|
|
// APIProxy contains any configuration needed for proxy mode
|
|
type APIProxy struct {
|
|
UseAutoAuthTokenRaw interface{} `hcl:"use_auto_auth_token"`
|
|
UseAutoAuthToken bool `hcl:"-"`
|
|
ForceAutoAuthToken bool `hcl:"-"`
|
|
EnforceConsistency string `hcl:"enforce_consistency"`
|
|
WhenInconsistent string `hcl:"when_inconsistent"`
|
|
}
|
|
|
|
// Cache contains any configuration needed for Cache mode
|
|
type Cache struct {
|
|
UseAutoAuthTokenRaw interface{} `hcl:"use_auto_auth_token"`
|
|
UseAutoAuthToken bool `hcl:"-"`
|
|
ForceAutoAuthToken bool `hcl:"-"`
|
|
EnforceConsistency string `hcl:"enforce_consistency"`
|
|
WhenInconsistent string `hcl:"when_inconsistent"`
|
|
Persist *agentproxyshared.PersistConfig `hcl:"persist"`
|
|
InProcDialer transportDialer `hcl:"-"`
|
|
}
|
|
|
|
// AutoAuth is the configured authentication method and sinks
|
|
type AutoAuth struct {
|
|
Method *Method `hcl:"-"`
|
|
Sinks []*Sink `hcl:"sinks"`
|
|
|
|
// NOTE: This is unsupported outside of testing and may disappear at any
|
|
// time.
|
|
EnableReauthOnNewCredentials bool `hcl:"enable_reauth_on_new_credentials"`
|
|
}
|
|
|
|
// Method represents the configuration for the authentication backend
|
|
type Method struct {
|
|
Type string
|
|
MountPath string `hcl:"mount_path"`
|
|
WrapTTLRaw interface{} `hcl:"wrap_ttl"`
|
|
WrapTTL time.Duration `hcl:"-"`
|
|
MinBackoffRaw interface{} `hcl:"min_backoff"`
|
|
MinBackoff time.Duration `hcl:"-"`
|
|
MaxBackoffRaw interface{} `hcl:"max_backoff"`
|
|
MaxBackoff time.Duration `hcl:"-"`
|
|
Namespace string `hcl:"namespace"`
|
|
ExitOnError bool `hcl:"exit_on_err"`
|
|
Config map[string]interface{}
|
|
}
|
|
|
|
// Sink defines a location to write the authenticated token
|
|
type Sink struct {
|
|
Type string
|
|
WrapTTLRaw interface{} `hcl:"wrap_ttl"`
|
|
WrapTTL time.Duration `hcl:"-"`
|
|
DHType string `hcl:"dh_type"`
|
|
DeriveKey bool `hcl:"derive_key"`
|
|
DHPath string `hcl:"dh_path"`
|
|
AAD string `hcl:"aad"`
|
|
AADEnvVar string `hcl:"aad_env_var"`
|
|
Config map[string]interface{}
|
|
}
|
|
|
|
// TemplateConfig defines global behaviors around template
|
|
type TemplateConfig struct {
|
|
ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"`
|
|
StaticSecretRenderIntRaw interface{} `hcl:"static_secret_render_interval"`
|
|
StaticSecretRenderInt time.Duration `hcl:"-"`
|
|
}
|
|
|
|
type ExecConfig struct {
|
|
Command []string `hcl:"command,attr" mapstructure:"command"`
|
|
RestartOnSecretChanges string `hcl:"restart_on_secret_changes,optional" mapstructure:"restart_on_secret_changes"`
|
|
RestartStopSignal os.Signal `hcl:"-" mapstructure:"restart_stop_signal"`
|
|
}
|
|
|
|
func NewConfig() *Config {
|
|
return &Config{
|
|
SharedConfig: new(configutil.SharedConfig),
|
|
}
|
|
}
|
|
|
|
// Merge merges two Agent configurations.
|
|
func (c *Config) Merge(c2 *Config) *Config {
|
|
if c2 == nil {
|
|
return c
|
|
}
|
|
|
|
result := NewConfig()
|
|
|
|
result.SharedConfig = c.SharedConfig
|
|
if c2.SharedConfig != nil {
|
|
result.SharedConfig = c.SharedConfig.Merge(c2.SharedConfig)
|
|
}
|
|
|
|
result.AutoAuth = c.AutoAuth
|
|
if c2.AutoAuth != nil {
|
|
result.AutoAuth = c2.AutoAuth
|
|
}
|
|
|
|
result.Cache = c.Cache
|
|
if c2.Cache != nil {
|
|
result.Cache = c2.Cache
|
|
}
|
|
|
|
result.APIProxy = c.APIProxy
|
|
if c2.APIProxy != nil {
|
|
result.APIProxy = c2.APIProxy
|
|
}
|
|
|
|
result.DisableMlock = c.DisableMlock
|
|
if c2.DisableMlock {
|
|
result.DisableMlock = c2.DisableMlock
|
|
}
|
|
|
|
// For these, ignore the non-specific one and overwrite them all
|
|
result.DisableIdleConnsAutoAuth = c.DisableIdleConnsAutoAuth
|
|
if c2.DisableIdleConnsAutoAuth {
|
|
result.DisableIdleConnsAutoAuth = c2.DisableIdleConnsAutoAuth
|
|
}
|
|
|
|
result.DisableIdleConnsAPIProxy = c.DisableIdleConnsAPIProxy
|
|
if c2.DisableIdleConnsAPIProxy {
|
|
result.DisableIdleConnsAPIProxy = c2.DisableIdleConnsAPIProxy
|
|
}
|
|
|
|
result.DisableIdleConnsTemplating = c.DisableIdleConnsTemplating
|
|
if c2.DisableIdleConnsTemplating {
|
|
result.DisableIdleConnsTemplating = c2.DisableIdleConnsTemplating
|
|
}
|
|
|
|
result.DisableKeepAlivesAutoAuth = c.DisableKeepAlivesAutoAuth
|
|
if c2.DisableKeepAlivesAutoAuth {
|
|
result.DisableKeepAlivesAutoAuth = c2.DisableKeepAlivesAutoAuth
|
|
}
|
|
|
|
result.DisableKeepAlivesAPIProxy = c.DisableKeepAlivesAPIProxy
|
|
if c2.DisableKeepAlivesAPIProxy {
|
|
result.DisableKeepAlivesAPIProxy = c2.DisableKeepAlivesAPIProxy
|
|
}
|
|
|
|
result.DisableKeepAlivesTemplating = c.DisableKeepAlivesTemplating
|
|
if c2.DisableKeepAlivesTemplating {
|
|
result.DisableKeepAlivesTemplating = c2.DisableKeepAlivesTemplating
|
|
}
|
|
|
|
result.TemplateConfig = c.TemplateConfig
|
|
if c2.TemplateConfig != nil {
|
|
result.TemplateConfig = c2.TemplateConfig
|
|
}
|
|
|
|
for _, l := range c.Templates {
|
|
result.Templates = append(result.Templates, l)
|
|
}
|
|
for _, l := range c2.Templates {
|
|
result.Templates = append(result.Templates, l)
|
|
}
|
|
|
|
result.ExitAfterAuth = c.ExitAfterAuth
|
|
if c2.ExitAfterAuth {
|
|
result.ExitAfterAuth = c2.ExitAfterAuth
|
|
}
|
|
|
|
result.Vault = c.Vault
|
|
if c2.Vault != nil {
|
|
result.Vault = c2.Vault
|
|
}
|
|
|
|
result.PidFile = c.PidFile
|
|
if c2.PidFile != "" {
|
|
result.PidFile = c2.PidFile
|
|
}
|
|
|
|
result.Exec = c.Exec
|
|
if c2.Exec != nil {
|
|
result.Exec = c2.Exec
|
|
}
|
|
|
|
for _, envTmpl := range c.EnvTemplates {
|
|
result.EnvTemplates = append(result.EnvTemplates, envTmpl)
|
|
}
|
|
|
|
for _, envTmpl := range c2.EnvTemplates {
|
|
result.EnvTemplates = append(result.EnvTemplates, envTmpl)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// IsDefaultListerDefined returns true if a default listener has been defined
|
|
// in this config
|
|
func (c *Config) IsDefaultListerDefined() bool {
|
|
for _, l := range c.Listeners {
|
|
if l.Role != "metrics_only" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ValidateConfig validates an Agent configuration after it has been fully merged together, to
|
|
// ensure that required combinations of configs are there
|
|
func (c *Config) ValidateConfig() error {
|
|
if c.APIProxy != nil && c.Cache != nil {
|
|
if c.Cache.UseAutoAuthTokenRaw != nil {
|
|
if c.APIProxy.UseAutoAuthTokenRaw != nil {
|
|
return fmt.Errorf("use_auto_auth_token defined in both api_proxy and cache config. Please remove this configuration from the cache block")
|
|
} else {
|
|
c.APIProxy.ForceAutoAuthToken = c.Cache.ForceAutoAuthToken
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.Cache != nil {
|
|
if len(c.Listeners) < 1 && len(c.Templates) < 1 {
|
|
return fmt.Errorf("enabling the cache requires at least 1 template or 1 listener to be defined")
|
|
}
|
|
|
|
if c.Cache.UseAutoAuthToken {
|
|
if c.AutoAuth == nil {
|
|
return fmt.Errorf("cache.use_auto_auth_token is true but auto_auth not configured")
|
|
}
|
|
if c.AutoAuth != nil && c.AutoAuth.Method != nil && c.AutoAuth.Method.WrapTTL > 0 {
|
|
return fmt.Errorf("cache.use_auto_auth_token is true and auto_auth uses wrapping")
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.APIProxy != nil {
|
|
if len(c.Listeners) < 1 {
|
|
return fmt.Errorf("configuring the api_proxy requires at least 1 listener to be defined")
|
|
}
|
|
|
|
if c.APIProxy.UseAutoAuthToken {
|
|
if c.AutoAuth == nil {
|
|
return fmt.Errorf("api_proxy.use_auto_auth_token is true but auto_auth not configured")
|
|
}
|
|
if c.AutoAuth != nil && c.AutoAuth.Method != nil && c.AutoAuth.Method.WrapTTL > 0 {
|
|
return fmt.Errorf("api_proxy.use_auto_auth_token is true and auto_auth uses wrapping")
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.AutoAuth != nil {
|
|
if len(c.AutoAuth.Sinks) == 0 &&
|
|
(c.APIProxy == nil || !c.APIProxy.UseAutoAuthToken) &&
|
|
len(c.Templates) == 0 &&
|
|
len(c.EnvTemplates) == 0 {
|
|
return fmt.Errorf("auto_auth requires at least one sink or at least one template or api_proxy.use_auto_auth_token=true")
|
|
}
|
|
}
|
|
|
|
if c.AutoAuth == nil && c.Cache == nil && len(c.Listeners) == 0 {
|
|
return fmt.Errorf("no auto_auth, cache, or listener block found in config")
|
|
}
|
|
|
|
return c.validateEnvTemplateConfig()
|
|
}
|
|
|
|
func (c *Config) validateEnvTemplateConfig() error {
|
|
// if we are not in env-template mode, exit early
|
|
if c.Exec == nil && len(c.EnvTemplates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if c.Exec == nil {
|
|
return fmt.Errorf("a top-level 'exec' element must be specified with 'env_template' entries")
|
|
}
|
|
|
|
if len(c.EnvTemplates) == 0 {
|
|
return fmt.Errorf("must specify at least one 'env_template' element with a top-level 'exec' element")
|
|
}
|
|
|
|
if c.APIProxy != nil {
|
|
return fmt.Errorf("'api_proxy' cannot be specified with 'env_template' entries")
|
|
}
|
|
|
|
if len(c.Templates) > 0 {
|
|
return fmt.Errorf("'template' cannot be specified with 'env_template' entries")
|
|
}
|
|
|
|
if len(c.Exec.Command) == 0 {
|
|
return fmt.Errorf("'exec' requires a non-empty 'command' field")
|
|
}
|
|
|
|
if !slices.Contains([]string{"always", "never"}, c.Exec.RestartOnSecretChanges) {
|
|
return fmt.Errorf("'exec.restart_on_secret_changes' unexpected value: %q", c.Exec.RestartOnSecretChanges)
|
|
}
|
|
|
|
uniqueKeys := make(map[string]struct{})
|
|
|
|
for _, template := range c.EnvTemplates {
|
|
// Required:
|
|
// - the key (environment variable name)
|
|
// - either "contents" or "source"
|
|
// Optional / permitted:
|
|
// - error_on_missing_key
|
|
// - error_fatal
|
|
// - left_delimiter
|
|
// - right_delimiter
|
|
// - ExtFuncMap
|
|
// - function_denylist / function_blacklist
|
|
|
|
if template.MapToEnvironmentVariable == nil {
|
|
return fmt.Errorf("env_template: an environment variable name is required")
|
|
}
|
|
|
|
key := *template.MapToEnvironmentVariable
|
|
|
|
if _, exists := uniqueKeys[key]; exists {
|
|
return fmt.Errorf("env_template: duplicate environment variable name: %q", key)
|
|
}
|
|
|
|
uniqueKeys[key] = struct{}{}
|
|
|
|
if template.Contents == nil && template.Source == nil {
|
|
return fmt.Errorf("env_template[%s]: either 'contents' or 'source' must be specified", key)
|
|
}
|
|
|
|
if template.Contents != nil && template.Source != nil {
|
|
return fmt.Errorf("env_template[%s]: 'contents' and 'source' cannot be specified together", key)
|
|
}
|
|
|
|
if template.Backup != nil {
|
|
return fmt.Errorf("env_template[%s]: 'backup' is not allowed", key)
|
|
}
|
|
|
|
if template.Command != nil {
|
|
return fmt.Errorf("env_template[%s]: 'command' is not allowed", key)
|
|
}
|
|
|
|
if template.CommandTimeout != nil {
|
|
return fmt.Errorf("env_template[%s]: 'command_timeout' is not allowed", key)
|
|
}
|
|
|
|
if template.CreateDestDirs != nil {
|
|
return fmt.Errorf("env_template[%s]: 'create_dest_dirs' is not allowed", key)
|
|
}
|
|
|
|
if template.Destination != nil {
|
|
return fmt.Errorf("env_template[%s]: 'destination' is not allowed", key)
|
|
}
|
|
|
|
if template.Exec != nil {
|
|
return fmt.Errorf("env_template[%s]: 'exec' is not allowed", key)
|
|
}
|
|
|
|
if template.Perms != nil {
|
|
return fmt.Errorf("env_template[%s]: 'perms' is not allowed", key)
|
|
}
|
|
|
|
if template.User != nil {
|
|
return fmt.Errorf("env_template[%s]: 'user' is not allowed", key)
|
|
}
|
|
|
|
if template.Uid != nil {
|
|
return fmt.Errorf("env_template[%s]: 'uid' is not allowed", key)
|
|
}
|
|
|
|
if template.Group != nil {
|
|
return fmt.Errorf("env_template[%s]: 'group' is not allowed", key)
|
|
}
|
|
|
|
if template.Gid != nil {
|
|
return fmt.Errorf("env_template[%s]: 'gid' is not allowed", key)
|
|
}
|
|
|
|
if template.Wait != nil {
|
|
return fmt.Errorf("env_template[%s]: 'wait' is not allowed", key)
|
|
}
|
|
|
|
if template.SandboxPath != nil {
|
|
return fmt.Errorf("env_template[%s]: 'sandbox_path' is not allowed", key)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadConfig loads the configuration at the given path, regardless if
|
|
// it's a file or directory.
|
|
func LoadConfig(path string) (*Config, error) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
return LoadConfigDir(path)
|
|
}
|
|
return LoadConfigFile(path)
|
|
}
|
|
|
|
// LoadConfigDir loads the configuration at the given path if it's a directory
|
|
func LoadConfigDir(dir string) (*Config, error) {
|
|
f, err := os.Open(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !fi.IsDir() {
|
|
return nil, fmt.Errorf("configuration path must be a directory: %q", dir)
|
|
}
|
|
|
|
var files []string
|
|
err = nil
|
|
for err != io.EOF {
|
|
var fis []os.FileInfo
|
|
fis, err = f.Readdir(128)
|
|
if err != nil && err != io.EOF {
|
|
return nil, err
|
|
}
|
|
|
|
for _, fi := range fis {
|
|
// Ignore directories
|
|
if fi.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Only care about files that are valid to load.
|
|
name := fi.Name()
|
|
skip := true
|
|
if strings.HasSuffix(name, ".hcl") {
|
|
skip = false
|
|
} else if strings.HasSuffix(name, ".json") {
|
|
skip = false
|
|
}
|
|
if skip || isTemporaryFile(name) {
|
|
continue
|
|
}
|
|
|
|
path := filepath.Join(dir, name)
|
|
files = append(files, path)
|
|
}
|
|
}
|
|
|
|
result := NewConfig()
|
|
for _, f := range files {
|
|
config, err := LoadConfigFile(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error loading %q: %w", f, err)
|
|
}
|
|
|
|
if result == nil {
|
|
result = config
|
|
} else {
|
|
result = result.Merge(config)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// isTemporaryFile returns true or false depending on whether the
|
|
// provided file name is a temporary file for the following editors:
|
|
// emacs or vim.
|
|
func isTemporaryFile(name string) bool {
|
|
return strings.HasSuffix(name, "~") || // vim
|
|
strings.HasPrefix(name, ".#") || // emacs
|
|
(strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#")) // emacs
|
|
}
|
|
|
|
// LoadConfigFile loads the configuration at the given path if it's a file
|
|
func LoadConfigFile(path string) (*Config, error) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
return nil, fmt.Errorf("location is a directory, not a file")
|
|
}
|
|
|
|
// Read the file
|
|
d, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse!
|
|
obj, err := hcl.Parse(string(d))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Attribute
|
|
ast.Walk(obj, func(n ast.Node) (ast.Node, bool) {
|
|
if k, ok := n.(*ast.ObjectKey); ok {
|
|
k.Token.Pos.Filename = path
|
|
}
|
|
return n, true
|
|
})
|
|
|
|
// Start building the result
|
|
result := NewConfig()
|
|
if err := hcl.DecodeObject(result, obj); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sharedConfig, err := configutil.ParseConfig(string(d))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Pruning custom headers for Agent for now
|
|
for _, ln := range sharedConfig.Listeners {
|
|
ln.CustomResponseHeaders = nil
|
|
}
|
|
|
|
result.SharedConfig = sharedConfig
|
|
|
|
list, ok := obj.Node.(*ast.ObjectList)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
|
|
}
|
|
|
|
if err := parseAutoAuth(result, list); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'auto_auth': %w", err)
|
|
}
|
|
|
|
if err := parseCache(result, list); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'cache':%w", err)
|
|
}
|
|
|
|
if err := parseAPIProxy(result, list); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'api_proxy':%w", err)
|
|
}
|
|
|
|
if err := parseTemplateConfig(result, list); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'template_config': %w", err)
|
|
}
|
|
|
|
if err := parseTemplates(result, list); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'template': %w", err)
|
|
}
|
|
|
|
if err := parseExec(result, list); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'exec': %w", err)
|
|
}
|
|
|
|
if err := parseEnvTemplates(result, list); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'env_template': %w", err)
|
|
}
|
|
|
|
if result.Cache != nil && result.APIProxy == nil && (result.Cache.UseAutoAuthToken || result.Cache.ForceAutoAuthToken) {
|
|
result.APIProxy = &APIProxy{
|
|
UseAutoAuthToken: result.Cache.UseAutoAuthToken,
|
|
ForceAutoAuthToken: result.Cache.ForceAutoAuthToken,
|
|
}
|
|
}
|
|
|
|
err = parseVault(result, list)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing 'vault':%w", err)
|
|
}
|
|
|
|
if result.Vault != nil {
|
|
// Set defaults
|
|
if result.Vault.Retry == nil {
|
|
result.Vault.Retry = &Retry{}
|
|
}
|
|
switch result.Vault.Retry.NumRetries {
|
|
case 0:
|
|
result.Vault.Retry.NumRetries = ctconfig.DefaultRetryAttempts
|
|
case -1:
|
|
result.Vault.Retry.NumRetries = 0
|
|
}
|
|
}
|
|
|
|
if disableIdleConnsEnv := os.Getenv(DisableIdleConnsEnv); disableIdleConnsEnv != "" {
|
|
result.DisableIdleConns, err = parseutil.ParseCommaStringSlice(strings.ToLower(disableIdleConnsEnv))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing environment variable %s: %v", DisableIdleConnsEnv, err)
|
|
}
|
|
}
|
|
|
|
for _, subsystem := range result.DisableIdleConns {
|
|
switch subsystem {
|
|
case "auto-auth":
|
|
result.DisableIdleConnsAutoAuth = true
|
|
case "caching", "proxying":
|
|
result.DisableIdleConnsAPIProxy = true
|
|
case "templating":
|
|
result.DisableIdleConnsTemplating = true
|
|
case "":
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("unknown disable_idle_connections value: %s", subsystem)
|
|
}
|
|
}
|
|
|
|
if disableKeepAlivesEnv := os.Getenv(DisableKeepAlivesEnv); disableKeepAlivesEnv != "" {
|
|
result.DisableKeepAlives, err = parseutil.ParseCommaStringSlice(strings.ToLower(disableKeepAlivesEnv))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing environment variable %s: %v", DisableKeepAlivesEnv, err)
|
|
}
|
|
}
|
|
|
|
for _, subsystem := range result.DisableKeepAlives {
|
|
switch subsystem {
|
|
case "auto-auth":
|
|
result.DisableKeepAlivesAutoAuth = true
|
|
case "caching", "proxying":
|
|
result.DisableKeepAlivesAPIProxy = true
|
|
case "templating":
|
|
result.DisableKeepAlivesTemplating = true
|
|
case "":
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("unknown disable_keep_alives value: %s", subsystem)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func parseVault(result *Config, list *ast.ObjectList) error {
|
|
name := "vault"
|
|
|
|
vaultList := list.Filter(name)
|
|
if len(vaultList.Items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(vaultList.Items) > 1 {
|
|
return fmt.Errorf("one and only one %q block is required", name)
|
|
}
|
|
|
|
item := vaultList.Items[0]
|
|
|
|
var v Vault
|
|
err := hcl.DecodeObject(&v, item.Val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v.TLSSkipVerifyRaw != nil {
|
|
v.TLSSkipVerify, err = parseutil.ParseBool(v.TLSSkipVerifyRaw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
result.Vault = &v
|
|
|
|
subs, ok := item.Val.(*ast.ObjectType)
|
|
if !ok {
|
|
return fmt.Errorf("could not parse %q as an object", name)
|
|
}
|
|
|
|
if err := parseRetry(result, subs.List); err != nil {
|
|
return fmt.Errorf("error parsing 'retry': %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseRetry(result *Config, list *ast.ObjectList) error {
|
|
name := "retry"
|
|
|
|
retryList := list.Filter(name)
|
|
if len(retryList.Items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(retryList.Items) > 1 {
|
|
return fmt.Errorf("one and only one %q block is required", name)
|
|
}
|
|
|
|
item := retryList.Items[0]
|
|
|
|
var r Retry
|
|
err := hcl.DecodeObject(&r, item.Val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result.Vault.Retry = &r
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseAPIProxy(result *Config, list *ast.ObjectList) error {
|
|
name := "api_proxy"
|
|
|
|
apiProxyList := list.Filter(name)
|
|
if len(apiProxyList.Items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(apiProxyList.Items) > 1 {
|
|
return fmt.Errorf("one and only one %q block is required", name)
|
|
}
|
|
|
|
item := apiProxyList.Items[0]
|
|
|
|
var apiProxy APIProxy
|
|
err := hcl.DecodeObject(&apiProxy, item.Val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if apiProxy.UseAutoAuthTokenRaw != nil {
|
|
apiProxy.UseAutoAuthToken, err = parseutil.ParseBool(apiProxy.UseAutoAuthTokenRaw)
|
|
if err != nil {
|
|
// Could be a value of "force" instead of "true"/"false"
|
|
switch apiProxy.UseAutoAuthTokenRaw.(type) {
|
|
case string:
|
|
v := apiProxy.UseAutoAuthTokenRaw.(string)
|
|
|
|
if !strings.EqualFold(v, "force") {
|
|
return fmt.Errorf("value of 'use_auto_auth_token' can be either true/false/force, %q is an invalid option", apiProxy.UseAutoAuthTokenRaw)
|
|
}
|
|
apiProxy.UseAutoAuthToken = true
|
|
apiProxy.ForceAutoAuthToken = true
|
|
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
result.APIProxy = &apiProxy
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseCache(result *Config, list *ast.ObjectList) error {
|
|
name := "cache"
|
|
|
|
cacheList := list.Filter(name)
|
|
if len(cacheList.Items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(cacheList.Items) > 1 {
|
|
return fmt.Errorf("one and only one %q block is required", name)
|
|
}
|
|
|
|
item := cacheList.Items[0]
|
|
|
|
var c Cache
|
|
err := hcl.DecodeObject(&c, item.Val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.UseAutoAuthTokenRaw != nil {
|
|
c.UseAutoAuthToken, err = parseutil.ParseBool(c.UseAutoAuthTokenRaw)
|
|
if err != nil {
|
|
// Could be a value of "force" instead of "true"/"false"
|
|
switch c.UseAutoAuthTokenRaw.(type) {
|
|
case string:
|
|
v := c.UseAutoAuthTokenRaw.(string)
|
|
|
|
if !strings.EqualFold(v, "force") {
|
|
return fmt.Errorf("value of 'use_auto_auth_token' can be either true/false/force, %q is an invalid option", c.UseAutoAuthTokenRaw)
|
|
}
|
|
c.UseAutoAuthToken = true
|
|
c.ForceAutoAuthToken = true
|
|
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
result.Cache = &c
|
|
|
|
subs, ok := item.Val.(*ast.ObjectType)
|
|
if !ok {
|
|
return fmt.Errorf("could not parse %q as an object", name)
|
|
}
|
|
subList := subs.List
|
|
if err := parsePersist(result, subList); err != nil {
|
|
return fmt.Errorf("error parsing persist: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parsePersist(result *Config, list *ast.ObjectList) error {
|
|
name := "persist"
|
|
|
|
persistList := list.Filter(name)
|
|
if len(persistList.Items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(persistList.Items) > 1 {
|
|
return fmt.Errorf("only one %q block is required", name)
|
|
}
|
|
|
|
item := persistList.Items[0]
|
|
|
|
var p agentproxyshared.PersistConfig
|
|
err := hcl.DecodeObject(&p, item.Val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.Type == "" {
|
|
if len(item.Keys) == 1 {
|
|
p.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
|
|
}
|
|
if p.Type == "" {
|
|
return errors.New("persist type must be specified")
|
|
}
|
|
}
|
|
|
|
result.Cache.Persist = &p
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseAutoAuth(result *Config, list *ast.ObjectList) error {
|
|
name := "auto_auth"
|
|
|
|
autoAuthList := list.Filter(name)
|
|
if len(autoAuthList.Items) == 0 {
|
|
return nil
|
|
}
|
|
if len(autoAuthList.Items) > 1 {
|
|
return fmt.Errorf("at most one %q block is allowed", name)
|
|
}
|
|
|
|
// Get our item
|
|
item := autoAuthList.Items[0]
|
|
|
|
var a AutoAuth
|
|
if err := hcl.DecodeObject(&a, item.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
result.AutoAuth = &a
|
|
|
|
subs, ok := item.Val.(*ast.ObjectType)
|
|
if !ok {
|
|
return fmt.Errorf("could not parse %q as an object", name)
|
|
}
|
|
subList := subs.List
|
|
|
|
if err := parseMethod(result, subList); err != nil {
|
|
return fmt.Errorf("error parsing 'method': %w", err)
|
|
}
|
|
if a.Method == nil {
|
|
return fmt.Errorf("no 'method' block found")
|
|
}
|
|
|
|
if err := parseSinks(result, subList); err != nil {
|
|
return fmt.Errorf("error parsing 'sink' stanzas: %w", err)
|
|
}
|
|
|
|
if result.AutoAuth.Method.WrapTTL > 0 {
|
|
if len(result.AutoAuth.Sinks) != 1 {
|
|
return fmt.Errorf("error parsing auto_auth: wrapping enabled on auth method and 0 or many sinks defined")
|
|
}
|
|
|
|
if result.AutoAuth.Sinks[0].WrapTTL > 0 {
|
|
return fmt.Errorf("error parsing auto_auth: wrapping enabled both on auth method and sink")
|
|
}
|
|
}
|
|
|
|
if result.AutoAuth.Method.MaxBackoffRaw != nil {
|
|
var err error
|
|
if result.AutoAuth.Method.MaxBackoff, err = parseutil.ParseDurationSecond(result.AutoAuth.Method.MaxBackoffRaw); err != nil {
|
|
return err
|
|
}
|
|
result.AutoAuth.Method.MaxBackoffRaw = nil
|
|
}
|
|
|
|
if result.AutoAuth.Method.MinBackoffRaw != nil {
|
|
var err error
|
|
if result.AutoAuth.Method.MinBackoff, err = parseutil.ParseDurationSecond(result.AutoAuth.Method.MinBackoffRaw); err != nil {
|
|
return err
|
|
}
|
|
result.AutoAuth.Method.MinBackoffRaw = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseMethod(result *Config, list *ast.ObjectList) error {
|
|
name := "method"
|
|
|
|
methodList := list.Filter(name)
|
|
if len(methodList.Items) != 1 {
|
|
return fmt.Errorf("one and only one %q block is required", name)
|
|
}
|
|
|
|
// Get our item
|
|
item := methodList.Items[0]
|
|
|
|
var m Method
|
|
if err := hcl.DecodeObject(&m, item.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
if m.Type == "" {
|
|
if len(item.Keys) == 1 {
|
|
m.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
|
|
}
|
|
if m.Type == "" {
|
|
return errors.New("method type must be specified")
|
|
}
|
|
}
|
|
|
|
// Default to Vault's default
|
|
if m.MountPath == "" {
|
|
m.MountPath = fmt.Sprintf("auth/%s", m.Type)
|
|
}
|
|
// Standardize on no trailing slash
|
|
m.MountPath = strings.TrimSuffix(m.MountPath, "/")
|
|
|
|
if m.WrapTTLRaw != nil {
|
|
var err error
|
|
if m.WrapTTL, err = parseutil.ParseDurationSecond(m.WrapTTLRaw); err != nil {
|
|
return err
|
|
}
|
|
m.WrapTTLRaw = nil
|
|
}
|
|
|
|
// Canonicalize namespace path if provided
|
|
m.Namespace = namespace.Canonicalize(m.Namespace)
|
|
|
|
result.AutoAuth.Method = &m
|
|
return nil
|
|
}
|
|
|
|
func parseSinks(result *Config, list *ast.ObjectList) error {
|
|
name := "sink"
|
|
|
|
sinkList := list.Filter(name)
|
|
if len(sinkList.Items) < 1 {
|
|
return nil
|
|
}
|
|
|
|
var ts []*Sink
|
|
|
|
for _, item := range sinkList.Items {
|
|
var s Sink
|
|
if err := hcl.DecodeObject(&s, item.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.Type == "" {
|
|
if len(item.Keys) == 1 {
|
|
s.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
|
|
}
|
|
if s.Type == "" {
|
|
return errors.New("sink type must be specified")
|
|
}
|
|
}
|
|
|
|
if s.WrapTTLRaw != nil {
|
|
var err error
|
|
if s.WrapTTL, err = parseutil.ParseDurationSecond(s.WrapTTLRaw); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("sink.%s", s.Type))
|
|
}
|
|
s.WrapTTLRaw = nil
|
|
}
|
|
|
|
switch s.DHType {
|
|
case "":
|
|
case "curve25519":
|
|
default:
|
|
return multierror.Prefix(errors.New("invalid value for 'dh_type'"), fmt.Sprintf("sink.%s", s.Type))
|
|
}
|
|
|
|
if s.AADEnvVar != "" {
|
|
s.AAD = os.Getenv(s.AADEnvVar)
|
|
s.AADEnvVar = ""
|
|
}
|
|
|
|
switch {
|
|
case s.DHPath == "" && s.DHType == "":
|
|
if s.AAD != "" {
|
|
return multierror.Prefix(errors.New("specifying AAD data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
|
|
}
|
|
if s.DeriveKey {
|
|
return multierror.Prefix(errors.New("specifying 'derive_key' data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
|
|
}
|
|
case s.DHPath != "" && s.DHType != "":
|
|
default:
|
|
return multierror.Prefix(errors.New("'dh_type' and 'dh_path' must be specified together"), fmt.Sprintf("sink.%s", s.Type))
|
|
}
|
|
|
|
ts = append(ts, &s)
|
|
}
|
|
|
|
result.AutoAuth.Sinks = ts
|
|
return nil
|
|
}
|
|
|
|
func parseTemplateConfig(result *Config, list *ast.ObjectList) error {
|
|
name := "template_config"
|
|
|
|
templateConfigList := list.Filter(name)
|
|
if len(templateConfigList.Items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(templateConfigList.Items) > 1 {
|
|
return fmt.Errorf("at most one %q block is allowed", name)
|
|
}
|
|
|
|
// Get our item
|
|
item := templateConfigList.Items[0]
|
|
|
|
var cfg TemplateConfig
|
|
if err := hcl.DecodeObject(&cfg, item.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
result.TemplateConfig = &cfg
|
|
|
|
if result.TemplateConfig.StaticSecretRenderIntRaw != nil {
|
|
var err error
|
|
if result.TemplateConfig.StaticSecretRenderInt, err = parseutil.ParseDurationSecond(result.TemplateConfig.StaticSecretRenderIntRaw); err != nil {
|
|
return err
|
|
}
|
|
result.TemplateConfig.StaticSecretRenderIntRaw = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseTemplates(result *Config, list *ast.ObjectList) error {
|
|
name := "template"
|
|
|
|
templateList := list.Filter(name)
|
|
if len(templateList.Items) < 1 {
|
|
return nil
|
|
}
|
|
|
|
var tcs []*ctconfig.TemplateConfig
|
|
|
|
for _, item := range templateList.Items {
|
|
var shadow interface{}
|
|
if err := hcl.DecodeObject(&shadow, item.Val); err != nil {
|
|
return fmt.Errorf("error decoding config: %s", err)
|
|
}
|
|
|
|
// Convert to a map and flatten the keys we want to flatten
|
|
parsed, ok := shadow.(map[string]interface{})
|
|
if !ok {
|
|
return errors.New("error converting config")
|
|
}
|
|
|
|
// flatten the wait or exec fields. The initial "wait" or "exec" value, if given, is a
|
|
// []map[string]interface{}, but we need it to be map[string]interface{}.
|
|
// Consul Template has a method flattenKeys that walks all of parsed and
|
|
// flattens every key. For Vault Agent, we only care about the wait input.
|
|
// Only one wait/exec stanza is supported, however Consul Template does not error
|
|
// with multiple instead it flattens them down, with last value winning.
|
|
// Here we take the last element of the parsed["wait"] or parsed["exec"] slice to keep
|
|
// consistency with Consul Template behavior.
|
|
wait, ok := parsed["wait"].([]map[string]interface{})
|
|
if ok {
|
|
parsed["wait"] = wait[len(wait)-1]
|
|
}
|
|
|
|
exec, ok := parsed["exec"].([]map[string]interface{})
|
|
if ok {
|
|
parsed["exec"] = exec[len(exec)-1]
|
|
}
|
|
|
|
var tc ctconfig.TemplateConfig
|
|
|
|
// Use mapstructure to populate the basic config fields
|
|
var md mapstructure.Metadata
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
ctconfig.StringToFileModeFunc(),
|
|
ctconfig.StringToWaitDurationHookFunc(),
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
),
|
|
ErrorUnused: true,
|
|
Metadata: &md,
|
|
Result: &tc,
|
|
})
|
|
if err != nil {
|
|
return errors.New("mapstructure decoder creation failed")
|
|
}
|
|
if err := decoder.Decode(parsed); err != nil {
|
|
return err
|
|
}
|
|
tcs = append(tcs, &tc)
|
|
}
|
|
result.Templates = tcs
|
|
return nil
|
|
}
|
|
|
|
func parseExec(result *Config, list *ast.ObjectList) error {
|
|
name := "exec"
|
|
|
|
execList := list.Filter(name)
|
|
if len(execList.Items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(execList.Items) > 1 {
|
|
return fmt.Errorf("at most one %q block is allowed", name)
|
|
}
|
|
|
|
item := execList.Items[0]
|
|
var shadow interface{}
|
|
if err := hcl.DecodeObject(&shadow, item.Val); err != nil {
|
|
return fmt.Errorf("error decoding config: %s", err)
|
|
}
|
|
|
|
parsed, ok := shadow.(map[string]interface{})
|
|
if !ok {
|
|
return errors.New("error converting config")
|
|
}
|
|
|
|
var execConfig ExecConfig
|
|
var md mapstructure.Metadata
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
ctconfig.StringToFileModeFunc(),
|
|
ctconfig.StringToWaitDurationHookFunc(),
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
ctsignals.StringToSignalFunc(),
|
|
),
|
|
ErrorUnused: true,
|
|
Metadata: &md,
|
|
Result: &execConfig,
|
|
})
|
|
if err != nil {
|
|
return errors.New("mapstructure decoder creation failed")
|
|
}
|
|
if err := decoder.Decode(parsed); err != nil {
|
|
return err
|
|
}
|
|
|
|
// if the user does not specify a restart signal, default to SIGTERM
|
|
if execConfig.RestartStopSignal == nil {
|
|
execConfig.RestartStopSignal = syscall.SIGTERM
|
|
}
|
|
|
|
if execConfig.RestartOnSecretChanges == "" {
|
|
execConfig.RestartOnSecretChanges = "always"
|
|
}
|
|
|
|
result.Exec = &execConfig
|
|
return nil
|
|
}
|
|
|
|
func parseEnvTemplates(result *Config, list *ast.ObjectList) error {
|
|
name := "env_template"
|
|
|
|
envTemplateList := list.Filter(name)
|
|
|
|
if len(envTemplateList.Items) < 1 {
|
|
return nil
|
|
}
|
|
|
|
envTemplates := make([]*ctconfig.TemplateConfig, 0, len(envTemplateList.Items))
|
|
|
|
for _, item := range envTemplateList.Items {
|
|
var shadow interface{}
|
|
if err := hcl.DecodeObject(&shadow, item.Val); err != nil {
|
|
return fmt.Errorf("error decoding config: %s", err)
|
|
}
|
|
|
|
// Convert to a map and flatten the keys we want to flatten
|
|
parsed, ok := shadow.(map[string]any)
|
|
if !ok {
|
|
return errors.New("error converting config")
|
|
}
|
|
|
|
var templateConfig ctconfig.TemplateConfig
|
|
var md mapstructure.Metadata
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
ctconfig.StringToFileModeFunc(),
|
|
ctconfig.StringToWaitDurationHookFunc(),
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
ctsignals.StringToSignalFunc(),
|
|
),
|
|
ErrorUnused: true,
|
|
Metadata: &md,
|
|
Result: &templateConfig,
|
|
})
|
|
if err != nil {
|
|
return errors.New("mapstructure decoder creation failed")
|
|
}
|
|
if err := decoder.Decode(parsed); err != nil {
|
|
return err
|
|
}
|
|
|
|
// parse the keys in the item for the environment variable name
|
|
if numberOfKeys := len(item.Keys); numberOfKeys != 1 {
|
|
return fmt.Errorf("expected one and only one environment variable name, got %d", numberOfKeys)
|
|
}
|
|
|
|
// hcl parses this with extra quotes if quoted in config file
|
|
environmentVariableName := strings.Trim(item.Keys[0].Token.Text, `"`)
|
|
|
|
templateConfig.MapToEnvironmentVariable = pointerutil.StringPtr(environmentVariableName)
|
|
|
|
envTemplates = append(envTemplates, &templateConfig)
|
|
}
|
|
|
|
result.EnvTemplates = envTemplates
|
|
return nil
|
|
}
|