vault/command/agent/config/config_test.go
Ryan Cragun 58a49e6ce0
VAULT-33758: IPv6 address conformance for proxy and agent (#29517)
This is a follow-up to our initial work[0] to address RFC-5952 §4 conformance for IPv6 addresses in Vault. The initial pass focused on the vault server configuration and start-up routines. This follow-up focuses on Agent and Proxy, with a few minor improvements for server.

The approach generally mirrors the server implementation but also adds support for normalization with CLI configuration overrides.

One aspect we do not normalize currently is Agent/Proxy client creation to the Vault server with credentials taken from environment variables, as it would require larger changes to the `api` module. In practice this ought to be fine for the majority of cases.

[0]: https://github.com/hashicorp/vault/pull/29228
2025-02-27 15:57:46 -07:00

2547 lines
63 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package config
import (
"os"
"syscall"
"testing"
"time"
"github.com/go-test/deep"
ctconfig "github.com/hashicorp/consul-template/config"
"github.com/hashicorp/vault/command/agentproxyshared"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/sdk/helper/pointerutil"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
)
func FloatPtr(t float64) *float64 {
return &t
}
func TestLoadConfigFile_AgentCache(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
Listeners: []*configutil.Listener{
{
Type: "unix",
Address: "/path/to/socket",
TLSDisable: true,
SocketMode: "configmode",
SocketUser: "configuser",
SocketGroup: "configgroup",
},
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
{
Type: "tcp",
Address: "127.0.0.1:3000",
Role: "metrics_only",
TLSDisable: true,
},
{
Type: "tcp",
Role: "default",
Address: "127.0.0.1:8400",
TLSKeyFile: "/path/to/cakey.pem",
TLSCertFile: "/path/to/cacert.pem",
},
},
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
APIProxy: &APIProxy{
UseAutoAuthToken: true,
ForceAutoAuthToken: false,
},
Cache: &Cache{
UseAutoAuthToken: true,
UseAutoAuthTokenRaw: true,
ForceAutoAuthToken: false,
Persist: &agentproxyshared.PersistConfig{
Type: "kubernetes",
Path: "/vault/agent-cache/",
KeepAfterImport: true,
ExitOnErr: true,
ServiceAccountTokenFile: "/tmp/serviceaccount/token",
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
CACert: "config_ca_cert",
CAPath: "config_ca_path",
TLSSkipVerifyRaw: interface{}("true"),
TLSSkipVerify: true,
ClientCert: "config_client_cert",
ClientKey: "config_client_key",
Retry: &Retry{
NumRetries: 12,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
config, err = LoadConfigFile("./test-fixtures/config-cache-embedded-type.hcl")
if err != nil {
t.Fatal(err)
}
expected.Vault.TLSSkipVerifyRaw = interface{}(true)
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigDir_AgentCache(t *testing.T) {
config, err := LoadConfig("./test-fixtures/config-dir-cache/")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
Listeners: []*configutil.Listener{
{
Type: "unix",
Address: "/path/to/socket",
TLSDisable: true,
SocketMode: "configmode",
SocketUser: "configuser",
SocketGroup: "configgroup",
},
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
{
Type: "tcp",
Address: "127.0.0.1:3000",
Role: "metrics_only",
TLSDisable: true,
},
{
Type: "tcp",
Role: "default",
Address: "127.0.0.1:8400",
TLSKeyFile: "/path/to/cakey.pem",
TLSCertFile: "/path/to/cacert.pem",
},
},
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
APIProxy: &APIProxy{
UseAutoAuthToken: true,
ForceAutoAuthToken: false,
},
Cache: &Cache{
UseAutoAuthToken: true,
UseAutoAuthTokenRaw: true,
ForceAutoAuthToken: false,
Persist: &agentproxyshared.PersistConfig{
Type: "kubernetes",
Path: "/vault/agent-cache/",
KeepAfterImport: true,
ExitOnErr: true,
ServiceAccountTokenFile: "/tmp/serviceaccount/token",
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
CACert: "config_ca_cert",
CAPath: "config_ca_path",
TLSSkipVerifyRaw: interface{}("true"),
TLSSkipVerify: true,
ClientCert: "config_client_cert",
ClientKey: "config_client_key",
Retry: &Retry{
NumRetries: 12,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
config, err = LoadConfigFile("./test-fixtures/config-dir-cache/config-cache1.hcl")
if err != nil {
t.Fatal(err)
}
config2, err := LoadConfigFile("./test-fixtures/config-dir-cache/config-cache2.hcl")
if err != nil {
t.Fatal(err)
}
mergedConfig := config.Merge(config2)
mergedConfig.Prune()
if diff := deep.Equal(mergedConfig, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigDir_AutoAuthAndListener(t *testing.T) {
config, err := LoadConfig("./test-fixtures/config-dir-auto-auth-and-listener/")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
config, err = LoadConfigFile("./test-fixtures/config-dir-auto-auth-and-listener/config1.hcl")
if err != nil {
t.Fatal(err)
}
config2, err := LoadConfigFile("./test-fixtures/config-dir-auto-auth-and-listener/config2.hcl")
mergedConfig := config.Merge(config2)
mergedConfig.Prune()
if diff := deep.Equal(mergedConfig, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigDir_VaultBlock(t *testing.T) {
config, err := LoadConfig("./test-fixtures/config-dir-vault-block/")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
CACert: "config_ca_cert",
CAPath: "config_ca_path",
TLSSkipVerifyRaw: interface{}("true"),
TLSSkipVerify: true,
ClientCert: "config_client_cert",
ClientKey: "config_client_key",
Retry: &Retry{
NumRetries: 12,
},
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
config, err = LoadConfigFile("./test-fixtures/config-dir-vault-block/config1.hcl")
if err != nil {
t.Fatal(err)
}
config2, err := LoadConfigFile("./test-fixtures/config-dir-vault-block/config2.hcl")
mergedConfig := config.Merge(config2)
mergedConfig.Prune()
if diff := deep.Equal(mergedConfig, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_AgentCache_NoListeners(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache-no-listeners.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
APIProxy: &APIProxy{
UseAutoAuthToken: true,
ForceAutoAuthToken: false,
},
Cache: &Cache{
UseAutoAuthToken: true,
UseAutoAuthTokenRaw: true,
ForceAutoAuthToken: false,
Persist: &agentproxyshared.PersistConfig{
Type: "kubernetes",
Path: "/vault/agent-cache/",
KeepAfterImport: true,
ExitOnErr: true,
ServiceAccountTokenFile: "/tmp/serviceaccount/token",
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
CACert: "config_ca_cert",
CAPath: "config_ca_path",
TLSSkipVerifyRaw: interface{}("true"),
TLSSkipVerify: true,
ClientCert: "config_client_cert",
ClientKey: "config_client_key",
Retry: &Retry{
NumRetries: 12,
},
},
Templates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
// Test_LoadConfigFile_AutoAuth_AddrConformance verifies basic config file
// loading in addition to RFC-5942 §4 normalization of auto-auth methods.
// See: https://rfc-editor.org/rfc/rfc5952.html
func Test_LoadConfigFile_AutoAuth_AddrConformance(t *testing.T) {
t.Setenv("TEST_AAD_ENV", "aad")
for name, method := range map[string]*Method{
"aws": {
Type: "aws",
MountPath: "auth/aws",
Namespace: "aws-namespace/",
Config: map[string]any{
"role": "foobar",
},
},
"azure": {
Type: "azure",
MountPath: "auth/azure",
Namespace: "azure-namespace/",
Config: map[string]any{
"authenticate_from_environment": true,
"role": "dev-role",
"resource": "https://[2001:0:0:1::1]",
},
},
"gcp": {
Type: "gcp",
MountPath: "auth/gcp",
Namespace: "gcp-namespace/",
Config: map[string]any{
"role": "dev-role",
"service_account": "https://[2001:db8:ac3:fe4::1]",
},
},
} {
t.Run(name, func(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-auto-auth-" + name + ".hcl")
require.NoError(t, err)
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
Listeners: []*configutil.Listener{
{
Type: "unix",
Address: "/path/to/socket",
TLSDisable: true,
AgentAPI: &configutil.AgentAPI{
EnableQuit: true,
},
},
{
Type: "tcp",
Address: "2001:db8::1:8200", // Normalized
TLSDisable: true,
},
{
Type: "tcp",
Address: "[2001:0:0:1::1]:3000", // Normalized
Role: "metrics_only",
TLSDisable: true,
},
{
Type: "tcp",
Role: "default",
Address: "2001:db8:0:1:1:1:1:1:8400", // Normalized
TLSKeyFile: "/path/to/cakey.pem",
TLSCertFile: "/path/to/cacert.pem",
},
},
LogFile: "/var/log/vault/vault-agent.log",
},
Vault: &Vault{
Address: "https://[2001:db8::1]:8200", // Address is normalized
Retry: &Retry{
NumRetries: 12, // Default number of retries when a vault stanza is set
},
},
AutoAuth: &AutoAuth{
Method: method, // Method properties are normalized correctly
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
{
Type: "file",
WrapTTL: 5 * time.Minute,
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath2",
AAD: "aad",
DeriveKey: true,
Config: map[string]interface{}{
"path": "/tmp/file-bar",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
require.EqualValues(t, expected, config)
})
}
}
func TestLoadConfigFile_Method_Wrapping(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-method-wrapping.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
ExitOnError: false,
WrapTTL: 5 * time.Minute,
MaxBackoff: 2 * time.Minute,
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Method_InitialBackoff(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-method-initial-backoff.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
ExitOnError: false,
WrapTTL: 5 * time.Minute,
MinBackoff: 5 * time.Second,
MaxBackoff: 2 * time.Minute,
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Method_ExitOnErr(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-method-exit-on-err.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
ExitOnError: true,
WrapTTL: 5 * time.Minute,
MinBackoff: 5 * time.Second,
MaxBackoff: 2 * time.Minute,
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_AgentCache_NoAutoAuth(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache-no-auto_auth.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
Cache: &Cache{},
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Bad_AgentCache_InconsisentAutoAuth(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-cache-inconsistent-auto_auth.hcl")
if err != nil {
t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err)
}
if config == nil {
t.Fatal("config was nil")
}
err = config.ValidateConfig()
if err == nil {
t.Fatal("ValidateConfig should return an error when use_auto_auth_token=true and no auto_auth section present")
}
}
func TestLoadConfigFile_Bad_AgentCache_ForceAutoAuthNoMethod(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-cache-force-token-no-auth-method.hcl")
if err != nil {
t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err)
}
if config == nil {
t.Fatal("config was nil")
}
err = config.ValidateConfig()
if err == nil {
t.Fatal("ValidateConfig should return an error when use_auto_auth_token=force and no auto_auth section present")
}
}
func TestLoadConfigFile_Bad_AgentCache_NoListeners(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/bad-config-cache-no-listeners.hcl")
if err != nil {
t.Fatalf("LoadConfigFile should return an error for this config")
}
}
func TestLoadConfigFile_Bad_AutoAuth_Wrapped_Multiple_Sinks(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/bad-config-auto_auth-wrapped-multiple-sinks.hcl")
if err == nil {
t.Fatalf("LoadConfigFile should return an error for this config, err: %v", err)
}
}
func TestLoadConfigFile_Bad_AutoAuth_Nosinks_Nocache_Notemplates(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-auto_auth-nosinks-nocache-notemplates.hcl")
if err != nil {
t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err)
}
if config == nil {
t.Fatal("config was nil")
}
err = config.ValidateConfig()
if err == nil {
t.Fatal("ValidateConfig should return an error when auto_auth configured and there are no sinks, caches or templates")
}
}
func TestLoadConfigFile_Bad_AutoAuth_Both_Wrapping_Types(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/bad-config-method-wrapping-and-sink-wrapping.hcl")
if err == nil {
t.Fatalf("LoadConfigFile should return an error for this config")
}
}
func TestLoadConfigFile_Bad_AgentCache_AutoAuth_Method_wrapping(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-cache-auto_auth-method-wrapping.hcl")
if err != nil {
t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err)
}
if config == nil {
t.Fatal("config was nil")
}
err = config.ValidateConfig()
if err == nil {
t.Fatal("ValidateConfig should return an error when auth_auth.method.wrap_ttl nonzero and cache.use_auto_auth_token=true")
}
}
func TestLoadConfigFile_Bad_APIProxy_And_Cache_Same_Config(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-api_proxy-cache.hcl")
if err != nil {
t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err)
}
if config == nil {
t.Fatal("config was nil")
}
err = config.ValidateConfig()
if err == nil {
t.Fatal("ValidateConfig should return an error when cache and api_proxy try and configure the same value")
}
}
func TestLoadConfigFile_AgentCache_AutoAuth_NoSink(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-no-sink.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
},
APIProxy: &APIProxy{
UseAutoAuthToken: true,
ForceAutoAuthToken: false,
},
Cache: &Cache{
UseAutoAuthToken: true,
UseAutoAuthTokenRaw: true,
ForceAutoAuthToken: false,
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_AgentCache_AutoAuth_Force(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-force.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
},
APIProxy: &APIProxy{
UseAutoAuthToken: true,
ForceAutoAuthToken: true,
},
Cache: &Cache{
UseAutoAuthToken: true,
UseAutoAuthTokenRaw: "force",
ForceAutoAuthToken: true,
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_AgentCache_AutoAuth_True(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-true.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
},
APIProxy: &APIProxy{
UseAutoAuthToken: true,
ForceAutoAuthToken: false,
},
Cache: &Cache{
UseAutoAuthToken: true,
UseAutoAuthTokenRaw: "true",
ForceAutoAuthToken: false,
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Agent_AutoAuth_APIProxyAllConfig(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-api_proxy-auto_auth-all-api_proxy-config.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
},
APIProxy: &APIProxy{
UseAutoAuthToken: true,
UseAutoAuthTokenRaw: "force",
ForceAutoAuthToken: true,
EnforceConsistency: "always",
WhenInconsistent: "forward",
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_AgentCache_AutoAuth_False(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-false.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
Cache: &Cache{
UseAutoAuthToken: false,
UseAutoAuthTokenRaw: "false",
ForceAutoAuthToken: false,
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_AgentCache_Persist(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-cache-persist-false.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
Cache: &Cache{
Persist: &agentproxyshared.PersistConfig{
Type: "kubernetes",
Path: "/vault/agent-cache/",
KeepAfterImport: false,
ExitOnErr: false,
ServiceAccountTokenFile: "",
},
},
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_AgentCache_PersistMissingType(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/config-cache-persist-empty-type.hcl")
if err == nil || os.IsNotExist(err) {
t.Fatal("expected error or file is missing")
}
}
func TestLoadConfigFile_TemplateConfig(t *testing.T) {
testCases := map[string]struct {
fixturePath string
expectedTemplateConfig TemplateConfig
}{
"set-true": {
"./test-fixtures/config-template_config.hcl",
TemplateConfig{
ExitOnRetryFailure: true,
StaticSecretRenderInt: 1 * time.Minute,
MaxConnectionsPerHost: 100,
LeaseRenewalThreshold: FloatPtr(0.8),
},
},
"empty": {
"./test-fixtures/config-template_config-empty.hcl",
TemplateConfig{
ExitOnRetryFailure: false,
MaxConnectionsPerHost: 10,
},
},
"missing": {
"./test-fixtures/config-template_config-missing.hcl",
TemplateConfig{
MaxConnectionsPerHost: 10,
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
config, err := LoadConfigFile(tc.fixturePath)
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
NumRetries: 5,
},
},
TemplateConfig: &tc.expectedTemplateConfig,
Templates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
})
}
}
// TestLoadConfigFile_TemplateConfig_MergeConfigs tests that in the case of multiple config files,
// a provided TemplateConfig in one file is properly preserved in the merged config
func TestLoadConfigFile_TemplateConfig_MergeConfigs(t *testing.T) {
configPaths := []string{
"./test-fixtures/config-template_config.hcl",
"./test-fixtures/config-template-min-no-auth.hcl",
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
NumRetries: 5,
},
},
TemplateConfig: &TemplateConfig{
ExitOnRetryFailure: true,
StaticSecretRenderInt: 1 * time.Minute,
MaxConnectionsPerHost: 100,
LeaseRenewalThreshold: FloatPtr(0.8),
},
Templates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
},
{
Source: pointerutil.StringPtr("/path/on/disk/to/template2.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render2.txt"),
},
},
}
cfg := NewConfig()
for _, path := range configPaths {
configFromPath, err := LoadConfigFile(path)
if err != nil {
t.Fatal(err)
}
cfg = cfg.Merge(configFromPath)
}
cfg.Prune()
if diff := deep.Equal(cfg, expected); diff != nil {
t.Fatal(diff)
}
}
// TestLoadConfigFile_Template tests template definitions in Vault Agent
func TestLoadConfigFile_Template(t *testing.T) {
testCases := map[string]struct {
fixturePath string
expectedTemplates []*ctconfig.TemplateConfig
}{
"min": {
fixturePath: "./test-fixtures/config-template-min.hcl",
expectedTemplates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
},
},
},
"full": {
fixturePath: "./test-fixtures/config-template-full.hcl",
expectedTemplates: []*ctconfig.TemplateConfig{
{
Backup: pointerutil.BoolPtr(true),
Command: []string{"restart service foo"},
CommandTimeout: pointerutil.TimeDurationPtr("60s"),
Contents: pointerutil.StringPtr("{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}"),
CreateDestDirs: pointerutil.BoolPtr(true),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
ErrMissingKey: pointerutil.BoolPtr(true),
LeftDelim: pointerutil.StringPtr("<<"),
Perms: pointerutil.FileModePtr(0o655),
RightDelim: pointerutil.StringPtr(">>"),
SandboxPath: pointerutil.StringPtr("/path/on/disk/where"),
Exec: &ctconfig.ExecConfig{
Command: []string{"foo"},
Timeout: pointerutil.TimeDurationPtr("10s"),
},
Wait: &ctconfig.WaitConfig{
Min: pointerutil.TimeDurationPtr("10s"),
Max: pointerutil.TimeDurationPtr("40s"),
},
},
},
},
"many": {
fixturePath: "./test-fixtures/config-template-many.hcl",
expectedTemplates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
ErrMissingKey: pointerutil.BoolPtr(false),
CreateDestDirs: pointerutil.BoolPtr(true),
Command: []string{"restart service foo"},
Perms: pointerutil.FileModePtr(0o600),
},
{
Source: pointerutil.StringPtr("/path/on/disk/to/template2.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render2.txt"),
Backup: pointerutil.BoolPtr(true),
Perms: pointerutil.FileModePtr(0o755),
Wait: &ctconfig.WaitConfig{
Min: pointerutil.TimeDurationPtr("2s"),
Max: pointerutil.TimeDurationPtr("10s"),
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
config, err := LoadConfigFile(tc.fixturePath)
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Templates: tc.expectedTemplates,
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
})
}
}
// TestLoadConfigFile_Template_NoSinks tests template definitions without sinks in Vault Agent
func TestLoadConfigFile_Template_NoSinks(t *testing.T) {
testCases := map[string]struct {
fixturePath string
expectedTemplates []*ctconfig.TemplateConfig
}{
"min": {
fixturePath: "./test-fixtures/config-template-min-nosink.hcl",
expectedTemplates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
},
},
},
"full": {
fixturePath: "./test-fixtures/config-template-full-nosink.hcl",
expectedTemplates: []*ctconfig.TemplateConfig{
{
Backup: pointerutil.BoolPtr(true),
Command: []string{"restart service foo"},
CommandTimeout: pointerutil.TimeDurationPtr("60s"),
Contents: pointerutil.StringPtr("{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}"),
CreateDestDirs: pointerutil.BoolPtr(true),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
ErrMissingKey: pointerutil.BoolPtr(true),
LeftDelim: pointerutil.StringPtr("<<"),
Perms: pointerutil.FileModePtr(0o655),
RightDelim: pointerutil.StringPtr(">>"),
SandboxPath: pointerutil.StringPtr("/path/on/disk/where"),
Wait: &ctconfig.WaitConfig{
Min: pointerutil.TimeDurationPtr("10s"),
Max: pointerutil.TimeDurationPtr("40s"),
},
},
},
},
"many": {
fixturePath: "./test-fixtures/config-template-many-nosink.hcl",
expectedTemplates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
ErrMissingKey: pointerutil.BoolPtr(false),
CreateDestDirs: pointerutil.BoolPtr(true),
Command: []string{"restart service foo"},
Perms: pointerutil.FileModePtr(0o600),
},
{
Source: pointerutil.StringPtr("/path/on/disk/to/template2.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render2.txt"),
Backup: pointerutil.BoolPtr(true),
Perms: pointerutil.FileModePtr(0o755),
Wait: &ctconfig.WaitConfig{
Min: pointerutil.TimeDurationPtr("2s"),
Max: pointerutil.TimeDurationPtr("10s"),
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
config, err := LoadConfigFile(tc.fixturePath)
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: nil,
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Templates: tc.expectedTemplates,
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
})
}
}
// TestLoadConfigFile_Template_WithCache tests ensures that cache {} stanza is
// permitted in vault agent configuration with template(s)
func TestLoadConfigFile_Template_WithCache(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-template-with-cache.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
},
Cache: &Cache{},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Templates: []*ctconfig.TemplateConfig{
{
Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"),
Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"),
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Vault_Retry(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-vault-retry.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
NumRetries: 5,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Vault_Retry_Empty(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-vault-retry-empty.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_EnforceConsistency(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-consistency.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
PidFile: "",
},
Cache: &Cache{
EnforceConsistency: "always",
WhenInconsistent: "retry",
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_EnforceConsistency_APIProxy(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-consistency-apiproxy.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*configutil.Listener{
{
Type: "tcp",
Address: "127.0.0.1:8300",
TLSDisable: true,
},
},
PidFile: "",
},
APIProxy: &APIProxy{
EnforceConsistency: "always",
WhenInconsistent: "retry",
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Idle_Conns_All(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-all.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableIdleConns: []string{"auto-auth", "caching", "templating", "proxying"},
DisableIdleConnsAPIProxy: true,
DisableIdleConnsAutoAuth: true,
DisableIdleConnsTemplating: true,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Idle_Conns_Auto_Auth(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-auto-auth.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableIdleConns: []string{"auto-auth"},
DisableIdleConnsAPIProxy: false,
DisableIdleConnsAutoAuth: true,
DisableIdleConnsTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Idle_Conns_Templating(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-templating.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableIdleConns: []string{"templating"},
DisableIdleConnsAPIProxy: false,
DisableIdleConnsAutoAuth: false,
DisableIdleConnsTemplating: true,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Idle_Conns_Caching(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-caching.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableIdleConns: []string{"caching"},
DisableIdleConnsAPIProxy: true,
DisableIdleConnsAutoAuth: false,
DisableIdleConnsTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Idle_Conns_Proxying(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-proxying.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableIdleConns: []string{"proxying"},
DisableIdleConnsAPIProxy: true,
DisableIdleConnsAutoAuth: false,
DisableIdleConnsTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Idle_Conns_Empty(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-empty.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableIdleConns: []string{},
DisableIdleConnsAPIProxy: false,
DisableIdleConnsAutoAuth: false,
DisableIdleConnsTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Idle_Conns_Env(t *testing.T) {
err := os.Setenv(DisableIdleConnsEnv, "auto-auth,caching,templating")
defer os.Unsetenv(DisableIdleConnsEnv)
if err != nil {
t.Fatal(err)
}
config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-empty.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableIdleConns: []string{"auto-auth", "caching", "templating"},
DisableIdleConnsAPIProxy: true,
DisableIdleConnsAutoAuth: true,
DisableIdleConnsTemplating: true,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Bad_Value_Disable_Idle_Conns(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/bad-config-disable-idle-connections.hcl")
if err == nil {
t.Fatal("should have error, it didn't")
}
}
func TestLoadConfigFile_Disable_Keep_Alives_All(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-all.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableKeepAlives: []string{"auto-auth", "caching", "templating", "proxying"},
DisableKeepAlivesAPIProxy: true,
DisableKeepAlivesAutoAuth: true,
DisableKeepAlivesTemplating: true,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Keep_Alives_Auto_Auth(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-auto-auth.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableKeepAlives: []string{"auto-auth"},
DisableKeepAlivesAPIProxy: false,
DisableKeepAlivesAutoAuth: true,
DisableKeepAlivesTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Keep_Alives_Templating(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-templating.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableKeepAlives: []string{"templating"},
DisableKeepAlivesAPIProxy: false,
DisableKeepAlivesAutoAuth: false,
DisableKeepAlivesTemplating: true,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Keep_Alives_Caching(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-caching.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableKeepAlives: []string{"caching"},
DisableKeepAlivesAPIProxy: true,
DisableKeepAlivesAutoAuth: false,
DisableKeepAlivesTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Keep_Alives_Proxying(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-proxying.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableKeepAlives: []string{"proxying"},
DisableKeepAlivesAPIProxy: true,
DisableKeepAlivesAutoAuth: false,
DisableKeepAlivesTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Keep_Alives_Empty(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-empty.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableKeepAlives: []string{},
DisableKeepAlivesAPIProxy: false,
DisableKeepAlivesAutoAuth: false,
DisableKeepAlivesTemplating: false,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Disable_Keep_Alives_Env(t *testing.T) {
err := os.Setenv(DisableKeepAlivesEnv, "auto-auth,caching,templating")
defer os.Unsetenv(DisableKeepAlivesEnv)
if err != nil {
t.Fatal(err)
}
config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-empty.hcl")
if err != nil {
t.Fatal(err)
}
expected := &Config{
SharedConfig: &configutil.SharedConfig{
PidFile: "./pidfile",
},
DisableKeepAlives: []string{"auto-auth", "caching", "templating"},
DisableKeepAlivesAPIProxy: true,
DisableKeepAlivesAutoAuth: true,
DisableKeepAlivesTemplating: true,
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
MountPath: "auth/aws",
Namespace: "my-namespace/",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
},
},
TemplateConfig: &TemplateConfig{
MaxConnectionsPerHost: DefaultTemplateConfigMaxConnsPerHost,
},
Vault: &Vault{
Address: "http://127.0.0.1:1111",
Retry: &Retry{
ctconfig.DefaultRetryAttempts,
},
},
}
config.Prune()
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}
func TestLoadConfigFile_Bad_Value_Disable_Keep_Alives(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/bad-config-disable-keep-alives.hcl")
if err == nil {
t.Fatal("should have error, it didn't")
}
}
// TestLoadConfigFile_EnvTemplates_Simple loads and validates an env_template config
func TestLoadConfigFile_EnvTemplates_Simple(t *testing.T) {
cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-simple.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := cfg.ValidateConfig(); err != nil {
t.Fatalf("validation error: %s", err)
}
expectedKey := "MY_DATABASE_USER"
found := false
for _, envTemplate := range cfg.EnvTemplates {
if *envTemplate.MapToEnvironmentVariable == expectedKey {
found = true
}
}
if !found {
t.Fatalf("expected environment variable name to be populated")
}
}
// TestLoadConfigFile_EnvTemplates_Complex loads and validates an env_template config
func TestLoadConfigFile_EnvTemplates_Complex(t *testing.T) {
cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-complex.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := cfg.ValidateConfig(); err != nil {
t.Fatalf("validation error: %s", err)
}
expectedKeys := []string{
"FOO_PASSWORD",
"FOO_USER",
}
envExists := func(key string) bool {
for _, envTmpl := range cfg.EnvTemplates {
if *envTmpl.MapToEnvironmentVariable == key {
return true
}
}
return false
}
for _, expected := range expectedKeys {
if !envExists(expected) {
t.Fatalf("expected environment variable %s", expected)
}
}
}
// TestLoadConfigFile_EnvTemplates_WithSource loads and validates an
// env_template config with "source" instead of "contents"
func TestLoadConfigFile_EnvTemplates_WithSource(t *testing.T) {
cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-with-source.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := cfg.ValidateConfig(); err != nil {
t.Fatalf("validation error: %s", err)
}
}
// TestLoadConfigFile_EnvTemplates_NoName ensures that env_template with no name triggers an error
func TestLoadConfigFile_EnvTemplates_NoName(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-no-name.hcl")
if err == nil {
t.Fatalf("expected error")
}
}
// TestLoadConfigFile_EnvTemplates_ExecInvalidSignal ensures that an invalid signal triggers an error
func TestLoadConfigFile_EnvTemplates_ExecInvalidSignal(t *testing.T) {
_, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-invalid-signal.hcl")
if err == nil {
t.Fatalf("expected error")
}
}
// TestLoadConfigFile_EnvTemplates_ExecSimple validates the exec section with default parameters
func TestLoadConfigFile_EnvTemplates_ExecSimple(t *testing.T) {
cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-simple.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := cfg.ValidateConfig(); err != nil {
t.Fatalf("validation error: %s", err)
}
expectedCmd := []string{"/path/to/my/app", "arg1", "arg2"}
if !slices.Equal(cfg.Exec.Command, expectedCmd) {
t.Fatal("exec.command does not have expected value")
}
// check defaults
if cfg.Exec.RestartOnSecretChanges != "always" {
t.Fatalf("expected cfg.Exec.RestartOnSecretChanges to be 'always', got '%s'", cfg.Exec.RestartOnSecretChanges)
}
if cfg.Exec.RestartStopSignal != syscall.SIGTERM {
t.Fatalf("expected cfg.Exec.RestartStopSignal to be 'syscall.SIGTERM', got '%s'", cfg.Exec.RestartStopSignal)
}
}
// TestLoadConfigFile_EnvTemplates_ExecComplex validates the exec section with non-default parameters
func TestLoadConfigFile_EnvTemplates_ExecComplex(t *testing.T) {
cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-complex.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := cfg.ValidateConfig(); err != nil {
t.Fatalf("validation error: %s", err)
}
if !slices.Equal(cfg.Exec.Command, []string{"env"}) {
t.Fatal("exec.command does not have expected value")
}
if cfg.Exec.RestartOnSecretChanges != "never" {
t.Fatalf("expected cfg.Exec.RestartOnSecretChanges to be 'never', got %q", cfg.Exec.RestartOnSecretChanges)
}
if cfg.Exec.RestartStopSignal != syscall.SIGINT {
t.Fatalf("expected cfg.Exec.RestartStopSignal to be 'syscall.SIGINT', got %q", cfg.Exec.RestartStopSignal)
}
}
// TestLoadConfigFile_Bad_EnvTemplates_MissingExec ensures that ValidateConfig
// errors when "env_template" stanza(s) are specified but "exec" is missing
func TestLoadConfigFile_Bad_EnvTemplates_MissingExec(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-missing-exec.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := config.ValidateConfig(); err == nil {
t.Fatal("expected an error from ValidateConfig: exec section is missing")
}
}
// TestLoadConfigFile_Bad_EnvTemplates_WithProxy ensures that ValidateConfig
// errors when both env_template and api_proxy stanzas are present
func TestLoadConfigFile_Bad_EnvTemplates_WithProxy(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-with-proxy.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := config.ValidateConfig(); err == nil {
t.Fatal("expected an error from ValidateConfig: listener / api_proxy are not compatible with env_template")
}
}
// TestLoadConfigFile_Bad_EnvTemplates_WithFileTemplates ensures that
// ValidateConfig errors when both env_template and template stanzas are present
func TestLoadConfigFile_Bad_EnvTemplates_WithFileTemplates(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-with-file-templates.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := config.ValidateConfig(); err == nil {
t.Fatal("expected an error from ValidateConfig: file template stanza is not compatible with env_template")
}
}
// TestLoadConfigFile_Bad_EnvTemplates_DisalowedFields ensure that
// ValidateConfig errors for disalowed env_template fields
func TestLoadConfigFile_Bad_EnvTemplates_DisalowedFields(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-disalowed-fields.hcl")
if err != nil {
t.Fatalf("error loading config file: %s", err)
}
if err := config.ValidateConfig(); err == nil {
t.Fatal("expected an error from ValidateConfig: disallowed fields specified in env_template")
}
}