agent: restart template runner on retry for unlimited retries (#11775)
* agent: restart template runner on retry for unlimited retries * template: log error message early * template: delegate retries back to template if param is set to true * agent: add and use the new template config stanza * agent: fix panic, fix existing tests * changelog: add changelog entry * agent: add tests for exit_on_retry_failure * agent: properly check on agent exit cases, add separate tests for missing key vs missing secrets * agent: add note on difference between missing key vs missing secret * docs: add docs for template_config * Update website/content/docs/agent/template-config.mdx Co-authored-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com> * Update website/content/docs/agent/template-config.mdx Co-authored-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com> * Update website/content/docs/agent/template-config.mdx Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com> * Update website/content/docs/agent/template-config.mdx Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com> * Update website/content/docs/agent/template-config.mdx Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com> * docs: fix exit_on_retry_failure, fix Functionality section * docs: update interaction title * template: add internal note on behavior for persist case * docs: update agent, template, and template-config docs * docs: update agent docs on retry stanza * Apply suggestions from code review Co-authored-by: Jim Kalafut <jkalafut@hashicorp.com> Co-authored-by: Theron Voran <tvoran@users.noreply.github.com> * Update changelog/11775.txt Co-authored-by: Brian Kassouf <briankassouf@users.noreply.github.com> * agent/test: rename expectExit to expectExitFromError * agent/test: add check on early exits on the happy path * Update website/content/docs/agent/template-config.mdx Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com> Co-authored-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com> Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com> Co-authored-by: Jim Kalafut <jkalafut@hashicorp.com> Co-authored-by: Theron Voran <tvoran@users.noreply.github.com> Co-authored-by: Brian Kassouf <briankassouf@users.noreply.github.com>
This commit is contained in:
parent
3ec17429e8
commit
c1a2a939f9
|
@ -0,0 +1,9 @@
|
|||
```release-note:change
|
||||
agent: Errors in the template engine will no longer cause agent to exit unless
|
||||
explicitly defined to do so. A new configuration parameter,
|
||||
`exit_on_retry_failure`, within the new top-level stanza, `template_config`, can
|
||||
be set to `true` in order to cause agent to exit. Note that for agent to exit if
|
||||
`template.error_on_missing_key` is set to `true`, `exit_on_retry_failure` must
|
||||
be also set to `true`. Otherwise, the template engine will log an error but then
|
||||
restart its internal runner.
|
||||
```
|
|
@ -119,6 +119,10 @@ func (ap *APIProxy) Send(ctx context.Context, req *SendRequest) (*SendResponse,
|
|||
}
|
||||
client.SetToken(req.Token)
|
||||
|
||||
// Derive and set a logger for the client
|
||||
clientLogger := ap.logger.Named("client")
|
||||
client.SetLogger(clientLogger)
|
||||
|
||||
// http.Transport will transparently request gzip and decompress the response, but only if
|
||||
// the client doesn't manually set the header. Removing any Accept-Encoding header allows the
|
||||
// transparent compression to occur.
|
||||
|
|
|
@ -22,11 +22,12 @@ import (
|
|||
type Config struct {
|
||||
*configutil.SharedConfig `hcl:"-"`
|
||||
|
||||
AutoAuth *AutoAuth `hcl:"auto_auth"`
|
||||
ExitAfterAuth bool `hcl:"exit_after_auth"`
|
||||
Cache *Cache `hcl:"cache"`
|
||||
Vault *Vault `hcl:"vault"`
|
||||
Templates []*ctconfig.TemplateConfig `hcl:"templates"`
|
||||
AutoAuth *AutoAuth `hcl:"auto_auth"`
|
||||
ExitAfterAuth bool `hcl:"exit_after_auth"`
|
||||
Cache *Cache `hcl:"cache"`
|
||||
Vault *Vault `hcl:"vault"`
|
||||
TemplateConfig *TemplateConfig `hcl:"template_config"`
|
||||
Templates []*ctconfig.TemplateConfig `hcl:"templates"`
|
||||
}
|
||||
|
||||
func (c *Config) Prune() {
|
||||
|
@ -116,6 +117,11 @@ type Sink struct {
|
|||
Config map[string]interface{}
|
||||
}
|
||||
|
||||
// TemplateConfig defines global behaviors around template
|
||||
type TemplateConfig struct {
|
||||
ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"`
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
SharedConfig: new(configutil.SharedConfig),
|
||||
|
@ -179,6 +185,10 @@ func LoadConfig(path string) (*Config, error) {
|
|||
return nil, fmt.Errorf("error parsing 'cache':%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)
|
||||
}
|
||||
|
@ -553,6 +563,31 @@ func parseSinks(result *Config, list *ast.ObjectList) error {
|
|||
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
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTemplates(result *Config, list *ast.ObjectList) error {
|
||||
name := "template"
|
||||
|
||||
|
|
|
@ -535,6 +535,59 @@ func TestLoadConfigFile_AgentCache_PersistMissingType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
"empty": {
|
||||
"./test-fixtures/config-template_config-empty.hcl",
|
||||
TemplateConfig{
|
||||
ExitOnRetryFailure: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
config, err := LoadConfig(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_Template tests template definitions in Vault Agent
|
||||
func TestLoadConfigFile_Template(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
vault {
|
||||
address = "http://127.0.0.1:1111"
|
||||
retry {
|
||||
num_retries = 5
|
||||
}
|
||||
}
|
||||
|
||||
template_config {}
|
||||
|
||||
template {
|
||||
source = "/path/on/disk/to/template.ctmpl"
|
||||
destination = "/path/on/disk/where/template/will/render.txt"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
vault {
|
||||
address = "http://127.0.0.1:1111"
|
||||
retry {
|
||||
num_retries = 5
|
||||
}
|
||||
}
|
||||
|
||||
template_config {
|
||||
exit_on_retry_failure = true
|
||||
}
|
||||
|
||||
template {
|
||||
source = "/path/on/disk/to/template.ctmpl"
|
||||
destination = "/path/on/disk/where/template/will/render.txt"
|
||||
}
|
|
@ -172,8 +172,20 @@ func (ts *Server) Run(ctx context.Context, incoming chan string, templates []*ct
|
|||
}
|
||||
|
||||
case err := <-ts.runner.ErrCh:
|
||||
ts.logger.Error("template server error", "error", err.Error())
|
||||
ts.runner.StopImmediately()
|
||||
return fmt.Errorf("template server: %w", err)
|
||||
|
||||
// Return after stopping the runner if exit on retry failure was
|
||||
// specified
|
||||
if ts.config.AgentConfig.TemplateConfig != nil && ts.config.AgentConfig.TemplateConfig.ExitOnRetryFailure {
|
||||
return fmt.Errorf("template server: %w", err)
|
||||
}
|
||||
|
||||
ts.runner, err = manager.NewRunner(runnerConfig, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template server failed to create: %w", err)
|
||||
}
|
||||
go ts.runner.Start()
|
||||
|
||||
case <-ts.runner.TemplateRenderedCh():
|
||||
// A template has been rendered, figure out what to do
|
||||
|
@ -253,6 +265,20 @@ func newRunnerConfig(sc *ServerConfig, templates ctconfig.TemplateConfigs) (*ctc
|
|||
// that need to be fixed for 1.7x/1.8
|
||||
if sc.AgentConfig.Cache != nil && sc.AgentConfig.Cache.Persist != nil && len(sc.AgentConfig.Listeners) != 0 {
|
||||
attempts = 0
|
||||
|
||||
// If we don't want exit on template retry failure (i.e. unlimited
|
||||
// retries), let consul-template handle retry and backoff logic.
|
||||
//
|
||||
// Note: This is a fixed value (12) that ends up being a multiplier to
|
||||
// retry.num_retires (i.e. 12 * N total retries per runner restart).
|
||||
// Since we are performing retries indefinitely this base number helps
|
||||
// prevent agent from spamming Vault if retry.num_retries is set to a
|
||||
// low value by forcing exponential backoff to be high towards the end
|
||||
// of retries during the process.
|
||||
if sc.AgentConfig.TemplateConfig != nil && !sc.AgentConfig.TemplateConfig.ExitOnRetryFailure {
|
||||
attempts = ctconfig.DefaultRetryAttempts
|
||||
}
|
||||
|
||||
scheme := "unix://"
|
||||
if sc.AgentConfig.Listeners[0].Type == "tcp" {
|
||||
scheme = "https://"
|
||||
|
|
|
@ -352,8 +352,9 @@ func TestServerRun(t *testing.T) {
|
|||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
templateMap map[string]*templateTest
|
||||
expectError bool
|
||||
templateMap map[string]*templateTest
|
||||
expectError bool
|
||||
exitOnRetryFailure bool
|
||||
}{
|
||||
"simple": {
|
||||
templateMap: map[string]*templateTest{
|
||||
|
@ -363,7 +364,8 @@ func TestServerRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectError: false,
|
||||
exitOnRetryFailure: false,
|
||||
},
|
||||
"multiple": {
|
||||
templateMap: map[string]*templateTest{
|
||||
|
@ -403,7 +405,8 @@ func TestServerRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectError: false,
|
||||
exitOnRetryFailure: false,
|
||||
},
|
||||
"bad secret": {
|
||||
templateMap: map[string]*templateTest{
|
||||
|
@ -413,7 +416,8 @@ func TestServerRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectError: true,
|
||||
exitOnRetryFailure: true,
|
||||
},
|
||||
"missing key": {
|
||||
templateMap: map[string]*templateTest{
|
||||
|
@ -424,7 +428,8 @@ func TestServerRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectError: true,
|
||||
exitOnRetryFailure: true,
|
||||
},
|
||||
"permission denied": {
|
||||
templateMap: map[string]*templateTest{
|
||||
|
@ -434,7 +439,8 @@ func TestServerRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectError: true,
|
||||
exitOnRetryFailure: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -458,6 +464,9 @@ func TestServerRun(t *testing.T) {
|
|||
NumRetries: 3,
|
||||
},
|
||||
},
|
||||
TemplateConfig: &config.TemplateConfig{
|
||||
ExitOnRetryFailure: tc.exitOnRetryFailure,
|
||||
},
|
||||
},
|
||||
LogLevel: hclog.Trace,
|
||||
LogWriter: hclog.DefaultOutput,
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/helper/logging"
|
||||
"github.com/hashicorp/vault/sdk/helper/pointerutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/mitchellh/cli"
|
||||
|
@ -1387,6 +1388,9 @@ vault {
|
|||
tls_skip_verify = true
|
||||
}
|
||||
%s
|
||||
template_config {
|
||||
exit_on_retry_failure = true
|
||||
}
|
||||
`, methodConf, serverClient.Address(), retryConf, templateConfig)
|
||||
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
|
@ -1691,3 +1695,307 @@ vault {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_TemplateConfig_ExitOnRetryFailure(t *testing.T) {
|
||||
//----------------------------------------------------
|
||||
// Start the server and agent
|
||||
//----------------------------------------------------
|
||||
logger := logging.NewVaultLogger(hclog.Trace)
|
||||
cluster := vault.NewTestCluster(t,
|
||||
&vault.CoreConfig{
|
||||
// Logger: logger,
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"approle": credAppRole.Factory,
|
||||
},
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"kv": logicalKv.Factory,
|
||||
},
|
||||
},
|
||||
&vault.TestClusterOptions{
|
||||
NumCores: 1,
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||
serverClient := cluster.Cores[0].Client
|
||||
|
||||
// Unset the environment variable so that agent picks up the right test
|
||||
// cluster address
|
||||
defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress))
|
||||
os.Unsetenv(api.EnvVaultAddress)
|
||||
|
||||
autoAuthConfig, cleanup := prepAgentApproleKV(t, serverClient)
|
||||
defer cleanup()
|
||||
|
||||
err := serverClient.Sys().TuneMount("secret", api.MountConfigInput{
|
||||
Options: map[string]string{
|
||||
"version": "2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = serverClient.Logical().Write("secret/data/otherapp", map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"username": "barstuff",
|
||||
"password": "zap",
|
||||
"cert": "something",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// make a temp directory to hold renders. Each test will create a temp dir
|
||||
// inside this one
|
||||
tmpDirRoot, err := ioutil.TempDir("", "agent-test-renders")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDirRoot)
|
||||
|
||||
// Note that missing key is different from a non-existent secret. A missing
|
||||
// key (2xx response with missing keys in the response map) can still yield
|
||||
// a successful render unless error_on_missing_key is specified, whereas a
|
||||
// missing secret (4xx response) always results in an error.
|
||||
missingKeyTemplateContent := `{{- with secret "secret/otherapp"}}{"secret": "other",
|
||||
{{- if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{- end }}}
|
||||
{{- end }}`
|
||||
missingKeyTemplateRender := `{"secret": "other",}`
|
||||
|
||||
badTemplateContent := `{{- with secret "secret/non-existent"}}{"secret": "other",
|
||||
{{- if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{- end }}}
|
||||
{{- end }}`
|
||||
|
||||
testCases := map[string]struct {
|
||||
exitOnRetryFailure *bool
|
||||
templateContents string
|
||||
expectTemplateRender string
|
||||
templateErrorOnMissingKey bool
|
||||
expectError bool
|
||||
expectExitFromError bool
|
||||
}{
|
||||
"true, no template error": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(true),
|
||||
templateContents: templateContents(0),
|
||||
expectTemplateRender: templateRendered(0),
|
||||
templateErrorOnMissingKey: false,
|
||||
expectError: false,
|
||||
expectExitFromError: false,
|
||||
},
|
||||
"true, with non-existent secret": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(true),
|
||||
templateContents: badTemplateContent,
|
||||
expectTemplateRender: "",
|
||||
templateErrorOnMissingKey: false,
|
||||
expectError: true,
|
||||
expectExitFromError: true,
|
||||
},
|
||||
"true, with missing key": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(true),
|
||||
templateContents: missingKeyTemplateContent,
|
||||
expectTemplateRender: missingKeyTemplateRender,
|
||||
templateErrorOnMissingKey: false,
|
||||
expectError: false,
|
||||
expectExitFromError: false,
|
||||
},
|
||||
"true, with missing key, with error_on_missing_key": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(true),
|
||||
templateContents: missingKeyTemplateContent,
|
||||
expectTemplateRender: "",
|
||||
templateErrorOnMissingKey: true,
|
||||
expectError: true,
|
||||
expectExitFromError: true,
|
||||
},
|
||||
"false, no template error": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(false),
|
||||
templateContents: templateContents(0),
|
||||
expectTemplateRender: templateRendered(0),
|
||||
templateErrorOnMissingKey: false,
|
||||
expectError: false,
|
||||
expectExitFromError: false,
|
||||
},
|
||||
"false, with non-existent secret": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(false),
|
||||
templateContents: badTemplateContent,
|
||||
expectTemplateRender: "",
|
||||
templateErrorOnMissingKey: false,
|
||||
expectError: true,
|
||||
expectExitFromError: false,
|
||||
},
|
||||
"false, with missing key": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(false),
|
||||
templateContents: missingKeyTemplateContent,
|
||||
expectTemplateRender: missingKeyTemplateRender,
|
||||
templateErrorOnMissingKey: false,
|
||||
expectError: false,
|
||||
expectExitFromError: false,
|
||||
},
|
||||
"false, with missing key, with error_on_missing_key": {
|
||||
exitOnRetryFailure: pointerutil.BoolPtr(false),
|
||||
templateContents: missingKeyTemplateContent,
|
||||
expectTemplateRender: missingKeyTemplateRender,
|
||||
templateErrorOnMissingKey: true,
|
||||
expectError: true,
|
||||
expectExitFromError: false,
|
||||
},
|
||||
"missing": {
|
||||
exitOnRetryFailure: nil,
|
||||
templateContents: templateContents(0),
|
||||
expectTemplateRender: templateRendered(0),
|
||||
templateErrorOnMissingKey: false,
|
||||
expectError: false,
|
||||
expectExitFromError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for tcName, tc := range testCases {
|
||||
t.Run(tcName, func(t *testing.T) {
|
||||
// create temp dir for this test run
|
||||
tmpDir, err := ioutil.TempDir(tmpDirRoot, tcName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
listenAddr := "127.0.0.1:18123"
|
||||
listenConfig := fmt.Sprintf(`
|
||||
listener "tcp" {
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
}
|
||||
`, listenAddr)
|
||||
|
||||
var exitOnRetryFailure string
|
||||
if tc.exitOnRetryFailure != nil {
|
||||
exitOnRetryFailure = fmt.Sprintf("exit_on_retry_failure = %t", *tc.exitOnRetryFailure)
|
||||
}
|
||||
templateConfig := fmt.Sprintf(`
|
||||
template_config = {
|
||||
%s
|
||||
}
|
||||
`, exitOnRetryFailure)
|
||||
|
||||
template := fmt.Sprintf(`
|
||||
template {
|
||||
contents = <<EOF
|
||||
%s
|
||||
EOF
|
||||
destination = "%s/render_0.json"
|
||||
error_on_missing_key = %t
|
||||
}
|
||||
`, tc.templateContents, tmpDir, tc.templateErrorOnMissingKey)
|
||||
|
||||
config := fmt.Sprintf(`
|
||||
# auto-auth stanza
|
||||
%s
|
||||
|
||||
vault {
|
||||
address = "%s"
|
||||
tls_skip_verify = true
|
||||
retry {
|
||||
num_retries = 3
|
||||
}
|
||||
}
|
||||
|
||||
# listener stanza
|
||||
%s
|
||||
|
||||
# template_config stanza
|
||||
%s
|
||||
|
||||
# template stanza
|
||||
%s
|
||||
`, autoAuthConfig, serverClient.Address(), listenConfig, templateConfig, template)
|
||||
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
// Start the agent
|
||||
ui, cmd := testAgentCommand(t, logger)
|
||||
cmd.startedCh = make(chan struct{})
|
||||
|
||||
// Channel to let verify() know to stop early if agent
|
||||
// has exited
|
||||
cmdRunDoneCh := make(chan struct{})
|
||||
var exitedEarly bool
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
var code int
|
||||
go func() {
|
||||
code = cmd.Run([]string{"-config", configPath})
|
||||
close(cmdRunDoneCh)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
verify := func() error {
|
||||
t.Helper()
|
||||
// We need to poll for a bit to give Agent time to render the
|
||||
// templates. Without this this, the test will attempt to read
|
||||
// the temp dir before Agent has had time to render and will
|
||||
// likely fail the test
|
||||
tick := time.Tick(1 * time.Second)
|
||||
timeout := time.After(15 * time.Second)
|
||||
var err error
|
||||
for {
|
||||
select {
|
||||
case <-cmdRunDoneCh:
|
||||
exitedEarly = true
|
||||
return nil
|
||||
case <-timeout:
|
||||
return fmt.Errorf("timed out waiting for templates to render, last error: %w", err)
|
||||
case <-tick:
|
||||
}
|
||||
// Check for files rendered in the directory and break
|
||||
// early for shutdown if we do have all the files
|
||||
// rendered
|
||||
|
||||
//----------------------------------------------------
|
||||
// Perform the tests
|
||||
//----------------------------------------------------
|
||||
|
||||
if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != 1 {
|
||||
err = fmt.Errorf("expected 1 template, got (%d)", numFiles)
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := filepath.Join(tmpDir, "render_0.json")
|
||||
var c []byte
|
||||
c, err = ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(string(c)) != tc.expectTemplateRender {
|
||||
err = fmt.Errorf("expected='%s', got='%s'", tc.expectTemplateRender, strings.TrimSpace(string(c)))
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err = verify()
|
||||
close(cmd.ShutdownCh)
|
||||
wg.Wait()
|
||||
|
||||
switch {
|
||||
case (code != 0 || err != nil) && tc.expectError:
|
||||
if exitedEarly != tc.expectExitFromError {
|
||||
t.Fatalf("expected program exit due to error to be '%t', got '%t'", tc.expectExitFromError, exitedEarly)
|
||||
}
|
||||
case code == 0 && err == nil && !tc.expectError:
|
||||
if exitedEarly {
|
||||
t.Fatalf("did not expect program to exit before verify completes")
|
||||
}
|
||||
default:
|
||||
if code != 0 {
|
||||
t.Logf("output from agent:\n%s", ui.OutputWriter.String())
|
||||
t.Logf("error from agent:\n%s", ui.ErrorWriter.String())
|
||||
}
|
||||
t.Fatalf("expectError=%v error=%v code=%d", tc.expectError, err, code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,8 @@ These are the currently-available general configuration option:
|
|||
|
||||
- `template` <code>([template][template]: <optional\>)</code> - Specifies options used for templating Vault secrets to files.
|
||||
|
||||
- `template_config` <code>([template_config][template-config]: <optional\>)</code> - Specifies templating engine behavior.
|
||||
|
||||
### vault Stanza
|
||||
|
||||
There can at most be one top level `vault` block and it has the following
|
||||
|
@ -94,12 +96,17 @@ configuration entries:
|
|||
|
||||
#### retry Stanza
|
||||
|
||||
The `vault` stanza may contain a `retry` stanza that controls how failing
|
||||
Vault requests are handled, whether these requests are issued in order to render
|
||||
The `vault` stanza may contain a `retry` stanza that controls how failing Vault
|
||||
requests are handled, whether these requests are issued in order to render
|
||||
templates, or are proxied requests coming from the proxy cache subsystem.
|
||||
Auto-auth, however, has its own notion of retrying and is not affected by this
|
||||
section.
|
||||
|
||||
For requests from the templating engine, Agent will reset its retry counter and
|
||||
perform retries again once all retries are exhausted. This means that templating
|
||||
will retry on failures indefinitely unless `exit_after_retry_failure` from the
|
||||
[`template_config`][template-config] stanza is set to `true`.
|
||||
|
||||
Here are the options for the `retry` stanza:
|
||||
|
||||
- `num_retries` `(int: 12)` - Specify how many times a failing request will
|
||||
|
@ -114,7 +121,13 @@ result codes: any 50x code except 501 ("not implemented"), as well as 412
|
|||
stale read due to eventual consistency. Requests coming from the template
|
||||
subsystem are retried regardless of the failure.
|
||||
|
||||
Second, the backoff algorithm used to set the time between retries differs for
|
||||
Second, templating retries may be performed by both the templating engine _and_
|
||||
the cache proxy if Agent [persistent
|
||||
cache][persistent-cache] is enabled. This is due to the
|
||||
fact that templating requests go through the cache proxy when persistence is
|
||||
enabled.
|
||||
|
||||
Third, the backoff algorithm used to set the time between retries differs for
|
||||
the template and cache subsystems. This is a technical limitation we hope
|
||||
to address in the future.
|
||||
|
||||
|
@ -198,7 +211,9 @@ template {
|
|||
[vault]: /docs/agent#vault-stanza
|
||||
[autoauth]: /docs/agent/autoauth
|
||||
[caching]: /docs/agent/caching
|
||||
[persistent-cache]: /docs/agent/caching/persistent-caches
|
||||
[template]: /docs/agent/template
|
||||
[template-config]: /docs/agent/template-config
|
||||
[listener]: /docs/agent#listener-stanza
|
||||
[listener_main]: /docs/configuration/listener/tcp
|
||||
[winsvc]: /docs/agent/winsvc
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: Vault Agent Template Config
|
||||
description: |-
|
||||
Vault Agent's Template Config to set Templating Engine behavior
|
||||
---
|
||||
|
||||
# Vault Agent Template Config
|
||||
|
||||
Template Config configures Vault Agent behavior common to all `template` stanzas.
|
||||
|
||||
For template-specific rendering configuration, refer to the parameters within the
|
||||
[`template`](/docs/agent/template) stanza.
|
||||
|
||||
## Functionality
|
||||
|
||||
The `template_config` stanza configures overall default behavior for the
|
||||
templating engine. Note that `template_config` can only be defined once, and is
|
||||
different from the `template` stanza. Unlike `template` which focuses on where
|
||||
and how a specific secret is rendered, `template_config` contains parameters
|
||||
affecting how the templating engine as a whole behaves and its interaction with
|
||||
the rest of Agent. This includes, but is not limited to, program exit behavior.
|
||||
Other parameters that apply to the templating engine as a whole may be added
|
||||
over time.
|
||||
|
||||
### Interaction between `exit_on_retry_failure` and `error_on_missing_key`
|
||||
|
||||
The parameter
|
||||
[`error_on_missing_key`](/docs/agent/template#error_on_missing_key) can be
|
||||
specified within the `template` stanza which determines if a template should
|
||||
error when a key is missing in the secret. When `error_on_missing_key` is not
|
||||
specified or set to `false` and the key to render is not in the secret's
|
||||
response, the templating engine will ignore it (or render `"<no value>"`) and
|
||||
continue on with its rendering.
|
||||
|
||||
If the desire is to have Agent fail and exit on a missing key, both
|
||||
`template.error_on_missing_key` and `template_config.exit_on_retry_failure` must
|
||||
be set to true. Otherwise, the templating engine will error and render to its
|
||||
destination, but agent will not exit and will retry until the key exists or until
|
||||
the process is terminated.
|
||||
|
||||
Note that a missing key from a secret's response is different from a missing or
|
||||
non-existent secret. The templating engine will always error if a secret is
|
||||
missing, but will only error for a missing key if `error_on_missing_key` is set.
|
||||
Whether Vault Agent will exit when the templating engine errors depends on the
|
||||
value of `exit_on_retry_failure`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The top level `template_config` block has the following configuration entries:
|
||||
|
||||
- `exit_on_retry_failure` `(bool: false)` - This option configures Vault Agent
|
||||
to exit after it has exhausted its number of template retry attempts due to
|
||||
failures.
|
|
@ -13,17 +13,20 @@ description: >-
|
|||
Vault Agent's Template functionality allows Vault secrets to be rendered to files
|
||||
using [Consul Template markup](https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md).
|
||||
|
||||
For globally applicable templating engine configuration, refer to the parameters
|
||||
within the [`template_config`](/docs/agent/template-config) stanza.
|
||||
|
||||
## Functionality
|
||||
|
||||
The `template` stanza configures the templating engine in the Vault agent for rendering
|
||||
secrets to files using Consul Template markup language. Multiple `template` stanzas
|
||||
can be defined to render multiple files.
|
||||
The `template` stanza configures the Vault agent for rendering secrets to files
|
||||
using Consul Template markup language. Multiple `template` stanzas can be
|
||||
defined to render multiple files.
|
||||
|
||||
When the agent is started with templating enabled, it will attempt to acquire a
|
||||
Vault token using the configured Method. On failure, it will back off for a short
|
||||
while (including some randomness to help prevent thundering herd scenarios) and
|
||||
retry. On success, secrets defined in the templates will be retrieved from Vault and
|
||||
rendered locally.
|
||||
Vault token using the configured auto-auth Method. On failure, it will back off
|
||||
for a short while (including some randomness to help prevent thundering herd
|
||||
scenarios) and retry. On success, secrets defined in the templates will be
|
||||
retrieved from Vault and rendered locally.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
|
@ -825,6 +825,10 @@
|
|||
"title": "Templates",
|
||||
"path": "agent/template"
|
||||
},
|
||||
{
|
||||
"title": "Template Config",
|
||||
"path": "agent/template-config"
|
||||
},
|
||||
{
|
||||
"title": "Windows service",
|
||||
"path": "agent/winsvc"
|
||||
|
|
Loading…
Reference in New Issue