vault: detect namespace change in config reload (#14298)
The `namespace` field was not included in the equality check between old and new Vault configurations, which meant that a Vault config change that only changed the namespace would not be detected as a change and the clients would not be reloaded. Also, the comparison for boolean fields such as `enabled` and `allow_unauthenticated` was on the pointer and not the value of that pointer, which results in spurious reloads in real config reload that is easily missed in typical test scenarios. Includes a minor refactor of the order of fields for `Copy` and `Merge` to match the struct fields in hopes it makes it harder to make this mistake in the future, as well as additional test coverage.
This commit is contained in:
parent
6398548ebd
commit
c732b215f0
|
@ -0,0 +1,7 @@
|
||||||
|
```release-note:bug
|
||||||
|
vault: Fixed a bug where changing the Vault configuration `namespace` field was not detected as a change during server configuration reload.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:bug
|
||||||
|
vault: Fixed a bug where Vault clients were recreated when the server configuration was reloaded, even if there were no changes to the Vault configuration.
|
||||||
|
```
|
|
@ -1085,6 +1085,39 @@ func TestServer_Reload_TLS_DowngradeFromTLS(t *testing.T) {
|
||||||
assert.True(agent.config.TLSConfig.IsEmpty())
|
assert.True(agent.config.TLSConfig.IsEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_Reload_VaultConfig(t *testing.T) {
|
||||||
|
ci.Parallel(t)
|
||||||
|
|
||||||
|
agent := NewTestAgent(t, t.Name(), func(c *Config) {
|
||||||
|
c.Server.NumSchedulers = pointer.Of(0)
|
||||||
|
c.Vault = &config.VaultConfig{
|
||||||
|
Enabled: pointer.Of(true),
|
||||||
|
Token: "vault-token",
|
||||||
|
Namespace: "vault-namespace",
|
||||||
|
Addr: "https://vault.consul:8200",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defer agent.Shutdown()
|
||||||
|
|
||||||
|
newConfig := agent.GetConfig().Copy()
|
||||||
|
newConfig.Vault = &config.VaultConfig{
|
||||||
|
Enabled: pointer.Of(true),
|
||||||
|
Token: "vault-token",
|
||||||
|
Namespace: "another-namespace",
|
||||||
|
Addr: "https://vault.consul:8200",
|
||||||
|
}
|
||||||
|
|
||||||
|
sconf, err := convertServerConfig(newConfig)
|
||||||
|
must.NoError(t, err)
|
||||||
|
agent.finalizeServerConfig(sconf)
|
||||||
|
|
||||||
|
// TODO: the vault client isn't accessible here, and we don't actually
|
||||||
|
// overwrite the agent's server config on reload. We probably should? See
|
||||||
|
// tests in nomad/server_test.go for verification of this code path's
|
||||||
|
// behavior on the VaultClient
|
||||||
|
must.NoError(t, agent.server.Reload(sconf))
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_ShouldReload_ReturnFalseForNoChanges(t *testing.T) {
|
func TestServer_ShouldReload_ReturnFalseForNoChanges(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
|
@ -214,6 +214,7 @@ func TestServer_Reload_Vault(t *testing.T) {
|
||||||
config := DefaultConfig()
|
config := DefaultConfig()
|
||||||
config.VaultConfig.Enabled = &tr
|
config.VaultConfig.Enabled = &tr
|
||||||
config.VaultConfig.Token = uuid.Generate()
|
config.VaultConfig.Token = uuid.Generate()
|
||||||
|
config.VaultConfig.Namespace = "nondefault"
|
||||||
|
|
||||||
if err := s1.Reload(config); err != nil {
|
if err := s1.Reload(config); err != nil {
|
||||||
t.Fatalf("Reload failed: %v", err)
|
t.Fatalf("Reload failed: %v", err)
|
||||||
|
@ -222,6 +223,10 @@ func TestServer_Reload_Vault(t *testing.T) {
|
||||||
if !s1.vault.Running() {
|
if !s1.vault.Running() {
|
||||||
t.Fatalf("Vault client should be running")
|
t.Fatalf("Vault client should be running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s1.vault.GetConfig().Namespace != "nondefault" {
|
||||||
|
t.Fatalf("Vault client did not get new namespace")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectionReset(msg string) bool {
|
func connectionReset(msg string) bool {
|
||||||
|
|
|
@ -106,14 +106,20 @@ func (c *VaultConfig) AllowsUnauthenticated() bool {
|
||||||
func (c *VaultConfig) Merge(b *VaultConfig) *VaultConfig {
|
func (c *VaultConfig) Merge(b *VaultConfig) *VaultConfig {
|
||||||
result := *c
|
result := *c
|
||||||
|
|
||||||
|
if b.Enabled != nil {
|
||||||
|
result.Enabled = b.Enabled
|
||||||
|
}
|
||||||
if b.Token != "" {
|
if b.Token != "" {
|
||||||
result.Token = b.Token
|
result.Token = b.Token
|
||||||
}
|
}
|
||||||
|
if b.Role != "" {
|
||||||
|
result.Role = b.Role
|
||||||
|
}
|
||||||
if b.Namespace != "" {
|
if b.Namespace != "" {
|
||||||
result.Namespace = b.Namespace
|
result.Namespace = b.Namespace
|
||||||
}
|
}
|
||||||
if b.Role != "" {
|
if b.AllowUnauthenticated != nil {
|
||||||
result.Role = b.Role
|
result.AllowUnauthenticated = b.AllowUnauthenticated
|
||||||
}
|
}
|
||||||
if b.TaskTokenTTL != "" {
|
if b.TaskTokenTTL != "" {
|
||||||
result.TaskTokenTTL = b.TaskTokenTTL
|
result.TaskTokenTTL = b.TaskTokenTTL
|
||||||
|
@ -136,17 +142,11 @@ func (c *VaultConfig) Merge(b *VaultConfig) *VaultConfig {
|
||||||
if b.TLSKeyFile != "" {
|
if b.TLSKeyFile != "" {
|
||||||
result.TLSKeyFile = b.TLSKeyFile
|
result.TLSKeyFile = b.TLSKeyFile
|
||||||
}
|
}
|
||||||
if b.TLSServerName != "" {
|
|
||||||
result.TLSServerName = b.TLSServerName
|
|
||||||
}
|
|
||||||
if b.AllowUnauthenticated != nil {
|
|
||||||
result.AllowUnauthenticated = b.AllowUnauthenticated
|
|
||||||
}
|
|
||||||
if b.TLSSkipVerify != nil {
|
if b.TLSSkipVerify != nil {
|
||||||
result.TLSSkipVerify = b.TLSSkipVerify
|
result.TLSSkipVerify = b.TLSSkipVerify
|
||||||
}
|
}
|
||||||
if b.Enabled != nil {
|
if b.TLSServerName != "" {
|
||||||
result.Enabled = b.Enabled
|
result.TLSServerName = b.TLSServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result
|
return &result
|
||||||
|
@ -188,9 +188,9 @@ func (c *VaultConfig) Copy() *VaultConfig {
|
||||||
return nc
|
return nc
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEqual compares two Vault configurations and returns a boolean indicating
|
// Equals compares two Vault configurations and returns a boolean indicating
|
||||||
// if they are equal.
|
// if they are equal.
|
||||||
func (c *VaultConfig) IsEqual(b *VaultConfig) bool {
|
func (c *VaultConfig) Equals(b *VaultConfig) bool {
|
||||||
if c == nil && b != nil {
|
if c == nil && b != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -198,12 +198,32 @@ func (c *VaultConfig) IsEqual(b *VaultConfig) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Enabled == nil || b.Enabled == nil {
|
||||||
|
if c.Enabled != b.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if *c.Enabled != *b.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if c.Token != b.Token {
|
if c.Token != b.Token {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if c.Role != b.Role {
|
if c.Role != b.Role {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if c.Namespace != b.Namespace {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AllowUnauthenticated == nil || b.AllowUnauthenticated == nil {
|
||||||
|
if c.AllowUnauthenticated != b.AllowUnauthenticated {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if *c.AllowUnauthenticated != *b.AllowUnauthenticated {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if c.TaskTokenTTL != b.TaskTokenTTL {
|
if c.TaskTokenTTL != b.TaskTokenTTL {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -225,17 +245,18 @@ func (c *VaultConfig) IsEqual(b *VaultConfig) bool {
|
||||||
if c.TLSKeyFile != b.TLSKeyFile {
|
if c.TLSKeyFile != b.TLSKeyFile {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.TLSSkipVerify == nil || b.TLSSkipVerify == nil {
|
||||||
|
if c.TLSSkipVerify != b.TLSSkipVerify {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if *c.TLSSkipVerify != *b.TLSSkipVerify {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if c.TLSServerName != b.TLSServerName {
|
if c.TLSServerName != b.TLSServerName {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if c.AllowUnauthenticated != b.AllowUnauthenticated {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if c.TLSSkipVerify != b.TLSSkipVerify {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if c.Enabled != b.Enabled {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,35 +3,36 @@ package config
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/nomad/ci"
|
"github.com/hashicorp/nomad/ci"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/hashicorp/nomad/helper/pointer"
|
||||||
|
"github.com/shoenig/test/must"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestVaultConfig_Merge(t *testing.T) {
|
func TestVaultConfig_Merge(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
trueValue, falseValue := true, false
|
|
||||||
c1 := &VaultConfig{
|
c1 := &VaultConfig{
|
||||||
Enabled: &falseValue,
|
Enabled: pointer.Of(false),
|
||||||
Token: "1",
|
Token: "1",
|
||||||
Role: "1",
|
Role: "1",
|
||||||
AllowUnauthenticated: &trueValue,
|
AllowUnauthenticated: pointer.Of(true),
|
||||||
TaskTokenTTL: "1",
|
TaskTokenTTL: "1",
|
||||||
Addr: "1",
|
Addr: "1",
|
||||||
TLSCaFile: "1",
|
TLSCaFile: "1",
|
||||||
TLSCaPath: "1",
|
TLSCaPath: "1",
|
||||||
TLSCertFile: "1",
|
TLSCertFile: "1",
|
||||||
TLSKeyFile: "1",
|
TLSKeyFile: "1",
|
||||||
TLSSkipVerify: &trueValue,
|
TLSSkipVerify: pointer.Of(true),
|
||||||
TLSServerName: "1",
|
TLSServerName: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
c2 := &VaultConfig{
|
c2 := &VaultConfig{
|
||||||
Enabled: &trueValue,
|
Enabled: pointer.Of(true),
|
||||||
Token: "2",
|
Token: "2",
|
||||||
Role: "2",
|
Role: "2",
|
||||||
AllowUnauthenticated: &falseValue,
|
AllowUnauthenticated: pointer.Of(false),
|
||||||
TaskTokenTTL: "2",
|
TaskTokenTTL: "2",
|
||||||
Addr: "2",
|
Addr: "2",
|
||||||
TLSCaFile: "2",
|
TLSCaFile: "2",
|
||||||
|
@ -43,17 +44,17 @@ func TestVaultConfig_Merge(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
e := &VaultConfig{
|
e := &VaultConfig{
|
||||||
Enabled: &trueValue,
|
Enabled: pointer.Of(true),
|
||||||
Token: "2",
|
Token: "2",
|
||||||
Role: "2",
|
Role: "2",
|
||||||
AllowUnauthenticated: &falseValue,
|
AllowUnauthenticated: pointer.Of(false),
|
||||||
TaskTokenTTL: "2",
|
TaskTokenTTL: "2",
|
||||||
Addr: "2",
|
Addr: "2",
|
||||||
TLSCaFile: "2",
|
TLSCaFile: "2",
|
||||||
TLSCaPath: "2",
|
TLSCaPath: "2",
|
||||||
TLSCertFile: "2",
|
TLSCertFile: "2",
|
||||||
TLSKeyFile: "2",
|
TLSKeyFile: "2",
|
||||||
TLSSkipVerify: &trueValue,
|
TLSSkipVerify: pointer.Of(true),
|
||||||
TLSServerName: "2",
|
TLSServerName: "2",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,72 +64,78 @@ func TestVaultConfig_Merge(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVaultConfig_IsEqual(t *testing.T) {
|
func TestVaultConfig_Equals(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
trueValue, falseValue := true, false
|
|
||||||
c1 := &VaultConfig{
|
c1 := &VaultConfig{
|
||||||
Enabled: &falseValue,
|
Enabled: pointer.Of(false),
|
||||||
Token: "1",
|
Token: "1",
|
||||||
Role: "1",
|
Role: "1",
|
||||||
AllowUnauthenticated: &trueValue,
|
Namespace: "1",
|
||||||
|
AllowUnauthenticated: pointer.Of(true),
|
||||||
TaskTokenTTL: "1",
|
TaskTokenTTL: "1",
|
||||||
Addr: "1",
|
Addr: "1",
|
||||||
|
ConnectionRetryIntv: time.Second,
|
||||||
TLSCaFile: "1",
|
TLSCaFile: "1",
|
||||||
TLSCaPath: "1",
|
TLSCaPath: "1",
|
||||||
TLSCertFile: "1",
|
TLSCertFile: "1",
|
||||||
TLSKeyFile: "1",
|
TLSKeyFile: "1",
|
||||||
TLSSkipVerify: &trueValue,
|
TLSSkipVerify: pointer.Of(true),
|
||||||
TLSServerName: "1",
|
TLSServerName: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
c2 := &VaultConfig{
|
c2 := &VaultConfig{
|
||||||
Enabled: &falseValue,
|
Enabled: pointer.Of(false),
|
||||||
Token: "1",
|
Token: "1",
|
||||||
Role: "1",
|
Role: "1",
|
||||||
AllowUnauthenticated: &trueValue,
|
Namespace: "1",
|
||||||
|
AllowUnauthenticated: pointer.Of(true),
|
||||||
TaskTokenTTL: "1",
|
TaskTokenTTL: "1",
|
||||||
Addr: "1",
|
Addr: "1",
|
||||||
|
ConnectionRetryIntv: time.Second,
|
||||||
TLSCaFile: "1",
|
TLSCaFile: "1",
|
||||||
TLSCaPath: "1",
|
TLSCaPath: "1",
|
||||||
TLSCertFile: "1",
|
TLSCertFile: "1",
|
||||||
TLSKeyFile: "1",
|
TLSKeyFile: "1",
|
||||||
TLSSkipVerify: &trueValue,
|
TLSSkipVerify: pointer.Of(true),
|
||||||
TLSServerName: "1",
|
TLSServerName: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
require.True(c1.IsEqual(c2))
|
must.Equals(t, c1, c2)
|
||||||
|
|
||||||
c3 := &VaultConfig{
|
c3 := &VaultConfig{
|
||||||
Enabled: &trueValue,
|
Enabled: pointer.Of(true),
|
||||||
Token: "1",
|
Token: "1",
|
||||||
Role: "1",
|
Role: "1",
|
||||||
AllowUnauthenticated: &trueValue,
|
Namespace: "1",
|
||||||
|
AllowUnauthenticated: pointer.Of(true),
|
||||||
TaskTokenTTL: "1",
|
TaskTokenTTL: "1",
|
||||||
Addr: "1",
|
Addr: "1",
|
||||||
|
ConnectionRetryIntv: time.Second,
|
||||||
TLSCaFile: "1",
|
TLSCaFile: "1",
|
||||||
TLSCaPath: "1",
|
TLSCaPath: "1",
|
||||||
TLSCertFile: "1",
|
TLSCertFile: "1",
|
||||||
TLSKeyFile: "1",
|
TLSKeyFile: "1",
|
||||||
TLSSkipVerify: &trueValue,
|
TLSSkipVerify: pointer.Of(true),
|
||||||
TLSServerName: "1",
|
TLSServerName: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
c4 := &VaultConfig{
|
c4 := &VaultConfig{
|
||||||
Enabled: &falseValue,
|
Enabled: pointer.Of(false),
|
||||||
Token: "1",
|
Token: "1",
|
||||||
Role: "1",
|
Role: "1",
|
||||||
AllowUnauthenticated: &trueValue,
|
Namespace: "1",
|
||||||
|
AllowUnauthenticated: pointer.Of(true),
|
||||||
TaskTokenTTL: "1",
|
TaskTokenTTL: "1",
|
||||||
Addr: "1",
|
Addr: "1",
|
||||||
|
ConnectionRetryIntv: time.Second,
|
||||||
TLSCaFile: "1",
|
TLSCaFile: "1",
|
||||||
TLSCaPath: "1",
|
TLSCaPath: "1",
|
||||||
TLSCertFile: "1",
|
TLSCertFile: "1",
|
||||||
TLSKeyFile: "1",
|
TLSKeyFile: "1",
|
||||||
TLSSkipVerify: &trueValue,
|
TLSSkipVerify: pointer.Of(true),
|
||||||
TLSServerName: "1",
|
TLSServerName: "1",
|
||||||
}
|
}
|
||||||
require.False(c3.IsEqual(c4))
|
|
||||||
|
must.NotEquals(t, c3, c4)
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,10 @@ type VaultClient interface {
|
||||||
// SetConfig updates the config used by the Vault client
|
// SetConfig updates the config used by the Vault client
|
||||||
SetConfig(config *config.VaultConfig) error
|
SetConfig(config *config.VaultConfig) error
|
||||||
|
|
||||||
|
// GetConfig returns a copy of the config used by the Vault client, for
|
||||||
|
// testing
|
||||||
|
GetConfig() *config.VaultConfig
|
||||||
|
|
||||||
// CreateToken takes an allocation and task and returns an appropriate Vault
|
// CreateToken takes an allocation and task and returns an appropriate Vault
|
||||||
// Secret
|
// Secret
|
||||||
CreateToken(ctx context.Context, a *structs.Allocation, task string) (*vapi.Secret, error)
|
CreateToken(ctx context.Context, a *structs.Allocation, task string) (*vapi.Secret, error)
|
||||||
|
@ -350,6 +354,13 @@ func (v *vaultClient) flush() {
|
||||||
v.tomb = &tomb.Tomb{}
|
v.tomb = &tomb.Tomb{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConfig returns a copy of this vault client's configuration, for testing.
|
||||||
|
func (v *vaultClient) GetConfig() *config.VaultConfig {
|
||||||
|
v.setConfigLock.Lock()
|
||||||
|
defer v.setConfigLock.Unlock()
|
||||||
|
return v.config.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
// SetConfig is used to update the Vault config being used. A temporary outage
|
// SetConfig is used to update the Vault config being used. A temporary outage
|
||||||
// may occur after calling as it re-establishes a connection to Vault
|
// may occur after calling as it re-establishes a connection to Vault
|
||||||
func (v *vaultClient) SetConfig(config *config.VaultConfig) error {
|
func (v *vaultClient) SetConfig(config *config.VaultConfig) error {
|
||||||
|
@ -363,7 +374,7 @@ func (v *vaultClient) SetConfig(config *config.VaultConfig) error {
|
||||||
defer v.l.Unlock()
|
defer v.l.Unlock()
|
||||||
|
|
||||||
// If reloading the same config, no-op
|
// If reloading the same config, no-op
|
||||||
if v.config.IsEqual(config) {
|
if v.config.Equals(config) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,6 +142,7 @@ func (v *TestVaultClient) MarkForRevocation(accessors []*structs.VaultAccessor)
|
||||||
|
|
||||||
func (v *TestVaultClient) Stop() {}
|
func (v *TestVaultClient) Stop() {}
|
||||||
func (v *TestVaultClient) SetActive(enabled bool) {}
|
func (v *TestVaultClient) SetActive(enabled bool) {}
|
||||||
|
func (v *TestVaultClient) GetConfig() *config.VaultConfig { return nil }
|
||||||
func (v *TestVaultClient) SetConfig(config *config.VaultConfig) error { return nil }
|
func (v *TestVaultClient) SetConfig(config *config.VaultConfig) error { return nil }
|
||||||
func (v *TestVaultClient) Running() bool { return true }
|
func (v *TestVaultClient) Running() bool { return true }
|
||||||
func (v *TestVaultClient) Stats() map[string]string { return map[string]string{} }
|
func (v *TestVaultClient) Stats() map[string]string { return map[string]string{} }
|
||||||
|
|
Loading…
Reference in New Issue