Merge pull request #2947 from hashicorp/f-vault-grace

Allow template to set Vault grace
This commit is contained in:
Alex Dadgar 2017-08-07 16:29:53 -07:00 committed by GitHub
commit 79d25b7db9
38 changed files with 1115 additions and 307 deletions

View File

@ -275,6 +275,7 @@ func TestJobs_Canonicalize(t *testing.T) {
EmbeddedTmpl: helper.StringToPtr("FOO=bar\n"),
DestPath: helper.StringToPtr("local/file.env"),
Envvars: helper.BoolToPtr(true),
VaultGrace: helper.TimeToPtr(3 * time.Second),
},
},
},
@ -389,6 +390,7 @@ func TestJobs_Canonicalize(t *testing.T) {
LeftDelim: helper.StringToPtr("{{"),
RightDelim: helper.StringToPtr("}}"),
Envvars: helper.BoolToPtr(false),
VaultGrace: helper.TimeToPtr(5 * time.Minute),
},
{
SourcePath: helper.StringToPtr(""),
@ -401,6 +403,7 @@ func TestJobs_Canonicalize(t *testing.T) {
LeftDelim: helper.StringToPtr("{{"),
RightDelim: helper.StringToPtr("}}"),
Envvars: helper.BoolToPtr(true),
VaultGrace: helper.TimeToPtr(3 * time.Second),
},
},
},

View File

@ -364,6 +364,7 @@ type Template struct {
LeftDelim *string `mapstructure:"left_delimiter"`
RightDelim *string `mapstructure:"right_delimiter"`
Envvars *bool `mapstructure:"env"`
VaultGrace *time.Duration `mapstructure:"vault_grace"`
}
func (tmpl *Template) Canonicalize() {
@ -404,6 +405,9 @@ func (tmpl *Template) Canonicalize() {
if tmpl.Envvars == nil {
tmpl.Envvars = helper.BoolToPtr(false)
}
if tmpl.VaultGrace == nil {
tmpl.VaultGrace = helper.TimeToPtr(5 * time.Minute)
}
}
type Vault struct {

View File

@ -17,6 +17,7 @@ import (
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver/env"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/structs"
)
@ -341,11 +342,6 @@ func templateRunner(tmpls []*structs.Template, config *config.Config,
return nil, nil, nil
}
runnerConfig, err := runnerConfig(config, vaultToken)
if err != nil {
return nil, nil, err
}
// Parse the templates
allowAbs := config.ReadBoolDefault(hostSrcOption, true)
ctmplMapping, err := parseTemplateConfigs(tmpls, taskDir, taskEnv, allowAbs)
@ -353,13 +349,11 @@ func templateRunner(tmpls []*structs.Template, config *config.Config,
return nil, nil, err
}
// Set the config
flat := ctconf.TemplateConfigs(make([]*ctconf.TemplateConfig, 0, len(ctmplMapping)))
for ctmpl := range ctmplMapping {
local := ctmpl
flat = append(flat, &local)
// Create the runner configuration.
runnerConfig, err := newRunnerConfig(config, vaultToken, ctmplMapping)
if err != nil {
return nil, nil, err
}
runnerConfig.Templates = &flat
runner, err := manager.NewRunner(runnerConfig, false, false)
if err != nil {
@ -429,12 +423,33 @@ func parseTemplateConfigs(tmpls []*structs.Template, taskDir string,
return ctmpls, nil
}
// runnerConfig returns a consul-template runner configuration, setting the
// Vault and Consul configurations based on the clients configs.
func runnerConfig(config *config.Config, vaultToken string) (*ctconf.Config, error) {
// newRunnerConfig returns a consul-template runner configuration, setting the
// Vault and Consul configurations based on the clients configs. The parameters
// are the client config, Vault token if set and the mapping of consul-templates
// to Nomad templates.
func newRunnerConfig(config *config.Config, vaultToken string,
templateMapping map[ctconf.TemplateConfig]*structs.Template) (*ctconf.Config, error) {
conf := ctconf.DefaultConfig()
t, f := true, false
// Gather the consul-template tempates
flat := ctconf.TemplateConfigs(make([]*ctconf.TemplateConfig, 0, len(templateMapping)))
for ctmpl := range templateMapping {
local := ctmpl
flat = append(flat, &local)
}
conf.Templates = &flat
// Go through the templates and determine the minimum Vault grace
vaultGrace := time.Duration(-1)
for _, tmpl := range templateMapping {
// Initial condition
if vaultGrace < 0 {
vaultGrace = tmpl.VaultGrace
} else if tmpl.VaultGrace < vaultGrace {
vaultGrace = tmpl.VaultGrace
}
}
// Force faster retries
if testRetryRate != 0 {
@ -450,7 +465,7 @@ func runnerConfig(config *config.Config, vaultToken string) (*ctconf.Config, err
if config.ConsulConfig.EnableSSL != nil && *config.ConsulConfig.EnableSSL {
verify := config.ConsulConfig.VerifySSL != nil && *config.ConsulConfig.VerifySSL
conf.Consul.SSL = &ctconf.SSLConfig{
Enabled: &t,
Enabled: helper.BoolToPtr(true),
Verify: &verify,
Cert: &config.ConsulConfig.CertFile,
Key: &config.ConsulConfig.KeyFile,
@ -465,7 +480,7 @@ func runnerConfig(config *config.Config, vaultToken string) (*ctconf.Config, err
}
conf.Consul.Auth = &ctconf.AuthConfig{
Enabled: &t,
Enabled: helper.BoolToPtr(true),
Username: &parts[0],
Password: &parts[1],
}
@ -475,17 +490,18 @@ func runnerConfig(config *config.Config, vaultToken string) (*ctconf.Config, err
// Setup the Vault config
// Always set these to ensure nothing is picked up from the environment
emptyStr := ""
conf.Vault.RenewToken = &f
conf.Vault.RenewToken = helper.BoolToPtr(false)
conf.Vault.Token = &emptyStr
if config.VaultConfig != nil && config.VaultConfig.IsEnabled() {
conf.Vault.Address = &config.VaultConfig.Addr
conf.Vault.Token = &vaultToken
conf.Vault.Grace = helper.TimeToPtr(vaultGrace)
if strings.HasPrefix(config.VaultConfig.Addr, "https") || config.VaultConfig.TLSCertFile != "" {
skipVerify := config.VaultConfig.TLSSkipVerify != nil && *config.VaultConfig.TLSSkipVerify
verify := !skipVerify
conf.Vault.SSL = &ctconf.SSLConfig{
Enabled: &t,
Enabled: helper.BoolToPtr(true),
Verify: &verify,
Cert: &config.VaultConfig.TLSCertFile,
Key: &config.VaultConfig.TLSKeyFile,
@ -495,8 +511,8 @@ func runnerConfig(config *config.Config, vaultToken string) (*ctconf.Config, err
}
} else {
conf.Vault.SSL = &ctconf.SSLConfig{
Enabled: &f,
Verify: &f,
Enabled: helper.BoolToPtr(false),
Verify: helper.BoolToPtr(false),
Cert: &emptyStr,
Key: &emptyStr,
CaCert: &emptyStr,

View File

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/nomad/nomad/structs"
sconfig "github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/testutil"
"github.com/stretchr/testify/assert"
)
const (
@ -1062,7 +1063,7 @@ func TestTaskTemplateManager_Config_ServerName(t *testing.T) {
Addr: "https://localhost/",
TLSServerName: "notlocalhost",
}
ctconf, err := runnerConfig(c, "token")
ctconf, err := newRunnerConfig(c, "token", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -1071,3 +1072,41 @@ func TestTaskTemplateManager_Config_ServerName(t *testing.T) {
t.Fatalf("expected %q but found %q", c.VaultConfig.TLSServerName, *ctconf.Vault.SSL.ServerName)
}
}
// TestTaskTemplateManager_Config_VaultGrace asserts the vault_grace setting is
// propogated to consul-template's configuration.
func TestTaskTemplateManager_Config_VaultGrace(t *testing.T) {
t.Parallel()
assert := assert.New(t)
c := config.DefaultConfig()
c.VaultConfig = &sconfig.VaultConfig{
Enabled: helper.BoolToPtr(true),
Addr: "https://localhost/",
TLSServerName: "notlocalhost",
}
// Make a template that will render immediately
templates := []*structs.Template{
{
EmbeddedTmpl: "bar",
DestPath: "foo",
ChangeMode: structs.TemplateChangeModeNoop,
VaultGrace: 10 * time.Second,
},
{
EmbeddedTmpl: "baz",
DestPath: "bam",
ChangeMode: structs.TemplateChangeModeNoop,
VaultGrace: 100 * time.Second,
},
}
taskEnv := env.NewTaskEnv(nil, nil)
ctmplMapping, err := parseTemplateConfigs(templates, "/fake/dir", taskEnv, false)
assert.Nil(err, "Parsing Templates")
ctconf, err := newRunnerConfig(c, "token", ctmplMapping)
assert.Nil(err, "Building Runner Config")
assert.NotNil(ctconf.Vault.Grace, "Vault Grace Pointer")
assert.Equal(10*time.Second, *ctconf.Vault.Grace, "Vault Grace Value")
}

View File

@ -781,6 +781,7 @@ func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) {
LeftDelim: *template.LeftDelim,
RightDelim: *template.RightDelim,
Envvars: *template.Envvars,
VaultGrace: *template.VaultGrace,
}
}
}

View File

@ -1170,6 +1170,8 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
Perms: helper.StringToPtr("666"),
LeftDelim: helper.StringToPtr("abc"),
RightDelim: helper.StringToPtr("def"),
Envvars: helper.BoolToPtr(true),
VaultGrace: helper.TimeToPtr(3 * time.Second),
},
},
DispatchPayload: &api.DispatchPayloadConfig{
@ -1357,6 +1359,8 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
Perms: "666",
LeftDelim: "abc",
RightDelim: "def",
Envvars: true,
VaultGrace: 3 * time.Second,
},
},
DispatchPayload: &structs.DispatchPayloadConfig{

View File

@ -863,6 +863,7 @@ func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error {
"source",
"splay",
"env",
"vault_grace",
}
if err := checkHCLKeys(o.Val, valid); err != nil {
return err

View File

@ -183,6 +183,7 @@ func TestParse(t *testing.T) {
Splay: helper.TimeToPtr(10 * time.Second),
Perms: helper.StringToPtr("0644"),
Envvars: helper.BoolToPtr(true),
VaultGrace: helper.TimeToPtr(33 * time.Second),
},
{
SourcePath: helper.StringToPtr("bar"),

View File

@ -158,6 +158,7 @@ job "binstore-storagelocker" {
change_signal = "foo"
splay = "10s"
env = true
vault_grace = "33s"
}
template {

View File

@ -3314,6 +3314,11 @@ type Template struct {
// Empty lines and lines starting with # will be ignored, but to avoid
// escaping issues #s within lines will not be treated as comments.
Envvars bool
// VaultGrace is the grace duration between lease renewal and reacquiring a
// secret. If the lease of a secret is less than the grace, a new secret is
// acquired.
VaultGrace time.Duration
}
// DefaultTemplate returns a default template.
@ -3387,6 +3392,10 @@ func (t *Template) Validate() error {
}
}
if t.VaultGrace.Nanoseconds() < 0 {
multierror.Append(&mErr, fmt.Errorf("Vault grace must be greater than zero: %v < 0", t.VaultGrace))
}
return mErr.ErrorOrNil()
}

View File

@ -230,7 +230,7 @@ func Parse(s string) (*Config, error) {
})
// FlattenFlatten keys belonging to the templates. We cannot do this above
// because it is an array of tmeplates.
// because it is an array of templates.
if templates, ok := parsed["template"].([]map[string]interface{}); ok {
for _, template := range templates {
flattenKeys(template, []string{

View File

@ -11,7 +11,7 @@ const (
// DefaultVaultGrace is the default grace period before which to read a new
// secret from Vault. If a lease is due to expire in 5 minutes, Consul
// Template will read a new secret at that time minus this value.
DefaultVaultGrace = 15 * time.Second
DefaultVaultGrace = 5 * time.Minute
// DefaultVaultRenewToken is the default value for if the Vault token should
// be renewed.

View File

@ -9,3 +9,5 @@ var ErrStopped = errors.New("dependency stopped")
// ErrContinue is a special error which says to continue (retry) on error.
var ErrContinue = errors.New("dependency continue")
var ErrLeaseExpired = errors.New("lease expired or is not renewable")

View File

@ -1,15 +1,23 @@
package dependency
import "time"
import (
"log"
"math/rand"
"time"
"github.com/hashicorp/vault/api"
)
var (
// VaultDefaultLeaseDuration is the default lease duration in seconds.
VaultDefaultLeaseDuration = 5 * time.Minute
)
// Secret is a vault secret.
// Secret is the structure returned for every secret within Vault.
type Secret struct {
RequestID string
// The request ID that generated this response
RequestID string
LeaseID string
LeaseDuration int
Renewable bool
@ -17,23 +25,171 @@ type Secret struct {
// Data is the actual contents of the secret. The format of the data
// is arbitrary and up to the secret backend.
Data map[string]interface{}
// Warnings contains any warnings related to the operation. These
// are not issues that caused the command to fail, but that the
// client should be aware of.
Warnings []string
// Auth, if non-nil, means that there was authentication information
// attached to this response.
Auth *SecretAuth
// WrapInfo, if non-nil, means that the initial response was wrapped in the
// cubbyhole of the given token (which has a TTL of the given number of
// seconds)
WrapInfo *SecretWrapInfo
}
// leaseDurationOrDefault returns a value or the default lease duration.
func leaseDurationOrDefault(d int) int {
if d == 0 {
return int(VaultDefaultLeaseDuration.Nanoseconds() / 1000000000)
}
return d
// SecretAuth is the structure containing auth information if we have it.
type SecretAuth struct {
ClientToken string
Accessor string
Policies []string
Metadata map[string]string
LeaseDuration int
Renewable bool
}
// vaultRenewDuration accepts a given renew duration (lease duration) and
// returns the cooresponding time.Duration. If the duration is 0 (not provided),
// this falls back to the VaultDefaultLeaseDuration.
func vaultRenewDuration(d int) time.Duration {
dur := time.Duration(d/2.0) * time.Second
if dur == 0 {
dur = VaultDefaultLeaseDuration
}
return dur
// SecretWrapInfo contains wrapping information if we have it. If what is
// contained is an authentication token, the accessor for the token will be
// available in WrappedAccessor.
type SecretWrapInfo struct {
Token string
TTL int
CreationTime time.Time
WrappedAccessor string
}
// vaultRenewDuration accepts a secret and returns the recommended amount of
// time to sleep.
func vaultRenewDuration(s *Secret) time.Duration {
// Handle whether this is an auth or a regular secret.
base := s.LeaseDuration
if s.Auth != nil && s.Auth.LeaseDuration > 0 {
base = s.Auth.LeaseDuration
}
// Ensure we have a lease duration, since sometimes this can be zero.
if base <= 0 {
base = int(VaultDefaultLeaseDuration.Seconds())
}
// Convert to float seconds.
sleep := float64(time.Duration(base) * time.Second)
// Renew at 1/3 the remaining lease. This will give us an opportunity to retry
// at least one more time should the first renewal fail.
sleep = sleep / 3.0
// Use a randomness so many clients do not hit Vault simultaneously.
sleep = sleep * (rand.Float64() + 1) / 2.0
return time.Duration(sleep)
}
// printVaultWarnings prints warnings for a given dependency.
func printVaultWarnings(d Dependency, warnings []string) {
for _, w := range warnings {
log.Printf("[WARN] %s: %s", d, w)
}
}
// vaultSecretRenewable determines if the given secret is renewable.
func vaultSecretRenewable(s *Secret) bool {
if s.Auth != nil {
return s.Auth.Renewable
}
return s.Renewable
}
// transformSecret transforms an api secret into our secret. This does not deep
// copy underlying deep data structures, so it's not safe to modify the vault
// secret as that may modify the data in the transformed secret.
func transformSecret(theirs *api.Secret) *Secret {
var ours Secret
updateSecret(&ours, theirs)
return &ours
}
// updateSecret updates our secret with the new data from the api, careful to
// not overwrite missing data. Renewals don't include the original secret, and
// we don't want to delete that data accidentially.
func updateSecret(ours *Secret, theirs *api.Secret) {
if theirs.RequestID != "" {
ours.RequestID = theirs.RequestID
}
if theirs.LeaseID != "" {
ours.LeaseID = theirs.LeaseID
}
if theirs.LeaseDuration != 0 {
ours.LeaseDuration = theirs.LeaseDuration
}
if theirs.Renewable {
ours.Renewable = theirs.Renewable
}
if len(theirs.Data) != 0 {
ours.Data = theirs.Data
}
if len(theirs.Warnings) != 0 {
ours.Warnings = theirs.Warnings
}
if theirs.Auth != nil {
if ours.Auth == nil {
ours.Auth = &SecretAuth{}
}
if theirs.Auth.ClientToken != "" {
ours.Auth.ClientToken = theirs.Auth.ClientToken
}
if theirs.Auth.Accessor != "" {
ours.Auth.Accessor = theirs.Auth.Accessor
}
if len(theirs.Auth.Policies) != 0 {
ours.Auth.Policies = theirs.Auth.Policies
}
if len(theirs.Auth.Metadata) != 0 {
ours.Auth.Metadata = theirs.Auth.Metadata
}
if theirs.Auth.LeaseDuration != 0 {
ours.Auth.LeaseDuration = theirs.Auth.LeaseDuration
}
if theirs.Auth.Renewable {
ours.Auth.Renewable = theirs.Auth.Renewable
}
}
if theirs.WrapInfo != nil {
if ours.WrapInfo == nil {
ours.WrapInfo = &SecretWrapInfo{}
}
if theirs.WrapInfo.Token != "" {
ours.WrapInfo.Token = theirs.WrapInfo.Token
}
if theirs.WrapInfo.TTL != 0 {
ours.WrapInfo.TTL = theirs.WrapInfo.TTL
}
if !theirs.WrapInfo.CreationTime.IsZero() {
ours.WrapInfo.CreationTime = theirs.WrapInfo.CreationTime
}
if theirs.WrapInfo.WrappedAccessor != "" {
ours.WrapInfo.WrappedAccessor = theirs.WrapInfo.WrappedAccessor
}
}
}

View File

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/hashicorp/vault/api"
"github.com/pkg/errors"
)
@ -21,6 +22,9 @@ type VaultReadQuery struct {
path string
secret *Secret
// vaultSecret is the actual Vault secret which we are renewing
vaultSecret *api.Secret
}
// NewVaultReadQuery creates a new datacenter dependency.
@ -47,71 +51,68 @@ func (d *VaultReadQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interfac
opts = opts.Merge(&QueryOptions{})
// If this is not the first query and we have a lease duration, sleep until we
// try to renew.
if opts.WaitIndex != 0 && d.secret != nil && d.secret.LeaseDuration != 0 {
dur := vaultRenewDuration(d.secret.LeaseDuration)
if d.secret != nil {
if vaultSecretRenewable(d.secret) {
log.Printf("[TRACE] %s: starting renewer", d)
log.Printf("[TRACE] %s: long polling for %s", d, dur)
renewer, err := clients.Vault().NewRenewer(&api.RenewerInput{
Grace: opts.VaultGrace,
Secret: d.vaultSecret,
})
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
}
go renewer.Renew()
defer renewer.Stop()
select {
case <-d.stopCh:
return nil, nil, ErrStopped
case <-time.After(dur):
RENEW:
for {
select {
case err := <-renewer.DoneCh():
if err != nil {
log.Printf("[WARN] %s: failed to renew: %s", d, err)
}
log.Printf("[WARN] %s: renewer returned (maybe the lease expired)", d)
break RENEW
case renewal := <-renewer.RenewCh():
log.Printf("[TRACE] %s: successfully renewed", d)
printVaultWarnings(d, renewal.Secret.Warnings)
updateSecret(d.secret, renewal.Secret)
case <-d.stopCh:
return nil, nil, ErrStopped
}
}
} else {
// The secret isn't renewable, probably the generic secret backend.
dur := vaultRenewDuration(d.secret)
if dur < opts.VaultGrace {
log.Printf("[TRACE] %s: remaining lease %s is less than grace, skipping sleep", d, dur)
} else {
log.Printf("[TRACE] %s: secret is not renewable, sleeping for %s", d, dur)
select {
case <-time.After(dur):
// The lease is almost expired, it's time to request a new one.
case <-d.stopCh:
return nil, nil, ErrStopped
}
}
}
}
// Attempt to renew the secret. If we do not have a secret or if that secret
// is not renewable, we will attempt a (re-)read later.
if d.secret != nil && d.secret.LeaseID != "" && d.secret.Renewable {
log.Printf("[TRACE] %s: PUT %s", d, &url.URL{
Path: "/v1/sys/renew/" + d.secret.LeaseID,
RawQuery: opts.String(),
})
renewal, err := clients.Vault().Sys().Renew(d.secret.LeaseID, 0)
if err == nil {
log.Printf("[TRACE] %s: successfully renewed %s", d, d.secret.LeaseID)
// Print any warnings
d.printWarnings(renewal.Warnings)
secret := &Secret{
RequestID: renewal.RequestID,
LeaseID: renewal.LeaseID,
LeaseDuration: d.secret.LeaseDuration,
Renewable: renewal.Renewable,
Data: d.secret.Data,
}
// For some older versions of Vault, the renewal did not include the
// remaining lease duration, so just use the original lease duration,
// because it's the best we can do.
if renewal.LeaseDuration != 0 {
secret.LeaseDuration = renewal.LeaseDuration
}
d.secret = secret
// If the remaining time on the lease is less than or equal to our
// configured grace period, generate a new credential now. This will help
// minimize downtime, since Vault will revoke credentials immediately
// when their maximum TTL expires.
remaining := time.Duration(d.secret.LeaseDuration) * time.Second
if remaining <= opts.VaultGrace {
log.Printf("[DEBUG] %s: remaining lease (%s) < grace (%s), acquiring new",
d, remaining, opts.VaultGrace)
return d.readSecret(clients, opts)
}
return respWithMetadata(secret)
}
// The renewal failed for some reason.
log.Printf("[WARN] %s: failed to renew %s: %s", d, d.secret.LeaseID, err)
// We don't have a secret, or the prior renewal failed
vaultSecret, err := d.readSecret(clients, opts)
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
}
// If we got this far, we either didn't have a secret to renew, the secret was
// not renewable, or the renewal failed, so attempt a fresh read.
return d.readSecret(clients, opts)
// Print any warnings
printVaultWarnings(d, vaultSecret.Warnings)
// Create the cloned secret which will be exposed to the template.
d.vaultSecret = vaultSecret
d.secret = transformSecret(vaultSecret)
return respWithMetadata(d.secret)
}
// CanShare returns if this dependency is shareable.
@ -134,38 +135,17 @@ func (d *VaultReadQuery) Type() Type {
return TypeVault
}
func (d *VaultReadQuery) printWarnings(warnings []string) {
for _, w := range warnings {
log.Printf("[WARN] %s: %s", d, w)
}
}
func (d *VaultReadQuery) readSecret(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) {
func (d *VaultReadQuery) readSecret(clients *ClientSet, opts *QueryOptions) (*api.Secret, error) {
log.Printf("[TRACE] %s: GET %s", d, &url.URL{
Path: "/v1/" + d.path,
RawQuery: opts.String(),
})
vaultSecret, err := clients.Vault().Logical().Read(d.path)
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
return nil, errors.Wrap(err, d.String())
}
// The secret could be nil if it does not exist.
if vaultSecret == nil {
return nil, nil, fmt.Errorf("%s: no secret exists at %s", d, d.path)
return nil, fmt.Errorf("no secret exists at %s", d.path)
}
// Print any warnings.
d.printWarnings(vaultSecret.Warnings)
// Create our cloned secret.
secret := &Secret{
LeaseID: vaultSecret.LeaseID,
LeaseDuration: leaseDurationOrDefault(vaultSecret.LeaseDuration),
Renewable: vaultSecret.Renewable,
Data: vaultSecret.Data,
}
d.secret = secret
return respWithMetadata(secret)
return vaultSecret, nil
}

View File

@ -2,9 +2,9 @@ package dependency
import (
"log"
"net/url"
"time"
"github.com/hashicorp/vault/api"
"github.com/pkg/errors"
)
@ -15,16 +15,24 @@ var (
// VaultTokenQuery is the dependency to Vault for a secret
type VaultTokenQuery struct {
stopCh chan struct{}
leaseID string
leaseDuration int
stopCh chan struct{}
secret *Secret
vaultSecret *api.Secret
}
// NewVaultTokenQuery creates a new dependency.
func NewVaultTokenQuery() (*VaultTokenQuery, error) {
func NewVaultTokenQuery(token string) (*VaultTokenQuery, error) {
vaultSecret := &api.Secret{
Auth: &api.SecretAuth{
ClientToken: token,
Renewable: true,
LeaseDuration: 1,
},
}
return &VaultTokenQuery{
stopCh: make(chan struct{}, 1),
stopCh: make(chan struct{}, 1),
vaultSecret: vaultSecret,
secret: transformSecret(vaultSecret),
}, nil
}
@ -38,44 +46,53 @@ func (d *VaultTokenQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interfa
opts = opts.Merge(&QueryOptions{})
log.Printf("[TRACE] %s: GET %s", d, &url.URL{
Path: "/v1/auth/token/renew-self",
RawQuery: opts.String(),
})
if vaultSecretRenewable(d.secret) {
log.Printf("[TRACE] %s: starting renewer", d)
// If this is not the first query and we have a lease duration, sleep until we
// try to renew.
if opts.WaitIndex != 0 && d.leaseDuration != 0 {
dur := vaultRenewDuration(d.leaseDuration)
renewer, err := clients.Vault().NewRenewer(&api.RenewerInput{
Grace: opts.VaultGrace,
Secret: d.vaultSecret,
})
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
}
go renewer.Renew()
defer renewer.Stop()
log.Printf("[TRACE] %s: long polling for %s", d, dur)
select {
case <-d.stopCh:
return nil, nil, ErrStopped
case <-time.After(dur):
RENEW:
for {
select {
case err := <-renewer.DoneCh():
if err != nil {
log.Printf("[WARN] %s: failed to renew: %s", d, err)
}
log.Printf("[WARN] %s: renewer returned (maybe the lease expired)", d)
break RENEW
case renewal := <-renewer.RenewCh():
log.Printf("[TRACE] %s: successfully renewed", d)
printVaultWarnings(d, renewal.Secret.Warnings)
updateSecret(d.secret, renewal.Secret)
case <-d.stopCh:
return nil, nil, ErrStopped
}
}
}
token, err := clients.Vault().Auth().Token().RenewSelf(0)
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
// The secret isn't renewable, probably the generic secret backend.
dur := vaultRenewDuration(d.secret)
if dur < opts.VaultGrace {
log.Printf("[TRACE] %s: remaining lease %s is less than grace, skipping sleep", d, dur)
} else {
log.Printf("[TRACE] %s: token is not renewable, sleeping for %s", d, dur)
select {
case <-time.After(dur):
// The lease is almost expired, it's time to request a new one.
case <-d.stopCh:
return nil, nil, ErrStopped
}
}
// Create our cloned secret
secret := &Secret{
LeaseID: token.LeaseID,
LeaseDuration: token.Auth.LeaseDuration,
Renewable: token.Auth.Renewable,
Data: token.Data,
}
d.leaseID = secret.LeaseID
d.leaseDuration = secret.LeaseDuration
log.Printf("[DEBUG] %s: renewed token", d)
return respWithMetadata(secret)
return nil, nil, ErrLeaseExpired
}
// CanShare returns if this dependency is shareable.

View File

@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/hashicorp/vault/api"
"github.com/pkg/errors"
)
@ -26,6 +27,9 @@ type VaultWriteQuery struct {
data map[string]interface{}
dataHash string
secret *Secret
// vaultSecret is the actual Vault secret which we are renewing
vaultSecret *api.Secret
}
// NewVaultWriteQuery creates a new datacenter dependency.
@ -54,70 +58,68 @@ func (d *VaultWriteQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interfa
opts = opts.Merge(&QueryOptions{})
// If this is not the first query and we have a lease duration, sleep until we
// try to renew.
if opts.WaitIndex != 0 && d.secret != nil && d.secret.LeaseDuration != 0 {
dur := vaultRenewDuration(d.secret.LeaseDuration)
if d.secret != nil {
if vaultSecretRenewable(d.secret) {
log.Printf("[TRACE] %s: starting renewer", d)
log.Printf("[TRACE] %s: long polling for %s", d, dur)
renewer, err := clients.Vault().NewRenewer(&api.RenewerInput{
Grace: opts.VaultGrace,
Secret: d.vaultSecret,
})
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
}
go renewer.Renew()
defer renewer.Stop()
select {
case <-d.stopCh:
return nil, nil, ErrStopped
case <-time.After(dur):
RENEW:
for {
select {
case err := <-renewer.DoneCh():
if err != nil {
log.Printf("[WARN] %s: failed to renew: %s", d, err)
}
log.Printf("[WARN] %s: renewer returned (maybe the lease expired)", d)
break RENEW
case renewal := <-renewer.RenewCh():
log.Printf("[TRACE] %s: successfully renewed", d)
printVaultWarnings(d, renewal.Secret.Warnings)
updateSecret(d.secret, renewal.Secret)
case <-d.stopCh:
return nil, nil, ErrStopped
}
}
} else {
// The secret isn't renewable, probably the generic secret backend.
dur := vaultRenewDuration(d.secret)
if dur < opts.VaultGrace {
log.Printf("[TRACE] %s: remaining lease %s is less than grace, skipping sleep", d, dur)
} else {
log.Printf("[TRACE] %s: secret is not renewable, sleeping for %s", d, dur)
select {
case <-time.After(dur):
// The lease is almost expired, it's time to request a new one.
case <-d.stopCh:
return nil, nil, ErrStopped
}
}
}
}
// Attempt to renew the secret. If we do not have a secret or if that secret
// is not renewable, we will attempt a (re-)write later.
if d.secret != nil && d.secret.LeaseID != "" && d.secret.Renewable {
log.Printf("[TRACE] %s: PUT %s", d, &url.URL{
Path: "/v1/sys/renew/" + d.secret.LeaseID,
RawQuery: opts.String(),
})
renewal, err := clients.Vault().Sys().Renew(d.secret.LeaseID, 0)
if err == nil {
log.Printf("[TRACE] %s: successfully renewed %s", d, d.secret.LeaseID)
// Print any warnings
d.printWarnings(renewal.Warnings)
secret := &Secret{
RequestID: renewal.RequestID,
LeaseID: renewal.LeaseID,
Renewable: renewal.Renewable,
Data: d.secret.Data,
}
// For some older versions of Vault, the renewal did not include the
// remaining lease duration, so just use the original lease duration,
// because it's the best we can do.
if renewal.LeaseDuration != 0 {
secret.LeaseDuration = renewal.LeaseDuration
}
d.secret = secret
// If the remaining time on the lease is less than or equal to our
// configured grace period, generate a new credential now. This will help
// minimize downtime, since Vault will revoke credentials immediately
// when their maximum TTL expires.
remaining := time.Duration(d.secret.LeaseDuration) * time.Second
if remaining <= opts.VaultGrace {
log.Printf("[DEBUG] %s: remaining lease (%s) < grace (%s), acquiring new",
d, remaining, opts.VaultGrace)
return d.writeSecret(clients, opts)
}
return respWithMetadata(secret)
}
// The renewal failed for some reason.
log.Printf("[WARN] %s: failed to renew %s: %s", d, d.secret.LeaseID, err)
// We don't have a secret, or the prior renewal failed
vaultSecret, err := d.writeSecret(clients, opts)
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
}
// If we got this far, we either didn't have a secret to renew, the secret was
// not renewable, or the renewal failed, so attempt a fresh write.
return d.writeSecret(clients, opts)
// Print any warnings
printVaultWarnings(d, vaultSecret.Warnings)
// Create the cloned secret which will be exposed to the template.
d.vaultSecret = vaultSecret
d.secret = transformSecret(vaultSecret)
return respWithMetadata(d.secret)
}
// CanShare returns if this dependency is shareable.
@ -164,7 +166,7 @@ func (d *VaultWriteQuery) printWarnings(warnings []string) {
}
}
func (d *VaultWriteQuery) writeSecret(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) {
func (d *VaultWriteQuery) writeSecret(clients *ClientSet, opts *QueryOptions) (*api.Secret, error) {
log.Printf("[TRACE] %s: PUT %s", d, &url.URL{
Path: "/v1/" + d.path,
RawQuery: opts.String(),
@ -172,27 +174,11 @@ func (d *VaultWriteQuery) writeSecret(clients *ClientSet, opts *QueryOptions) (i
vaultSecret, err := clients.Vault().Logical().Write(d.path, d.data)
if err != nil {
return nil, nil, errors.Wrap(err, d.String())
return nil, errors.Wrap(err, d.String())
}
// The secret could be nil if it does not exist.
if vaultSecret == nil {
return nil, nil, fmt.Errorf("%s: no secret exists at %s", d, d.path)
return nil, fmt.Errorf("no secret exists at %s", d.path)
}
// Print any warnings.
for _, w := range vaultSecret.Warnings {
log.Printf("[WARN] %s: %s", d, w)
}
// Create our cloned secret.
secret := &Secret{
LeaseID: vaultSecret.LeaseID,
LeaseDuration: leaseDurationOrDefault(vaultSecret.LeaseDuration),
Renewable: vaultSecret.Renewable,
Data: vaultSecret.Data,
}
d.secret = secret
return respWithMetadata(secret)
return vaultSecret, nil
}

View File

@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
)
// RenderInput is used as input to the render function.
type RenderInput struct {
Backup bool
Contents []byte
@ -20,9 +21,22 @@ type RenderInput struct {
Perms os.FileMode
}
// RenderResult is returned and stored. It contains the status of the render
// operationg.
type RenderResult struct {
DidRender bool
// DidRender indicates if the template rendered to disk. This will be false in
// the event of an error, but it will also be false in dry mode or when the
// template on disk matches the new result.
DidRender bool
// WouldRender indicates if the template would have rendered to disk. This
// will return false in the event of an error, but will return true in dry
// mode or when the template on disk matches the new result.
WouldRender bool
// Contents are the actual contents of the resulting template from the render
// operation.
Contents []byte
}
// Render atomically renders a file contents to disk, returning a result of
@ -37,6 +51,7 @@ func Render(i *RenderInput) (*RenderResult, error) {
return &RenderResult{
DidRender: false,
WouldRender: true,
Contents: existing,
}, nil
}
@ -51,6 +66,7 @@ func Render(i *RenderInput) (*RenderResult, error) {
return &RenderResult{
DidRender: true,
WouldRender: true,
Contents: existing,
}, nil
}

View File

@ -42,8 +42,7 @@ type Runner struct {
dry, once bool
// outStream and errStream are the io.Writer streams where the runner will
// write information. These streams can be set using the SetOutStream()
// and SetErrStream() functions.
// write information.
// inStream is the ioReader where the runner will read information.
outStream, errStream io.Writer
@ -118,6 +117,9 @@ type RenderEvent struct {
// Template is the template attempting to be rendered.
Template *template.Template
// Contents is the raw, rendered contents from the template.
Contents []byte
// TemplateConfigs is the list of template configs that correspond to this
// template.
TemplateConfigs []*config.TemplateConfig
@ -643,6 +645,9 @@ func (r *Runner) Run() error {
event.DidRender = true
event.LastDidRender = renderTime
// Update the contents
event.Contents = result.Contents
// Record that at least one template was rendered.
renderedAny = true
@ -1175,6 +1180,7 @@ func newWatcher(c *config.Config, clients *dep.ClientSet, once bool) (*watch.Wat
RetryFuncDefault: nil,
RetryFuncVault: watch.RetryFunc(c.Vault.Retry.RetryFunc()),
VaultGrace: config.TimeDurationVal(c.Vault.Grace),
VaultToken: config.StringVal(c.Vault.Token),
})
if err != nil {
return nil, errors.Wrap(err, "runner")

View File

@ -150,7 +150,7 @@ func (v *View) poll(viewCh chan<- *View, errCh chan<- error) {
// example, Consul make have an outage, but when it returns, the view
// is unchanged. We have to reset the counter retries, but not update the
// actual template.
log.Printf("[TRACE] view %s successful contact, resetting retries", v.dependency)
log.Printf("[TRACE] (view) %s successful contact, resetting retries", v.dependency)
retries = 0
goto WAIT
case err := <-fetchErrCh:

View File

@ -62,6 +62,9 @@ type NewWatcherInput struct {
// RenewVault indicates if this watcher should renew Vault tokens.
RenewVault bool
// VaultToken is the vault token to renew.
VaultToken string
// RetryFuncs specify the different ways to retry based on the upstream.
RetryFuncConsul RetryFunc
RetryFuncDefault RetryFunc
@ -90,7 +93,7 @@ func NewWatcher(i *NewWatcherInput) (*Watcher, error) {
// Start a watcher for the Vault renew if that config was specified
if i.RenewVault {
vt, err := dep.NewVaultTokenQuery()
vt, err := dep.NewVaultTokenQuery(i.VaultToken)
if err != nil {
return nil, errors.Wrap(err, "watcher")
}

View File

@ -135,6 +135,26 @@ func (c *TokenAuth) RenewSelf(increment int) (*Secret, error) {
return ParseSecret(resp.Body)
}
// RenewTokenAsSelf behaves like renew-self, but authenticates using a provided
// token instead of the token attached to the client.
func (c *TokenAuth) RenewTokenAsSelf(token string, increment int) (*Secret, error) {
r := c.c.NewRequest("PUT", "/v1/auth/token/renew-self")
r.ClientToken = token
body := map[string]interface{}{"increment": increment}
if err := r.SetJSONBody(body); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ParseSecret(resp.Body)
}
// RevokeAccessor revokes a token associated with the given accessor
// along with all the child tokens.
func (c *TokenAuth) RevokeAccessor(accessor string) error {

View File

@ -6,13 +6,17 @@ import (
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/net/http2"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-rootcerts"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/sethgrid/pester"
)
@ -21,10 +25,12 @@ const EnvVaultCACert = "VAULT_CACERT"
const EnvVaultCAPath = "VAULT_CAPATH"
const EnvVaultClientCert = "VAULT_CLIENT_CERT"
const EnvVaultClientKey = "VAULT_CLIENT_KEY"
const EnvVaultClientTimeout = "VAULT_CLIENT_TIMEOUT"
const EnvVaultInsecure = "VAULT_SKIP_VERIFY"
const EnvVaultTLSServerName = "VAULT_TLS_SERVER_NAME"
const EnvVaultWrapTTL = "VAULT_WRAP_TTL"
const EnvVaultMaxRetries = "VAULT_MAX_RETRIES"
const EnvVaultToken = "VAULT_TOKEN"
// WrappingLookupFunc is a function that, given an HTTP verb and a path,
// returns an optional string duration to be used for response wrapping (e.g.
@ -50,6 +56,9 @@ type Config struct {
// MaxRetries controls the maximum number of times to retry when a 5xx error
// occurs. Set to 0 or less to disable retrying. Defaults to 0.
MaxRetries int
// Timeout is for setting custom timeout parameter in the HttpClient
Timeout time.Duration
}
// TLSConfig contains the parameters needed to configure TLS on the HTTP client
@ -84,8 +93,7 @@ type TLSConfig struct {
// setting the `VAULT_ADDR` environment variable.
func DefaultConfig() *Config {
config := &Config{
Address: "https://127.0.0.1:8200",
Address: "https://127.0.0.1:8200",
HttpClient: cleanhttp.DefaultClient(),
}
config.HttpClient.Timeout = time.Second * 60
@ -104,9 +112,8 @@ func DefaultConfig() *Config {
// ConfigureTLS takes a set of TLS configurations and applies those to the the HTTP client.
func (c *Config) ConfigureTLS(t *TLSConfig) error {
if c.HttpClient == nil {
return fmt.Errorf("config HTTP Client must be set")
c.HttpClient = DefaultConfig().HttpClient
}
var clientCert tls.Certificate
@ -154,6 +161,7 @@ func (c *Config) ReadEnvironment() error {
var envCAPath string
var envClientCert string
var envClientKey string
var envClientTimeout time.Duration
var envInsecure bool
var envTLSServerName string
var envMaxRetries *uint64
@ -181,6 +189,13 @@ func (c *Config) ReadEnvironment() error {
if v := os.Getenv(EnvVaultClientKey); v != "" {
envClientKey = v
}
if t := os.Getenv(EnvVaultClientTimeout); t != "" {
clientTimeout, err := parseutil.ParseDurationSecond(t)
if err != nil {
return fmt.Errorf("Could not parse %s", EnvVaultClientTimeout)
}
envClientTimeout = clientTimeout
}
if v := os.Getenv(EnvVaultInsecure); v != "" {
var err error
envInsecure, err = strconv.ParseBool(v)
@ -213,6 +228,10 @@ func (c *Config) ReadEnvironment() error {
c.MaxRetries = int(*envMaxRetries) + 1
}
if envClientTimeout != 0 {
c.Timeout = envClientTimeout
}
return nil
}
@ -247,6 +266,11 @@ func NewClient(c *Config) (*Client, error) {
c.HttpClient = DefaultConfig().HttpClient
}
tp := c.HttpClient.Transport.(*http.Transport)
if err := http2.ConfigureTransport(tp); err != nil {
return nil, err
}
redirFunc := func() {
// Ensure redirects are not automatically followed
// Note that this is sane for the API client as it has its own
@ -254,9 +278,9 @@ func NewClient(c *Config) (*Client, error) {
// but in e.g. http_test actual redirect handling is necessary
c.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// Returning this value causes the Go net library to not close the
// response body and nil out the error. Otherwise pester tries
// response body and to nil out the error. Otherwise pester tries
// three times on every redirect because it sees an error from this
// function being passed through.
// function (to prevent redirects) passing through to it.
return http.ErrUseLastResponse
}
}
@ -268,7 +292,7 @@ func NewClient(c *Config) (*Client, error) {
config: c,
}
if token := os.Getenv("VAULT_TOKEN"); token != "" {
if token := os.Getenv(EnvVaultToken); token != "" {
client.SetToken(token)
}
@ -292,6 +316,16 @@ func (c *Client) Address() string {
return c.addr.String()
}
// SetMaxRetries sets the number of retries that will be used in the case of certain errors
func (c *Client) SetMaxRetries(retries int) {
c.config.MaxRetries = retries
}
// SetClientTimeout sets the client request timeout
func (c *Client) SetClientTimeout(timeout time.Duration) {
c.config.Timeout = timeout
}
// SetWrappingLookupFunc sets a lookup function that returns desired wrap TTLs
// for a given operation and path
func (c *Client) SetWrappingLookupFunc(lookupFunc WrappingLookupFunc) {
@ -315,16 +349,22 @@ func (c *Client) ClearToken() {
c.token = ""
}
// Clone creates a copy of this client.
func (c *Client) Clone() (*Client, error) {
return NewClient(c.config)
}
// NewRequest creates a new raw request object to query the Vault server
// configured for this client. This is an advanced method and generally
// doesn't need to be called externally.
func (c *Client) NewRequest(method, path string) *Request {
func (c *Client) NewRequest(method, requestPath string) *Request {
req := &Request{
Method: method,
URL: &url.URL{
User: c.addr.User,
Scheme: c.addr.Scheme,
Host: c.addr.Host,
Path: path,
Path: path.Join(c.addr.Path, requestPath),
},
ClientToken: c.token,
Params: make(map[string][]string),
@ -332,18 +372,21 @@ func (c *Client) NewRequest(method, path string) *Request {
var lookupPath string
switch {
case strings.HasPrefix(path, "/v1/"):
lookupPath = strings.TrimPrefix(path, "/v1/")
case strings.HasPrefix(path, "v1/"):
lookupPath = strings.TrimPrefix(path, "v1/")
case strings.HasPrefix(requestPath, "/v1/"):
lookupPath = strings.TrimPrefix(requestPath, "/v1/")
case strings.HasPrefix(requestPath, "v1/"):
lookupPath = strings.TrimPrefix(requestPath, "v1/")
default:
lookupPath = path
lookupPath = requestPath
}
if c.wrappingLookupFunc != nil {
req.WrapTTL = c.wrappingLookupFunc(method, lookupPath)
} else {
req.WrapTTL = DefaultWrappingLookupFunc(method, lookupPath)
}
if c.config.Timeout != 0 {
c.config.HttpClient.Timeout = c.config.Timeout
}
return req
}

302
vendor/github.com/hashicorp/vault/api/renewer.go generated vendored Normal file
View File

@ -0,0 +1,302 @@
package api
import (
"errors"
"math/rand"
"sync"
"time"
)
var (
ErrRenewerMissingInput = errors.New("missing input to renewer")
ErrRenewerMissingSecret = errors.New("missing secret to renew")
ErrRenewerNotRenewable = errors.New("secret is not renewable")
ErrRenewerNoSecretData = errors.New("returned empty secret data")
// DefaultRenewerGrace is the default grace period
DefaultRenewerGrace = 15 * time.Second
// DefaultRenewerRenewBuffer is the default size of the buffer for renew
// messages on the channel.
DefaultRenewerRenewBuffer = 5
)
// Renewer is a process for renewing a secret.
//
// renewer, err := client.NewRenewer(&RenewerInput{
// Secret: mySecret,
// })
// go renewer.Renew()
// defer renewer.Stop()
//
// for {
// select {
// case err := <-renewer.DoneCh():
// if err != nil {
// log.Fatal(err)
// }
//
// // Renewal is now over
// case renewal := <-renewer.RenewCh():
// log.Printf("Successfully renewed: %#v", renewal)
// }
// }
//
//
// The `DoneCh` will return if renewal fails or if the remaining lease duration
// after a renewal is less than or equal to the grace (in number of seconds). In
// both cases, the caller should attempt a re-read of the secret. Clients should
// check the return value of the channel to see if renewal was successful.
type Renewer struct {
l sync.Mutex
client *Client
secret *Secret
grace time.Duration
random *rand.Rand
doneCh chan error
renewCh chan *RenewOutput
stopped bool
stopCh chan struct{}
}
// RenewerInput is used as input to the renew function.
type RenewerInput struct {
// Secret is the secret to renew
Secret *Secret
// Grace is a minimum renewal before returning so the upstream client
// can do a re-read. This can be used to prevent clients from waiting
// too long to read a new credential and incur downtime.
Grace time.Duration
// Rand is the randomizer to use for underlying randomization. If not
// provided, one will be generated and seeded automatically. If provided, it
// is assumed to have already been seeded.
Rand *rand.Rand
// RenewBuffer is the size of the buffered channel where renew messages are
// dispatched.
RenewBuffer int
}
// RenewOutput is the metadata returned to the client (if it's listening) to
// renew messages.
type RenewOutput struct {
// RenewedAt is the timestamp when the renewal took place (UTC).
RenewedAt time.Time
// Secret is the underlying renewal data. It's the same struct as all data
// that is returned from Vault, but since this is renewal data, it will not
// usually include the secret itself.
Secret *Secret
}
// NewRenewer creates a new renewer from the given input.
func (c *Client) NewRenewer(i *RenewerInput) (*Renewer, error) {
if i == nil {
return nil, ErrRenewerMissingInput
}
secret := i.Secret
if secret == nil {
return nil, ErrRenewerMissingSecret
}
grace := i.Grace
if grace == 0 {
grace = DefaultRenewerGrace
}
random := i.Rand
if random == nil {
random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
}
renewBuffer := i.RenewBuffer
if renewBuffer == 0 {
renewBuffer = DefaultRenewerRenewBuffer
}
return &Renewer{
client: c,
secret: secret,
grace: grace,
random: random,
doneCh: make(chan error, 1),
renewCh: make(chan *RenewOutput, renewBuffer),
stopped: false,
stopCh: make(chan struct{}),
}, nil
}
// DoneCh returns the channel where the renewer will publish when renewal stops.
// If there is an error, this will be an error.
func (r *Renewer) DoneCh() <-chan error {
return r.doneCh
}
// RenewCh is a channel that receives a message when a successful renewal takes
// place and includes metadata about the renewal.
func (r *Renewer) RenewCh() <-chan *RenewOutput {
return r.renewCh
}
// Stop stops the renewer.
func (r *Renewer) Stop() {
r.l.Lock()
if !r.stopped {
close(r.stopCh)
r.stopped = true
}
r.l.Unlock()
}
// Renew starts a background process for renewing this secret. When the secret
// is has auth data, this attempts to renew the auth (token). When the secret
// has a lease, this attempts to renew the lease.
func (r *Renewer) Renew() {
var result error
if r.secret.Auth != nil {
result = r.renewAuth()
} else {
result = r.renewLease()
}
select {
case r.doneCh <- result:
case <-r.stopCh:
}
}
// renewAuth is a helper for renewing authentication.
func (r *Renewer) renewAuth() error {
if !r.secret.Auth.Renewable || r.secret.Auth.ClientToken == "" {
return ErrRenewerNotRenewable
}
client, token := r.client, r.secret.Auth.ClientToken
for {
// Check if we are stopped.
select {
case <-r.stopCh:
return nil
default:
}
// Renew the auth.
renewal, err := client.Auth().Token().RenewTokenAsSelf(token, 0)
if err != nil {
return err
}
// Push a message that a renewal took place.
select {
case r.renewCh <- &RenewOutput{time.Now().UTC(), renewal}:
default:
}
// Somehow, sometimes, this happens.
if renewal == nil || renewal.Auth == nil {
return ErrRenewerNoSecretData
}
// Do nothing if we are not renewable
if !renewal.Auth.Renewable {
return ErrRenewerNotRenewable
}
// Grab the lease duration and sleep duration - note that we grab the auth
// lease duration, not the secret lease duration.
leaseDuration := time.Duration(renewal.Auth.LeaseDuration) * time.Second
sleepDuration := r.sleepDuration(leaseDuration)
// If we are within grace, return now.
if leaseDuration <= r.grace || sleepDuration <= r.grace {
return nil
}
select {
case <-r.stopCh:
return nil
case <-time.After(sleepDuration):
continue
}
}
}
// renewLease is a helper for renewing a lease.
func (r *Renewer) renewLease() error {
if !r.secret.Renewable || r.secret.LeaseID == "" {
return ErrRenewerNotRenewable
}
client, leaseID := r.client, r.secret.LeaseID
for {
// Check if we are stopped.
select {
case <-r.stopCh:
return nil
default:
}
// Renew the lease.
renewal, err := client.Sys().Renew(leaseID, 0)
if err != nil {
return err
}
// Push a message that a renewal took place.
select {
case r.renewCh <- &RenewOutput{time.Now().UTC(), renewal}:
default:
}
// Somehow, sometimes, this happens.
if renewal == nil {
return ErrRenewerNoSecretData
}
// Do nothing if we are not renewable
if !renewal.Renewable {
return ErrRenewerNotRenewable
}
// Grab the lease duration and sleep duration
leaseDuration := time.Duration(renewal.LeaseDuration) * time.Second
sleepDuration := r.sleepDuration(leaseDuration)
// If we are within grace, return now.
if leaseDuration <= r.grace || sleepDuration <= r.grace {
return nil
}
select {
case <-r.stopCh:
return nil
case <-time.After(sleepDuration):
continue
}
}
}
// sleepDuration calculates the time to sleep given the base lease duration. The
// base is the resulting lease duration. It will be reduced to 1/3 and
// multiplied by a random float between 0.0 and 1.0. This extra randomness
// prevents multiple clients from all trying to renew simultaneously.
func (r *Renewer) sleepDuration(base time.Duration) time.Duration {
sleep := float64(base)
// Renew at 1/3 the remaining lease. This will give us an opportunity to retry
// at least one more time should the first renewal fail.
sleep = sleep / 3.0
// Use a randomness so many clients do not hit Vault simultaneously.
sleep = sleep * (r.random.Float64() + 1) / 2.0
return time.Duration(sleep)
}

View File

@ -14,6 +14,7 @@ type Request struct {
Method string
URL *url.URL
Params url.Values
Headers http.Header
ClientToken string
WrapTTL string
Obj interface{}
@ -55,10 +56,19 @@ func (r *Request) ToHTTP() (*http.Request, error) {
return nil, err
}
req.URL.User = r.URL.User
req.URL.Scheme = r.URL.Scheme
req.URL.Host = r.URL.Host
req.Host = r.URL.Host
if r.Headers != nil {
for header, vals := range r.Headers {
for _, val := range vals {
req.Header.Add(header, val)
}
}
}
if len(r.ClientToken) != 0 {
req.Header.Set("X-Vault-Token", r.ClientToken)
}

View File

@ -25,8 +25,9 @@ func (r *Response) DecodeJSON(out interface{}) error {
// this will fully consume the response body, but will not close it. The
// body must still be closed manually.
func (r *Response) Error() error {
// 200 to 399 are okay status codes
if r.StatusCode >= 200 && r.StatusCode < 400 {
// 200 to 399 are okay status codes. 429 is the code for health status of
// standby nodes.
if (r.StatusCode >= 200 && r.StatusCode < 400) || r.StatusCode == 429 {
return nil
}

View File

@ -3,6 +3,7 @@ package api
import (
"fmt"
"github.com/fatih/structs"
"github.com/mitchellh/mapstructure"
)
@ -71,13 +72,18 @@ func (c *Sys) ListAudit() (map[string]*Audit, error) {
return mounts, nil
}
// DEPRECATED: Use EnableAuditWithOptions instead
func (c *Sys) EnableAudit(
path string, auditType string, desc string, opts map[string]string) error {
body := map[string]interface{}{
"type": auditType,
"description": desc,
"options": opts,
}
return c.EnableAuditWithOptions(path, &EnableAuditOptions{
Type: auditType,
Description: desc,
Options: opts,
})
}
func (c *Sys) EnableAuditWithOptions(path string, options *EnableAuditOptions) error {
body := structs.Map(options)
r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/sys/audit/%s", path))
if err := r.SetJSONBody(body); err != nil {
@ -106,9 +112,17 @@ func (c *Sys) DisableAudit(path string) error {
// individually documented because the map almost directly to the raw HTTP API
// documentation. Please refer to that documentation for more details.
type EnableAuditOptions struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Options map[string]string `json:"options" structs:"options"`
Local bool `json:"local" structs:"local"`
}
type Audit struct {
Path string
Type string
Description string
Options map[string]string
Local bool
}

View File

@ -3,6 +3,7 @@ package api
import (
"fmt"
"github.com/fatih/structs"
"github.com/mitchellh/mapstructure"
)
@ -42,11 +43,16 @@ func (c *Sys) ListAuth() (map[string]*AuthMount, error) {
return mounts, nil
}
// DEPRECATED: Use EnableAuthWithOptions instead
func (c *Sys) EnableAuth(path, authType, desc string) error {
body := map[string]string{
"type": authType,
"description": desc,
}
return c.EnableAuthWithOptions(path, &EnableAuthOptions{
Type: authType,
Description: desc,
})
}
func (c *Sys) EnableAuthWithOptions(path string, options *EnableAuthOptions) error {
body := structs.Map(options)
r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/auth/%s", path))
if err := r.SetJSONBody(body); err != nil {
@ -75,13 +81,23 @@ func (c *Sys) DisableAuth(path string) error {
// individually documentd because the map almost directly to the raw HTTP API
// documentation. Please refer to that documentation for more details.
type EnableAuthOptions struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Local bool `json:"local" structs:"local"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
type AuthMount struct {
Type string `json:"type" structs:"type" mapstructure:"type"`
Description string `json:"description" structs:"description" mapstructure:"description"`
Accessor string `json:"accessor" structs:"accessor" mapstructure:"accessor"`
Config AuthConfigOutput `json:"config" structs:"config" mapstructure:"config"`
Local bool `json:"local" structs:"local" mapstructure:"local"`
}
type AuthConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}

View File

@ -0,0 +1,56 @@
package api
func (c *Sys) CORSStatus() (*CORSResponse, error) {
r := c.c.NewRequest("GET", "/v1/sys/config/cors")
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result CORSResponse
err = resp.DecodeJSON(&result)
return &result, err
}
func (c *Sys) ConfigureCORS(req *CORSRequest) (*CORSResponse, error) {
r := c.c.NewRequest("PUT", "/v1/sys/config/cors")
if err := r.SetJSONBody(req); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result CORSResponse
err = resp.DecodeJSON(&result)
return &result, err
}
func (c *Sys) DisableCORS() (*CORSResponse, error) {
r := c.c.NewRequest("DELETE", "/v1/sys/config/cors")
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result CORSResponse
err = resp.DecodeJSON(&result)
return &result, err
}
type CORSRequest struct {
AllowedOrigins string `json:"allowed_origins"`
Enabled bool `json:"enabled"`
}
type CORSResponse struct {
AllowedOrigins string `json:"allowed_origins"`
Enabled bool `json:"enabled"`
}

24
vendor/github.com/hashicorp/vault/api/sys_health.go generated vendored Normal file
View File

@ -0,0 +1,24 @@
package api
func (c *Sys) Health() (*HealthResponse, error) {
r := c.c.NewRequest("GET", "/v1/sys/health")
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result HealthResponse
err = resp.DecodeJSON(&result)
return &result, err
}
type HealthResponse struct {
Initialized bool `json:"initialized"`
Sealed bool `json:"sealed"`
Standby bool `json:"standby"`
ServerTimeUTC int64 `json:"server_time_utc"`
Version string `json:"version"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
}

View File

@ -14,7 +14,8 @@ func (c *Sys) Leader() (*LeaderResponse, error) {
}
type LeaderResponse struct {
HAEnabled bool `json:"ha_enabled"`
IsSelf bool `json:"is_self"`
LeaderAddress string `json:"leader_address"`
HAEnabled bool `json:"ha_enabled"`
IsSelf bool `json:"is_self"`
LeaderAddress string `json:"leader_address"`
LeaderClusterAddress string `json:"leader_cluster_address"`
}

View File

@ -1,7 +1,7 @@
package api
func (c *Sys) Renew(id string, increment int) (*Secret, error) {
r := c.c.NewRequest("PUT", "/v1/sys/renew")
r := c.c.NewRequest("PUT", "/v1/sys/leases/renew")
body := map[string]interface{}{
"increment": increment,
@ -21,7 +21,7 @@ func (c *Sys) Renew(id string, increment int) (*Secret, error) {
}
func (c *Sys) Revoke(id string) error {
r := c.c.NewRequest("PUT", "/v1/sys/revoke/"+id)
r := c.c.NewRequest("PUT", "/v1/sys/leases/revoke/"+id)
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
@ -30,7 +30,7 @@ func (c *Sys) Revoke(id string) error {
}
func (c *Sys) RevokePrefix(id string) error {
r := c.c.NewRequest("PUT", "/v1/sys/revoke-prefix/"+id)
r := c.c.NewRequest("PUT", "/v1/sys/leases/revoke-prefix/"+id)
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
@ -39,7 +39,7 @@ func (c *Sys) RevokePrefix(id string) error {
}
func (c *Sys) RevokeForce(id string) error {
r := c.c.NewRequest("PUT", "/v1/sys/revoke-force/"+id)
r := c.c.NewRequest("PUT", "/v1/sys/leases/revoke-force/"+id)
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()

View File

@ -123,20 +123,27 @@ type MountInput struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Config MountConfigInput `json:"config" structs:"config"`
Local bool `json:"local" structs:"local"`
}
type MountConfigInput struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
type MountOutput struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Accessor string `json:"accessor" structs:"accessor"`
Config MountConfigOutput `json:"config" structs:"config"`
Local bool `json:"local" structs:"local"`
}
type MountConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}

View File

@ -53,6 +53,7 @@ type SealStatusResponse struct {
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
Nonce string `json:"nonce"`
Version string `json:"version"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`

View File

@ -2,13 +2,19 @@ package jsonutil
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"github.com/hashicorp/vault/helper/compressutil"
)
// Encodes/Marshals the given object into JSON
func EncodeJSON(in interface{}) ([]byte, error) {
if in == nil {
return nil, fmt.Errorf("input for encoding is nil")
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(in); err != nil {
@ -17,15 +23,60 @@ func EncodeJSON(in interface{}) ([]byte, error) {
return buf.Bytes(), nil
}
// Decodes/Unmarshals the given JSON into a desired object
// EncodeJSONAndCompress encodes the given input into JSON and compresses the
// encoded value (using Gzip format BestCompression level, by default). A
// canary byte is placed at the beginning of the returned bytes for the logic
// in decompression method to identify compressed input.
func EncodeJSONAndCompress(in interface{}, config *compressutil.CompressionConfig) ([]byte, error) {
if in == nil {
return nil, fmt.Errorf("input for encoding is nil")
}
// First JSON encode the given input
encodedBytes, err := EncodeJSON(in)
if err != nil {
return nil, err
}
if config == nil {
config = &compressutil.CompressionConfig{
Type: compressutil.CompressionTypeGzip,
GzipCompressionLevel: gzip.BestCompression,
}
}
return compressutil.Compress(encodedBytes, config)
}
// DecodeJSON tries to decompress the given data. The call to decompress, fails
// if the content was not compressed in the first place, which is identified by
// a canary byte before the compressed data. If the data is not compressed, it
// is JSON decoded directly. Otherwise the decompressed data will be JSON
// decoded.
func DecodeJSON(data []byte, out interface{}) error {
if data == nil {
if data == nil || len(data) == 0 {
return fmt.Errorf("'data' being decoded is nil")
}
if out == nil {
return fmt.Errorf("output parameter 'out' is nil")
}
// Decompress the data if it was compressed in the first place
decompressedBytes, uncompressed, err := compressutil.Decompress(data)
if err != nil {
return fmt.Errorf("failed to decompress JSON: err: %v", err)
}
if !uncompressed && (decompressedBytes == nil || len(decompressedBytes) == 0) {
return fmt.Errorf("decompressed data being decoded is invalid")
}
// If the input supplied failed to contain the compression canary, it
// will be notified by the compression utility. Decode the decompressed
// input.
if !uncompressed {
data = decompressedBytes
}
return DecodeJSONFromReader(bytes.NewReader(data), out)
}

48
vendor/vendor.json vendored
View File

@ -611,44 +611,44 @@
{
"checksumSHA1": "Nu2j1GusM7ZH0uYrGzqr1K7yH7I=",
"path": "github.com/hashicorp/consul-template/child",
"revision": "ecbc27c1922fed2f562e7fb63e1ad24e818fa60e",
"revisionTime": "2017-07-05T14:04:00Z"
"revision": "7b3f45039cf3ad1a758683fd3eebb1cc72affa06",
"revisionTime": "2017-08-01T00:58:49Z"
},
{
"checksumSHA1": "QWcGW3wELSp/YsOVzCW02oEYR7c=",
"checksumSHA1": "lemUzh6uQDMxuvTT/BREYdGcS0U=",
"path": "github.com/hashicorp/consul-template/config",
"revision": "ecbc27c1922fed2f562e7fb63e1ad24e818fa60e",
"revisionTime": "2017-07-05T14:04:00Z"
"revision": "7b3f45039cf3ad1a758683fd3eebb1cc72affa06",
"revisionTime": "2017-08-01T00:58:49Z"
},
{
"checksumSHA1": "mV7yjHpIfO4yRAdQaBlAqdGDKO8=",
"checksumSHA1": "WVZ+pqn/HLLXjj+Tj5ZZvD7w6r0=",
"path": "github.com/hashicorp/consul-template/dependency",
"revision": "ecbc27c1922fed2f562e7fb63e1ad24e818fa60e",
"revisionTime": "2017-07-05T14:04:00Z"
"revision": "7b3f45039cf3ad1a758683fd3eebb1cc72affa06",
"revisionTime": "2017-08-01T00:58:49Z"
},
{
"checksumSHA1": "ZTlPhrxNzME75A4ydXM88TFt3Qs=",
"checksumSHA1": "Cu8hIII8Z6FAuunFI/jXPLl0nQA=",
"path": "github.com/hashicorp/consul-template/manager",
"revision": "ecbc27c1922fed2f562e7fb63e1ad24e818fa60e",
"revisionTime": "2017-07-05T14:04:00Z"
"revision": "7b3f45039cf3ad1a758683fd3eebb1cc72affa06",
"revisionTime": "2017-08-01T00:58:49Z"
},
{
"checksumSHA1": "oskgb0WteBKOItG8NNDduM7E/D0=",
"path": "github.com/hashicorp/consul-template/signals",
"revision": "ecbc27c1922fed2f562e7fb63e1ad24e818fa60e",
"revisionTime": "2017-07-05T14:04:00Z"
"revision": "7b3f45039cf3ad1a758683fd3eebb1cc72affa06",
"revisionTime": "2017-08-01T00:58:49Z"
},
{
"checksumSHA1": "zSvJlNfZS3fCRlFaZ7r9Q+N17T8=",
"path": "github.com/hashicorp/consul-template/template",
"revision": "ecbc27c1922fed2f562e7fb63e1ad24e818fa60e",
"revisionTime": "2017-07-05T14:04:00Z"
"revision": "7b3f45039cf3ad1a758683fd3eebb1cc72affa06",
"revisionTime": "2017-08-01T00:58:49Z"
},
{
"checksumSHA1": "85W96Fo50FmrMaba7Dk12aDfwWs=",
"checksumSHA1": "b4+Y+02pY2Y5620F9ALzKg8Zmdw=",
"path": "github.com/hashicorp/consul-template/watch",
"revision": "ecbc27c1922fed2f562e7fb63e1ad24e818fa60e",
"revisionTime": "2017-07-05T14:04:00Z"
"revision": "7b3f45039cf3ad1a758683fd3eebb1cc72affa06",
"revisionTime": "2017-08-01T00:58:49Z"
},
{
"checksumSHA1": "jfELEMRhiTcppZmRH+ZwtkVS5Uw=",
@ -911,16 +911,16 @@
"revisionTime": "2016-08-21T23:40:57Z"
},
{
"checksumSHA1": "31yBeS6U3xm7VJ7ZvDxRgBxXP0A=",
"checksumSHA1": "hLIXn9iQhPcjY+/G64j3mIlLlK8=",
"path": "github.com/hashicorp/vault/api",
"revision": "f4adc7fa960ed8e828f94bc6785bcdbae8d1b263",
"revisionTime": "2016-12-16T21:07:16Z"
"revision": "0c3e14f047aede0a70256e1e8b321610910b246e",
"revisionTime": "2017-08-01T15:50:41Z"
},
{
"checksumSHA1": "5lR6EdY0ARRdKAq3hZcL38STD8Q=",
"checksumSHA1": "yUiSTPf0QUuL2r/81sjuytqBoeQ=",
"path": "github.com/hashicorp/vault/helper/jsonutil",
"revision": "bcf98fa8d61d1870c3af689f1b090b29a9c12d8c",
"revisionTime": "2016-08-02T20:35:37Z"
"revision": "0c3e14f047aede0a70256e1e8b321610910b246e",
"revisionTime": "2017-08-01T15:50:41Z"
},
{
"checksumSHA1": "VMaF3Q7RIrRzvbnPbqxuSLryOvc=",

View File

@ -736,6 +736,14 @@ README][ct].
splay value before invoking the change mode. Should be specified in
nanoseconds.
- `VaultGrace` - Specifies the grace period between lease renewal and secret
re-acquisition. When renewing a secret, if the remaining lease is less than or
equal to the configured grace, the template will request a new credential.
This prevents Vault from revoking the secret at its expiration and the task
having a stale secret. If the grace is set to a value that is higher than your
default TTL or max TTL, the template will always read a new secret. If the
task defines several templates, the `vault_grace` will be set to the lowest
value across all the templates.
```json
{

View File

@ -94,6 +94,15 @@ README][ct]. Since Nomad v0.6.0, templates can be read as environment variables.
prevent a thundering herd problem where all task instances restart at the same
time.
- `vault_grace` `(string: "5m")` - Specifies the grace period between lease
renewal and secret re-acquisition. When renewing a secret, if the remaining
lease is less than or equal to the configured grace, the template will request
a new credential. This prevents Vault from revoking the secret at its
expiration and the task having a stale secret. If the grace is set to a value
that is higher than your default TTL or max TTL, the template will always read
a new secret. If the task defines several templates, the `vault_grace` will be
set to the lowest value across all the templates.
## `template` Examples
The following examples only show the `template` stanzas. Remember that the