Cloudeploy/consul-template/config.go

609 lines
16 KiB
Go

package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/hashicorp/consul-template/watch"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl"
"github.com/mitchellh/mapstructure"
)
// The pattern to split the config template syntax on
var configTemplateRe = regexp.MustCompile("([a-zA-Z]:)?([^:]+)")
// defaultFilePerms are the default file permissions for templates rendered
// onto disk when a specific file permission has not already been specified.
const defaultFilePerms = 0644
// defaultDedupPrefix is the default prefix used for de-duplication mode
const defaultDedupPrefix = "consul-template/dedup/"
// Config is used to configure Consul Template
type Config struct {
// Path is the path to this configuration file on disk. This value is not
// read from disk by rather dynamically populated by the code so the Config
// has a reference to the path to the file on disk that created it.
Path string `json:"path" mapstructure:"-"`
// Consul is the location of the Consul instance to query (may be an IP
// address or FQDN) with port.
Consul string `json:"consul" mapstructure:"consul"`
// Token is the Consul API token.
Token string `json:"-" mapstructure:"token"`
// Auth is the HTTP basic authentication for communicating with Consul.
Auth *AuthConfig `json:"auth" mapstructure:"auth"`
// Vault is the configuration for connecting to a vault server.
Vault *VaultConfig `json:"vault" mapstructure:"vault"`
// SSL indicates we should use a secure connection while talking to
// Consul. This requires Consul to be configured to serve HTTPS.
SSL *SSLConfig `json:"ssl" mapstructure:"ssl"`
// Syslog is the configuration for syslog.
Syslog *SyslogConfig `json:"syslog" mapstructure:"syslog"`
// MaxStale is the maximum amount of time for staleness from Consul as given
// by LastContact. If supplied, Consul Template will query all servers instead
// of just the leader.
MaxStale time.Duration `json:"max_stale" mapstructure:"max_stale"`
// ConfigTemplates is a slice of the ConfigTemplate objects in the config.
ConfigTemplates []*ConfigTemplate `json:"templates" mapstructure:"template"`
// Retry is the duration of time to wait between Consul failures.
Retry time.Duration `json:"retry" mapstructure:"retry"`
// Wait is the quiescence timers.
Wait *watch.Wait `json:"wait" mapstructure:"wait"`
// PidFile is the path on disk where a PID file should be written containing
// this processes PID.
PidFile string `json:"pid_file" mapstructure:"pid_file"`
// LogLevel is the level with which to log for this config.
LogLevel string `json:"log_level" mapstructure:"log_level"`
// Deduplicate is used to configure the dedup settings
Deduplicate *DeduplicateConfig `json:"deduplicate" mapstructure:"deduplicate"`
// Reap is used to configure automatic reaping of child processes.
Reap bool `json:"reap" mapstructure:"reap"`
// setKeys is the list of config keys that were set by the user.
setKeys map[string]struct{}
}
// Merge merges the values in config into this config object. Values in the
// config object overwrite the values in c.
func (c *Config) Merge(config *Config) {
if config.WasSet("path") {
c.Path = config.Path
}
if config.WasSet("consul") {
c.Consul = config.Consul
}
if config.WasSet("token") {
c.Token = config.Token
}
if config.WasSet("vault") {
if c.Vault == nil {
c.Vault = &VaultConfig{}
}
if config.WasSet("vault.address") {
c.Vault.Address = config.Vault.Address
}
if config.WasSet("vault.token") {
c.Vault.Token = config.Vault.Token
}
if config.WasSet("vault.renew") {
c.Vault.Renew = config.Vault.Renew
}
if config.WasSet("vault.ssl") {
if c.Vault.SSL == nil {
c.Vault.SSL = &SSLConfig{}
}
if config.WasSet("vault.ssl.verify") {
c.Vault.SSL.Verify = config.Vault.SSL.Verify
c.Vault.SSL.Enabled = true
}
if config.WasSet("vault.ssl.cert") {
c.Vault.SSL.Cert = config.Vault.SSL.Cert
c.Vault.SSL.Enabled = true
}
if config.WasSet("vault.ssl.ca_cert") {
c.Vault.SSL.CaCert = config.Vault.SSL.CaCert
c.Vault.SSL.Enabled = true
}
if config.WasSet("vault.ssl.enabled") {
c.Vault.SSL.Enabled = config.Vault.SSL.Enabled
}
}
}
if config.WasSet("auth") {
if c.Auth == nil {
c.Auth = &AuthConfig{}
}
if config.WasSet("auth.username") {
c.Auth.Username = config.Auth.Username
c.Auth.Enabled = true
}
if config.WasSet("auth.password") {
c.Auth.Password = config.Auth.Password
c.Auth.Enabled = true
}
if config.WasSet("auth.enabled") {
c.Auth.Enabled = config.Auth.Enabled
}
}
if config.WasSet("ssl") {
if c.SSL == nil {
c.SSL = &SSLConfig{}
}
if config.WasSet("ssl.verify") {
c.SSL.Verify = config.SSL.Verify
c.SSL.Enabled = true
}
if config.WasSet("ssl.cert") {
c.SSL.Cert = config.SSL.Cert
c.SSL.Enabled = true
}
if config.WasSet("ssl.key") {
c.SSL.Key = config.SSL.Key
c.SSL.Enabled = true
}
if config.WasSet("ssl.ca_cert") {
c.SSL.CaCert = config.SSL.CaCert
c.SSL.Enabled = true
}
if config.WasSet("ssl.enabled") {
c.SSL.Enabled = config.SSL.Enabled
}
}
if config.WasSet("syslog") {
if c.Syslog == nil {
c.Syslog = &SyslogConfig{}
}
if config.WasSet("syslog.facility") {
c.Syslog.Facility = config.Syslog.Facility
c.Syslog.Enabled = true
}
if config.WasSet("syslog.enabled") {
c.Syslog.Enabled = config.Syslog.Enabled
}
}
if config.WasSet("max_stale") {
c.MaxStale = config.MaxStale
}
if len(config.ConfigTemplates) > 0 {
if c.ConfigTemplates == nil {
c.ConfigTemplates = make([]*ConfigTemplate, 0, 1)
}
for _, template := range config.ConfigTemplates {
c.ConfigTemplates = append(c.ConfigTemplates, &ConfigTemplate{
Source: template.Source,
Destination: template.Destination,
Command: template.Command,
Perms: template.Perms,
Backup: template.Backup,
})
}
}
if config.WasSet("retry") {
c.Retry = config.Retry
}
if config.WasSet("wait") {
c.Wait = &watch.Wait{
Min: config.Wait.Min,
Max: config.Wait.Max,
}
}
if config.WasSet("pid_file") {
c.PidFile = config.PidFile
}
if config.WasSet("log_level") {
c.LogLevel = config.LogLevel
}
if config.WasSet("deduplicate") {
if c.Deduplicate == nil {
c.Deduplicate = &DeduplicateConfig{}
}
if config.WasSet("deduplicate.enabled") {
c.Deduplicate.Enabled = config.Deduplicate.Enabled
}
if config.WasSet("deduplicate.prefix") {
c.Deduplicate.Prefix = config.Deduplicate.Prefix
}
}
if config.WasSet("reap") {
c.Reap = config.Reap
}
if c.setKeys == nil {
c.setKeys = make(map[string]struct{})
}
for k, _ := range config.setKeys {
if _, ok := c.setKeys[k]; !ok {
c.setKeys[k] = struct{}{}
}
}
}
// WasSet determines if the given key was set in the config (as opposed to just
// having the default value).
func (c *Config) WasSet(key string) bool {
if _, ok := c.setKeys[key]; ok {
return true
}
return false
}
// set is a helper function for marking a key as set.
func (c *Config) set(key string) {
if _, ok := c.setKeys[key]; !ok {
c.setKeys[key] = struct{}{}
}
}
// ParseConfig reads the configuration file at the given path and returns a new
// Config struct with the data populated.
func ParseConfig(path string) (*Config, error) {
var errs *multierror.Error
// Read the contents of the file
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading config at %q: %s", path, err)
}
// Parse the file (could be HCL or JSON)
var shadow interface{}
if err := hcl.Decode(&shadow, string(contents)); err != nil {
return nil, fmt.Errorf("error decoding config at %q: %s", path, err)
}
// Convert to a map and flatten the keys we want to flatten
parsed, ok := shadow.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("error converting config at %q", path)
}
flattenKeys(parsed, []string{"auth", "ssl", "syslog", "vault", "deduplicate"})
// Create a new, empty config
config := new(Config)
// Use mapstructure to populate the basic config fields
metadata := new(mapstructure.Metadata)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
watch.StringToWaitDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
mapstructure.StringToTimeDurationHookFunc(),
),
ErrorUnused: true,
Metadata: metadata,
Result: config,
})
if err != nil {
errs = multierror.Append(errs, err)
return nil, errs.ErrorOrNil()
}
if err := decoder.Decode(parsed); err != nil {
errs = multierror.Append(errs, err)
return nil, errs.ErrorOrNil()
}
// Store a reference to the path where this config was read from
config.Path = path
// Ensure there's a default value for the template's file permissions
for _, t := range config.ConfigTemplates {
if t.Perms == 0000 {
t.Perms = defaultFilePerms
}
}
// Update the list of set keys
if config.setKeys == nil {
config.setKeys = make(map[string]struct{})
}
for _, key := range metadata.Keys {
if _, ok := config.setKeys[key]; !ok {
config.setKeys[key] = struct{}{}
}
}
config.setKeys["path"] = struct{}{}
d := DefaultConfig()
d.Merge(config)
config = d
return config, errs.ErrorOrNil()
}
// ConfigFromPath iterates and merges all configuration files in a given
// directory, returning the resulting config.
func ConfigFromPath(path string) (*Config, error) {
// Ensure the given filepath exists
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("config: missing file/folder: %s", path)
}
// Check if a file was given or a path to a directory
stat, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("config: error stating file: %s", err)
}
// Recursively parse directories, single load files
if stat.Mode().IsDir() {
// Ensure the given filepath has at least one config file
_, err := ioutil.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("config: error listing directory: %s", err)
}
// Create a blank config to merge off of
config := DefaultConfig()
// Potential bug: Walk does not follow symlinks!
err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
// If WalkFunc had an error, just return it
if err != nil {
return err
}
// Do nothing for directories
if info.IsDir() {
return nil
}
// Parse and merge the config
newConfig, err := ParseConfig(path)
if err != nil {
return err
}
config.Merge(newConfig)
return nil
})
if err != nil {
return nil, fmt.Errorf("config: walk error: %s", err)
}
return config, nil
} else if stat.Mode().IsRegular() {
return ParseConfig(path)
}
return nil, fmt.Errorf("config: unknown filetype: %q", stat.Mode().String())
}
// DefaultConfig returns the default configuration struct.
func DefaultConfig() *Config {
logLevel := os.Getenv("CONSUL_TEMPLATE_LOG")
if logLevel == "" {
logLevel = "WARN"
}
config := &Config{
Vault: &VaultConfig{
Renew: true,
SSL: &SSLConfig{
Enabled: true,
Verify: true,
},
},
Auth: &AuthConfig{
Enabled: false,
},
SSL: &SSLConfig{
Enabled: false,
Verify: true,
},
Syslog: &SyslogConfig{
Enabled: false,
Facility: "LOCAL0",
},
Deduplicate: &DeduplicateConfig{
Enabled: false,
Prefix: defaultDedupPrefix,
TTL: 15 * time.Second,
},
ConfigTemplates: make([]*ConfigTemplate, 0),
Retry: 5 * time.Second,
MaxStale: 1 * time.Second,
Wait: &watch.Wait{},
LogLevel: logLevel,
Reap: os.Getpid() == 1,
setKeys: make(map[string]struct{}),
}
if v := os.Getenv("CONSUL_HTTP_ADDR"); v != "" {
config.Consul = v
}
if v := os.Getenv("CONSUL_TOKEN"); v != "" {
config.Token = v
}
if v := os.Getenv("VAULT_ADDR"); v != "" {
config.Vault.Address = v
}
if v := os.Getenv("VAULT_CAPATH"); v != "" {
config.Vault.SSL.Cert = v
}
if v := os.Getenv("VAULT_CACERT"); v != "" {
config.Vault.SSL.CaCert = v
}
if v := os.Getenv("VAULT_SKIP_VERIFY"); v != "" {
config.Vault.SSL.Verify = false
}
return config
}
// AuthConfig is the HTTP basic authentication data.
type AuthConfig struct {
Enabled bool `json:"enabled" mapstructure:"enabled"`
Username string `json:"username" mapstructure:"username"`
Password string `json:"password" mapstructure:"password"`
}
// String is the string representation of this authentication. If authentication
// is not enabled, this returns the empty string. The username and password will
// be separated by a colon.
func (a *AuthConfig) String() string {
if !a.Enabled {
return ""
}
if a.Password != "" {
return fmt.Sprintf("%s:%s", a.Username, a.Password)
}
return a.Username
}
// DeduplicateConfig is used to enable the de-duplication mode, which depends
// on electing a leader per-template and watching of a key. This is used
// to reduce the cost of many instances of CT running the same template.
type DeduplicateConfig struct {
// Controls if deduplication mode is enabled
Enabled bool `json:"enabled" mapstructure:"enabled"`
// Controls the KV prefix used. Defaults to defaultDedupPrefix
Prefix string `json:"prefix" mapstructure:"prefix"`
// TTL is the Session TTL used for lock acquisition, defaults to 15 seconds.
TTL time.Duration `json:"ttl" mapstructure:"ttl"`
}
// SSLConfig is the configuration for SSL.
type SSLConfig struct {
Enabled bool `json:"enabled" mapstructure:"enabled"`
Verify bool `json:"verify" mapstructure:"verify"`
Cert string `json:"cert,omitempty" mapstructure:"cert"`
Key string `json:"key,omitempty" mapstructure:"key"`
CaCert string `json:"ca_cert,omitempty" mapstructure:"ca_cert"`
}
// SyslogConfig is the configuration for syslog.
type SyslogConfig struct {
Enabled bool `json:"enabled" mapstructure:"enabled"`
Facility string `json:"facility" mapstructure:"facility"`
}
// ConfigTemplate is the representation of an input template, output location,
// and optional command to execute when rendered
type ConfigTemplate struct {
Source string `json:"source" mapstructure:"source"`
Destination string `json:"destination" mapstructure:"destination"`
Command string `json:"command,omitempty" mapstructure:"command"`
Perms os.FileMode `json:"perms" mapstructure:"perms"`
Backup bool `json:"backup" mapstructure:"backup"`
}
// VaultConfig is the configuration for connecting to a vault server.
type VaultConfig struct {
Address string `json:"address,omitempty" mapstructure:"address"`
Token string `json:"-" mapstructure:"token"`
Renew bool `json:"renew" mapstructure:"renew"`
// SSL indicates we should use a secure connection while talking to Vault.
SSL *SSLConfig `json:"ssl" mapstructure:"ssl"`
}
// ParseConfigTemplate parses a string into a ConfigTemplate struct
func ParseConfigTemplate(s string) (*ConfigTemplate, error) {
if len(strings.TrimSpace(s)) < 1 {
return nil, errors.New("cannot specify empty template declaration")
}
var source, destination, command string
parts := configTemplateRe.FindAllString(s, -1)
switch len(parts) {
case 1:
source = parts[0]
case 2:
source, destination = parts[0], parts[1]
case 3:
source, destination, command = parts[0], parts[1], parts[2]
default:
return nil, errors.New("invalid template declaration format")
}
return &ConfigTemplate{
Source: source,
Destination: destination,
Command: command,
Perms: defaultFilePerms,
}, nil
}
// flattenKeys is a function that takes a map[string]interface{} and recursively
// flattens any keys that are a []map[string]interface{} where the key is in the
// given list of keys.
func flattenKeys(m map[string]interface{}, keys []string) {
keyMap := make(map[string]struct{})
for _, key := range keys {
keyMap[key] = struct{}{}
}
var flatten func(map[string]interface{})
flatten = func(m map[string]interface{}) {
for k, v := range m {
if _, ok := keyMap[k]; !ok {
continue
}
switch typed := v.(type) {
case []map[string]interface{}:
if len(typed) > 0 {
last := typed[len(typed)-1]
flatten(last)
m[k] = last
} else {
m[k] = nil
}
case map[string]interface{}:
flatten(typed)
m[k] = typed
default:
m[k] = v
}
}
}
flatten(m)
}