open-nomad/command/agent/config_parse.go
James Rasell d7b311ce55
acl: correctly resolve ACL roles within client cache. (#14922)
The client ACL cache was not accounting for tokens which included
ACL role links. This change modifies the behaviour to resolve role
links to policies. It will also now store ACL roles within the
cache for quick lookup. The cache TTL is configurable in the same
manner as policies or tokens.

Another small fix is included that takes into account the ACL
token expiry time. This was not included, which meant tokens with
expiry could be used past the expiry time, until they were GC'd.
2022-10-20 09:37:32 +02:00

289 lines
9.6 KiB
Go

package agent
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/hashicorp/hcl"
client "github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/structs/config"
)
// ParseConfigFile returns an agent.Config from parsed from a file.
func ParseConfigFile(path string) (*Config, error) {
// slurp
var buf bytes.Buffer
path, err := filepath.Abs(path)
if err != nil {
return nil, err
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
if _, err := io.Copy(&buf, f); err != nil {
return nil, err
}
// parse
c := &Config{
Client: &ClientConfig{
ServerJoin: &ServerJoin{},
TemplateConfig: &client.ClientTemplateConfig{
Wait: &client.WaitConfig{},
WaitBounds: &client.WaitConfig{},
ConsulRetry: &client.RetryConfig{},
VaultRetry: &client.RetryConfig{},
NomadRetry: &client.RetryConfig{},
},
},
Server: &ServerConfig{
PlanRejectionTracker: &PlanRejectionTracker{},
ServerJoin: &ServerJoin{},
},
ACL: &ACLConfig{},
Audit: &config.AuditConfig{},
Consul: &config.ConsulConfig{},
Autopilot: &config.AutopilotConfig{},
Telemetry: &Telemetry{},
Vault: &config.VaultConfig{},
}
err = hcl.Decode(c, buf.String())
if err != nil {
return nil, fmt.Errorf("failed to decode HCL file %s: %w", path, err)
}
// convert strings to time.Durations
tds := []durationConversionMap{
{"gc_interval", &c.Client.GCInterval, &c.Client.GCIntervalHCL, nil},
{"acl.token_ttl", &c.ACL.TokenTTL, &c.ACL.TokenTTLHCL, nil},
{"acl.policy_ttl", &c.ACL.PolicyTTL, &c.ACL.PolicyTTLHCL, nil},
{"acl.policy_ttl", &c.ACL.RoleTTL, &c.ACL.RoleTTLHCL, nil},
{"acl.token_min_expiration_ttl", &c.ACL.TokenMinExpirationTTL, &c.ACL.TokenMinExpirationTTLHCL, nil},
{"acl.token_max_expiration_ttl", &c.ACL.TokenMaxExpirationTTL, &c.ACL.TokenMaxExpirationTTLHCL, nil},
{"client.server_join.retry_interval", &c.Client.ServerJoin.RetryInterval, &c.Client.ServerJoin.RetryIntervalHCL, nil},
{"server.heartbeat_grace", &c.Server.HeartbeatGrace, &c.Server.HeartbeatGraceHCL, nil},
{"server.min_heartbeat_ttl", &c.Server.MinHeartbeatTTL, &c.Server.MinHeartbeatTTLHCL, nil},
{"server.failover_heartbeat_ttl", &c.Server.FailoverHeartbeatTTL, &c.Server.FailoverHeartbeatTTLHCL, nil},
{"server.plan_rejection_tracker.node_window", &c.Server.PlanRejectionTracker.NodeWindow, &c.Server.PlanRejectionTracker.NodeWindowHCL, nil},
{"server.retry_interval", &c.Server.RetryInterval, &c.Server.RetryIntervalHCL, nil},
{"server.server_join.retry_interval", &c.Server.ServerJoin.RetryInterval, &c.Server.ServerJoin.RetryIntervalHCL, nil},
{"consul.timeout", &c.Consul.Timeout, &c.Consul.TimeoutHCL, nil},
{"autopilot.server_stabilization_time", &c.Autopilot.ServerStabilizationTime, &c.Autopilot.ServerStabilizationTimeHCL, nil},
{"autopilot.last_contact_threshold", &c.Autopilot.LastContactThreshold, &c.Autopilot.LastContactThresholdHCL, nil},
{"telemetry.collection_interval", &c.Telemetry.collectionInterval, &c.Telemetry.CollectionInterval, nil},
{"client.template.block_query_wait", nil, &c.Client.TemplateConfig.BlockQueryWaitTimeHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.BlockQueryWaitTime = d
},
},
{"client.template.max_stale", nil, &c.Client.TemplateConfig.MaxStaleHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.MaxStale = d
}},
{"client.template.wait.min", nil, &c.Client.TemplateConfig.Wait.MinHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.Wait.Min = d
},
},
{"client.template.wait.max", nil, &c.Client.TemplateConfig.Wait.MaxHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.Wait.Max = d
},
},
{"client.template.wait_bounds.min", nil, &c.Client.TemplateConfig.WaitBounds.MinHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.WaitBounds.Min = d
},
},
{"client.template.wait_bounds.max", nil, &c.Client.TemplateConfig.WaitBounds.MaxHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.WaitBounds.Max = d
},
},
{"client.template.consul_retry.backoff", nil, &c.Client.TemplateConfig.ConsulRetry.BackoffHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.ConsulRetry.Backoff = d
},
},
{"client.template.consul_retry.max_backoff", nil, &c.Client.TemplateConfig.ConsulRetry.MaxBackoffHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.ConsulRetry.MaxBackoff = d
},
},
{"client.template.vault_retry.backoff", nil, &c.Client.TemplateConfig.VaultRetry.BackoffHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.VaultRetry.Backoff = d
},
},
{"client.template.vault_retry.max_backoff", nil, &c.Client.TemplateConfig.VaultRetry.MaxBackoffHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.VaultRetry.MaxBackoff = d
},
},
{"client.template.nomad_retry.backoff", nil, &c.Client.TemplateConfig.NomadRetry.BackoffHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.NomadRetry.Backoff = d
},
},
{"client.template.nomad_retry.max_backoff", nil, &c.Client.TemplateConfig.NomadRetry.MaxBackoffHCL,
func(d *time.Duration) {
c.Client.TemplateConfig.NomadRetry.MaxBackoff = d
},
},
}
// Add enterprise audit sinks for time.Duration parsing
for i, sink := range c.Audit.Sinks {
tds = append(tds, durationConversionMap{
fmt.Sprintf("audit.sink.%d", i), &sink.RotateDuration, &sink.RotateDurationHCL, nil})
}
// convert strings to time.Durations
err = convertDurations(tds)
if err != nil {
return nil, err
}
// report unexpected keys
err = extraKeys(c)
if err != nil {
return nil, err
}
// Set client template config or its members to nil if not set.
finalizeClientTemplateConfig(c)
return c, nil
}
// durationConversionMap holds args for one duration conversion
type durationConversionMap struct {
targetFieldPath string
targetField *time.Duration
sourceField *string
setFunc func(*time.Duration)
}
// convertDurations parses the duration strings specified in the config files
// into time.Durations
func convertDurations(xs []durationConversionMap) error {
for _, x := range xs {
// if targetField is not a pointer itself, use the field map.
if x.targetField != nil && x.sourceField != nil && "" != *x.sourceField {
d, err := time.ParseDuration(*x.sourceField)
if err != nil {
return fmt.Errorf("%s can't parse time duration %s", x.targetFieldPath, *x.sourceField)
}
*x.targetField = d
} else if x.setFunc != nil && x.sourceField != nil && "" != *x.sourceField {
// if targetField is a pointer itself, use the setFunc closure.
d, err := time.ParseDuration(*x.sourceField)
if err != nil {
return fmt.Errorf("%s can't parse time duration %s", x.targetFieldPath, *x.sourceField)
}
x.setFunc(&d)
}
}
return nil
}
func extraKeys(c *Config) error {
// hcl leaves behind extra keys when parsing JSON. These keys
// are kept on the top level, taken from slices or the keys of
// structs contained in slices. Clean up before looking for
// extra keys.
for range c.HTTPAPIResponseHeaders {
helper.RemoveEqualFold(&c.ExtraKeysHCL, "http_api_response_headers")
}
for _, p := range c.Plugins {
helper.RemoveEqualFold(&c.ExtraKeysHCL, p.Name)
helper.RemoveEqualFold(&c.ExtraKeysHCL, "config")
helper.RemoveEqualFold(&c.ExtraKeysHCL, "plugin")
}
for _, k := range []string{"options", "meta", "chroot_env", "servers", "server_join"} {
helper.RemoveEqualFold(&c.ExtraKeysHCL, k)
helper.RemoveEqualFold(&c.ExtraKeysHCL, "client")
}
// stats is an unused key, continue to silently ignore it
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, "stats")
// Remove HostVolume extra keys
for _, hv := range c.Client.HostVolumes {
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, hv.Name)
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, "host_volume")
}
// Remove HostNetwork extra keys
for _, hn := range c.Client.HostNetworks {
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, hn.Name)
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, "host_network")
}
// Remove AuditConfig extra keys
for _, f := range c.Audit.Filters {
helper.RemoveEqualFold(&c.Audit.ExtraKeysHCL, f.Name)
helper.RemoveEqualFold(&c.Audit.ExtraKeysHCL, "filter")
}
for _, s := range c.Audit.Sinks {
helper.RemoveEqualFold(&c.Audit.ExtraKeysHCL, s.Name)
helper.RemoveEqualFold(&c.Audit.ExtraKeysHCL, "sink")
}
for _, k := range []string{"enabled_schedulers", "start_join", "retry_join", "server_join"} {
helper.RemoveEqualFold(&c.ExtraKeysHCL, k)
helper.RemoveEqualFold(&c.ExtraKeysHCL, "server")
}
for _, k := range []string{"datadog_tags"} {
helper.RemoveEqualFold(&c.ExtraKeysHCL, k)
helper.RemoveEqualFold(&c.ExtraKeysHCL, "telemetry")
}
return helper.UnusedKeys(c)
}
// hcl.Decode will error if the ClientTemplateConfig isn't initialized with empty
// structs, however downstream code expect nils if the struct only contains fields
// with the zero value for its type. This function nils out type members that are
// structs where all the member fields are just the zero value for its type.
func finalizeClientTemplateConfig(config *Config) {
if config.Client.TemplateConfig.Wait.IsEmpty() {
config.Client.TemplateConfig.Wait = nil
}
if config.Client.TemplateConfig.WaitBounds.IsEmpty() {
config.Client.TemplateConfig.WaitBounds = nil
}
if config.Client.TemplateConfig.ConsulRetry.IsEmpty() {
config.Client.TemplateConfig.ConsulRetry = nil
}
if config.Client.TemplateConfig.VaultRetry.IsEmpty() {
config.Client.TemplateConfig.VaultRetry = nil
}
if config.Client.TemplateConfig.NomadRetry.IsEmpty() {
config.Client.TemplateConfig.NomadRetry = nil
}
if config.Client.TemplateConfig.IsEmpty() {
config.Client.TemplateConfig = nil
}
}