Skip to content

Commit

Permalink
config: fix client.template config merging with defaults (#20165)
Browse files Browse the repository at this point in the history
When loading the client configuration, the user-specified `client.template`
block was not properly merged with the default values. As a result, if the user
set any `client.template` field, all the other field defaulted to their zero
values instead of the documented defaults.

This changeset:
* Adds the missing `Merge` method for the client template config and ensures
  it's called.
* Makes a single source of truth for the default template configuration,
  instead of two different constructors.
* Extends the tests to cover the merge of a partial block better.

Fixes: #20164
  • Loading branch information
tgross committed Mar 20, 2024
1 parent f87ed2f commit 3d4cd91
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 89 deletions.
3 changes: 3 additions & 0 deletions .changelog/20165.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
template: Fixed a bug where a partial `client.template` block would cause defaults for unspecified fields to be ignored
```
102 changes: 75 additions & 27 deletions client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,28 @@ type ClientTemplateConfig struct {
NomadRetry *RetryConfig `hcl:"nomad_retry,optional"`
}

func DefaultTemplateConfig() *ClientTemplateConfig {
return &ClientTemplateConfig{
FunctionDenylist: DefaultTemplateFunctionDenylist,
DisableSandbox: false,
BlockQueryWaitTime: pointer.Of(5 * time.Minute), // match Consul default
MaxStale: pointer.Of(DefaultTemplateMaxStale), // match Consul default
Wait: &WaitConfig{
Min: pointer.Of(5 * time.Second),
Max: pointer.Of(4 * time.Minute),
},
ConsulRetry: &RetryConfig{
Attempts: pointer.Of(0), // unlimited
},
VaultRetry: &RetryConfig{
Attempts: pointer.Of(0), // unlimited
},
NomadRetry: &RetryConfig{
Attempts: pointer.Of(0), // unlimited
},
}
}

// Copy returns a deep copy of a ClientTemplateConfig
func (c *ClientTemplateConfig) Copy() *ClientTemplateConfig {
if c == nil {
Expand Down Expand Up @@ -468,6 +490,50 @@ func (c *ClientTemplateConfig) IsEmpty() bool {
c.NomadRetry.IsEmpty()
}

func (c *ClientTemplateConfig) Merge(o *ClientTemplateConfig) *ClientTemplateConfig {
if c == nil {
return o
}

result := *c
if o == nil {
return &result
}

if o.FunctionDenylist != nil {
result.FunctionDenylist = slices.Clone(o.FunctionDenylist)
}
if o.FunctionBlacklist != nil {
result.FunctionBlacklist = slices.Clone(o.FunctionBlacklist)
}

if o.DisableSandbox {
result.DisableSandbox = true
}

result.MaxStale = pointer.Merge(result.MaxStale, o.MaxStale)
result.BlockQueryWaitTime = pointer.Merge(result.BlockQueryWaitTime, o.BlockQueryWaitTime)

if o.Wait != nil {
result.Wait = c.Wait.Merge(o.Wait)
}
if o.WaitBounds != nil {
result.WaitBounds = c.WaitBounds.Merge(o.WaitBounds)
}

if o.ConsulRetry != nil {
result.ConsulRetry = c.ConsulRetry.Merge(o.ConsulRetry)
}
if o.VaultRetry != nil {
result.VaultRetry = c.VaultRetry.Merge(o.VaultRetry)
}
if o.NomadRetry != nil {
result.NomadRetry = c.NomadRetry.Merge(o.NomadRetry)
}

return &result
}

// WaitConfig is mirrored from templateconfig.WaitConfig because we need to handle
// the HCL conversion which happens in agent.ParseConfigFile
// NOTE: Since Consul Template requires pointers, this type uses pointers to fields
Expand Down Expand Up @@ -770,33 +836,15 @@ func DefaultConfig() *Config {
GCMaxAllocs: 50,
NoHostUUID: true,
DisableRemoteExec: false,
TemplateConfig: &ClientTemplateConfig{
FunctionDenylist: DefaultTemplateFunctionDenylist,
DisableSandbox: false,
BlockQueryWaitTime: pointer.Of(5 * time.Minute), // match Consul default
MaxStale: pointer.Of(DefaultTemplateMaxStale), // match Consul default
Wait: &WaitConfig{
Min: pointer.Of(5 * time.Second),
Max: pointer.Of(4 * time.Minute),
},
ConsulRetry: &RetryConfig{
Attempts: pointer.Of(0), // unlimited
},
VaultRetry: &RetryConfig{
Attempts: pointer.Of(0), // unlimited
},
NomadRetry: &RetryConfig{
Attempts: pointer.Of(0), // unlimited
},
},
RPCHoldTimeout: 5 * time.Second,
CNIPath: "/opt/cni/bin",
CNIConfigDir: "/opt/cni/config",
CNIInterfacePrefix: "eth",
HostNetworks: map[string]*structs.ClientHostNetworkConfig{},
CgroupParent: cgutil.GetCgroupParent(""),
MaxDynamicPort: structs.DefaultMinDynamicPort,
MinDynamicPort: structs.DefaultMaxDynamicPort,
TemplateConfig: DefaultTemplateConfig(),
RPCHoldTimeout: 5 * time.Second,
CNIPath: "/opt/cni/bin",
CNIConfigDir: "/opt/cni/config",
CNIInterfacePrefix: "eth",
HostNetworks: map[string]*structs.ClientHostNetworkConfig{},
CgroupParent: cgutil.GetCgroupParent(""),
MaxDynamicPort: structs.DefaultMinDynamicPort,
MinDynamicPort: structs.DefaultMaxDynamicPort,
}
}

Expand Down
2 changes: 1 addition & 1 deletion command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
conf.DisableRemoteExec = agentConfig.Client.DisableRemoteExec

if agentConfig.Client.TemplateConfig != nil {
conf.TemplateConfig = agentConfig.Client.TemplateConfig.Copy()
conf.TemplateConfig = conf.TemplateConfig.Merge(agentConfig.Client.TemplateConfig)
}

hvMap := make(map[string]*structs.ClientHostVolumeConfig, len(agentConfig.Client.HostVolumes))
Expand Down
11 changes: 2 additions & 9 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1230,10 +1230,6 @@ func DevConfig(mode *devModeConfig) *Config {
conf.Client.GCDiskUsageThreshold = 99
conf.Client.GCInodeUsageThreshold = 99
conf.Client.GCMaxAllocs = 50
conf.Client.TemplateConfig = &client.ClientTemplateConfig{
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
}
conf.Client.Options[fingerprint.TightenNetworkTimeoutsConfig] = "true"
conf.Client.BindWildcardDefaultHostNetwork = true
conf.Client.NomadServiceDiscovery = pointer.Of(true)
Expand Down Expand Up @@ -1281,10 +1277,7 @@ func DefaultConfig() *Config {
RetryInterval: 30 * time.Second,
RetryMaxAttempts: 0,
},
TemplateConfig: &client.ClientTemplateConfig{
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
},
TemplateConfig: client.DefaultTemplateConfig(),
BindWildcardDefaultHostNetwork: true,
CNIPath: "/opt/cni/bin",
CNIConfigDir: "/opt/cni/config",
Expand Down Expand Up @@ -2117,7 +2110,7 @@ func (a *ClientConfig) Merge(b *ClientConfig) *ClientConfig {
}

if b.TemplateConfig != nil {
result.TemplateConfig = b.TemplateConfig
result.TemplateConfig = result.TemplateConfig.Merge(b.TemplateConfig)
}

// Add the servers
Expand Down
178 changes: 126 additions & 52 deletions command/agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,17 @@ func TestConfig_Merge(t *testing.T) {
MaxKillTimeout: "50s",
DisableRemoteExec: false,
TemplateConfig: &client.ClientTemplateConfig{
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
BlockQueryWaitTime: pointer.Of(5 * time.Minute),
MaxStale: pointer.Of(client.DefaultTemplateMaxStale),
Wait: &client.WaitConfig{
Min: pointer.Of(5 * time.Second),
Max: pointer.Of(4 * time.Minute),
},
ConsulRetry: &client.RetryConfig{Attempts: pointer.Of(0)},
VaultRetry: &client.RetryConfig{Attempts: pointer.Of(0)},
NomadRetry: &client.RetryConfig{Attempts: pointer.Of(0)},
},
Reserved: &Resources{
CPU: 15,
Expand Down Expand Up @@ -1421,56 +1430,121 @@ func TestEventBroker_Parse(t *testing.T) {
func TestConfig_LoadConsulTemplateConfig(t *testing.T) {
ci.Parallel(t)

defaultConfig := DefaultConfig()
// Test that loading without template config didn't create load errors
agentConfig, err := LoadConfig("test-resources/minimal_client.hcl")
require.NoError(t, err)

// Test loading with this config didn't create load errors
agentConfig, err = LoadConfig("test-resources/client_with_template.hcl")
require.NoError(t, err)

agentConfig = defaultConfig.Merge(agentConfig)

clientAgent := Agent{config: agentConfig}
clientConfig, err := clientAgent.clientConfig()
require.NoError(t, err)

templateConfig := clientConfig.TemplateConfig

// Make sure all fields to test are set
require.NotNil(t, templateConfig.BlockQueryWaitTime)
require.NotNil(t, templateConfig.MaxStale)
require.NotNil(t, templateConfig.Wait)
require.NotNil(t, templateConfig.WaitBounds)
require.NotNil(t, templateConfig.ConsulRetry)
require.NotNil(t, templateConfig.VaultRetry)
require.NotNil(t, templateConfig.NomadRetry)

// Direct properties
require.Equal(t, 300*time.Second, *templateConfig.MaxStale)
require.Equal(t, 90*time.Second, *templateConfig.BlockQueryWaitTime)
// Wait
require.Equal(t, 2*time.Second, *templateConfig.Wait.Min)
require.Equal(t, 60*time.Second, *templateConfig.Wait.Max)
// WaitBounds
require.Equal(t, 2*time.Second, *templateConfig.WaitBounds.Min)
require.Equal(t, 60*time.Second, *templateConfig.WaitBounds.Max)
// Consul Retry
require.NotNil(t, templateConfig.ConsulRetry)
require.Equal(t, 5, *templateConfig.ConsulRetry.Attempts)
require.Equal(t, 5*time.Second, *templateConfig.ConsulRetry.Backoff)
require.Equal(t, 10*time.Second, *templateConfig.ConsulRetry.MaxBackoff)
// Vault Retry
require.NotNil(t, templateConfig.VaultRetry)
require.Equal(t, 10, *templateConfig.VaultRetry.Attempts)
require.Equal(t, 15*time.Second, *templateConfig.VaultRetry.Backoff)
require.Equal(t, 20*time.Second, *templateConfig.VaultRetry.MaxBackoff)
// Nomad Retry
require.NotNil(t, templateConfig.NomadRetry)
require.Equal(t, 15, *templateConfig.NomadRetry.Attempts)
require.Equal(t, 20*time.Second, *templateConfig.NomadRetry.Backoff)
require.Equal(t, 25*time.Second, *templateConfig.NomadRetry.MaxBackoff)
t.Run("minimal client expect defaults", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig("test-resources/minimal_client.hcl")
must.NoError(t, err)

Check failure on line 1436 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
agentConfig = defaultConfig.Merge(agentConfig)
must.Eq(t, defaultConfig.Client.TemplateConfig, agentConfig.Client.TemplateConfig)

Check failure on line 1438 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
})

t.Run("client config with nil function denylist", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig("test-resources/client_with_function_denylist_nil.hcl")
must.NoError(t, err)

Check failure on line 1444 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
agentConfig = defaultConfig.Merge(agentConfig)

templateConfig := agentConfig.Client.TemplateConfig
must.Len(t, 2, templateConfig.FunctionDenylist)

Check failure on line 1448 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
})

t.Run("client config with basic template", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig("test-resources/client_with_basic_template.hcl")
must.NoError(t, err)

Check failure on line 1454 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
agentConfig = defaultConfig.Merge(agentConfig)

templateConfig := agentConfig.Client.TemplateConfig

// check explicit overrides
must.Eq(t, true, templateConfig.DisableSandbox)

Check failure on line 1460 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
must.Len(t, 0, templateConfig.FunctionDenylist)

Check failure on line 1461 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must

// check all the complex defaults
must.Eq(t, 87600*time.Hour, *templateConfig.MaxStale)

Check failure on line 1464 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
must.Eq(t, 5*time.Minute, *templateConfig.BlockQueryWaitTime)

Check failure on line 1465 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must

// Wait
must.NotNil(t, templateConfig.Wait)

Check failure on line 1468 in command/agent/config_test.go

View workflow job for this annotation

GitHub Actions / tests-groups (quick)

undefined: must
must.Eq(t, 5*time.Second, *templateConfig.Wait.Min)
must.Eq(t, 4*time.Minute, *templateConfig.Wait.Max)

// WaitBounds
must.Nil(t, templateConfig.WaitBounds)

// Consul Retry
must.NotNil(t, templateConfig.ConsulRetry)
must.Eq(t, 0, *templateConfig.ConsulRetry.Attempts)
must.Nil(t, templateConfig.ConsulRetry.Backoff)
must.Nil(t, templateConfig.ConsulRetry.MaxBackoff)

// Vault Retry
must.NotNil(t, templateConfig.VaultRetry)
must.Eq(t, 0, *templateConfig.VaultRetry.Attempts)
must.Nil(t, templateConfig.VaultRetry.Backoff)
must.Nil(t, templateConfig.VaultRetry.MaxBackoff)

// Nomad Retry
must.NotNil(t, templateConfig.NomadRetry)
must.Eq(t, 0, *templateConfig.NomadRetry.Attempts)
must.Nil(t, templateConfig.NomadRetry.Backoff)
must.Nil(t, templateConfig.NomadRetry.MaxBackoff)
})

t.Run("client config with full template block", func(t *testing.T) {
defaultConfig := DefaultConfig()

agentConfig, err := LoadConfig("test-resources/client_with_template.hcl")
must.NoError(t, err)

agentConfig = defaultConfig.Merge(agentConfig)

clientAgent := Agent{config: agentConfig}
clientConfig, err := clientAgent.clientConfig()
must.NoError(t, err)

templateConfig := clientConfig.TemplateConfig

// Make sure all fields to test are set
must.NotNil(t, templateConfig.BlockQueryWaitTime)
must.NotNil(t, templateConfig.MaxStale)
must.NotNil(t, templateConfig.Wait)
must.NotNil(t, templateConfig.WaitBounds)
must.NotNil(t, templateConfig.ConsulRetry)
must.NotNil(t, templateConfig.VaultRetry)
must.NotNil(t, templateConfig.NomadRetry)

// Direct properties
must.Eq(t, 300*time.Second, *templateConfig.MaxStale)
must.Eq(t, 90*time.Second, *templateConfig.BlockQueryWaitTime)

// Wait
must.Eq(t, 2*time.Second, *templateConfig.Wait.Min)
must.Eq(t, 60*time.Second, *templateConfig.Wait.Max)

// WaitBounds
must.Eq(t, 2*time.Second, *templateConfig.WaitBounds.Min)
must.Eq(t, 60*time.Second, *templateConfig.WaitBounds.Max)

// Consul Retry
must.NotNil(t, templateConfig.ConsulRetry)
must.Eq(t, 5, *templateConfig.ConsulRetry.Attempts)
must.Eq(t, 5*time.Second, *templateConfig.ConsulRetry.Backoff)
must.Eq(t, 10*time.Second, *templateConfig.ConsulRetry.MaxBackoff)

// Vault Retry
must.NotNil(t, templateConfig.VaultRetry)
must.Eq(t, 10, *templateConfig.VaultRetry.Attempts)
must.Eq(t, 15*time.Second, *templateConfig.VaultRetry.Backoff)
must.Eq(t, 20*time.Second, *templateConfig.VaultRetry.MaxBackoff)

// Nomad Retry
must.NotNil(t, templateConfig.NomadRetry)
must.Eq(t, 15, *templateConfig.NomadRetry.Attempts)
must.Eq(t, 20*time.Second, *templateConfig.NomadRetry.Backoff)
must.Eq(t, 25*time.Second, *templateConfig.NomadRetry.MaxBackoff)
})

}

func TestConfig_LoadConsulTemplate_FunctionDenylist(t *testing.T) {
Expand Down

0 comments on commit 3d4cd91

Please sign in to comment.