Merge pull request #9456 from hashicorp/dnephin/config-deprecation

config: Use DeprecatedConfig struct for deprecated config fields
This commit is contained in:
Daniel Nephin 2021-09-29 11:37:40 -04:00 committed by GitHub
commit 6b33e3bfd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 166 additions and 42 deletions

View File

@ -346,10 +346,12 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
for _, err := range validateEnterpriseConfigKeys(&c2) { for _, err := range validateEnterpriseConfigKeys(&c2) {
b.warn("%s", err) b.warn("%s", err)
} }
b.Warnings = append(b.Warnings, md.Warnings...)
// if we have a single 'check' or 'service' we need to add them to the // if we have a single 'check' or 'service' we need to add them to the
// list of checks and services first since we cannot merge them // list of checks and services first since we cannot merge them
// generically and later values would clobber earlier ones. // generically and later values would clobber earlier ones.
// TODO: move to applyDeprecatedConfig
if c2.Check != nil { if c2.Check != nil {
c2.Checks = append(c2.Checks, *c2.Check) c2.Checks = append(c2.Checks, *c2.Check)
c2.Check = nil c2.Check = nil
@ -733,16 +735,6 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
aclsEnabled := false aclsEnabled := false
primaryDatacenter := strings.ToLower(stringVal(c.PrimaryDatacenter)) primaryDatacenter := strings.ToLower(stringVal(c.PrimaryDatacenter))
if c.ACLDatacenter != nil {
b.warn("The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.")
if primaryDatacenter == "" {
primaryDatacenter = strings.ToLower(stringVal(c.ACLDatacenter))
}
// when the acl_datacenter config is used it implicitly enables acls
aclsEnabled = true
}
if c.ACL.Enabled != nil { if c.ACL.Enabled != nil {
aclsEnabled = boolVal(c.ACL.Enabled) aclsEnabled = boolVal(c.ACL.Enabled)
@ -887,7 +879,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
EnablePersistence: boolValWithDefault(c.ACL.EnableTokenPersistence, false), EnablePersistence: boolValWithDefault(c.ACL.EnableTokenPersistence, false),
ACLDefaultToken: stringValWithDefault(c.ACL.Tokens.Default, stringVal(c.ACLToken)), ACLDefaultToken: stringValWithDefault(c.ACL.Tokens.Default, stringVal(c.ACLToken)),
ACLAgentToken: stringValWithDefault(c.ACL.Tokens.Agent, stringVal(c.ACLAgentToken)), ACLAgentToken: stringValWithDefault(c.ACL.Tokens.Agent, stringVal(c.ACLAgentToken)),
ACLAgentMasterToken: stringValWithDefault(c.ACL.Tokens.AgentMaster, stringVal(c.ACLAgentMasterToken)), ACLAgentMasterToken: stringVal(c.ACL.Tokens.AgentMaster),
ACLReplicationToken: stringValWithDefault(c.ACL.Tokens.Replication, stringVal(c.ACLReplicationToken)), ACLReplicationToken: stringValWithDefault(c.ACL.Tokens.Replication, stringVal(c.ACLReplicationToken)),
}, },

View File

@ -15,7 +15,7 @@ type Source interface {
// Source returns an identifier for the Source that can be used in error message // Source returns an identifier for the Source that can be used in error message
Source() string Source() string
// Parse a configuration and return the result. // Parse a configuration and return the result.
Parse() (Config, mapstructure.Metadata, error) Parse() (Config, Metadata, error)
} }
// ErrNoData indicates to Builder.Build that the source contained no data, and // ErrNoData indicates to Builder.Build that the source contained no data, and
@ -34,9 +34,10 @@ func (f FileSource) Source() string {
} }
// Parse a config file in either JSON or HCL format. // Parse a config file in either JSON or HCL format.
func (f FileSource) Parse() (Config, mapstructure.Metadata, error) { func (f FileSource) Parse() (Config, Metadata, error) {
m := Metadata{}
if f.Name == "" || f.Data == "" { if f.Name == "" || f.Data == "" {
return Config{}, mapstructure.Metadata{}, ErrNoData return Config{}, m, ErrNoData
} }
var raw map[string]interface{} var raw map[string]interface{}
@ -51,10 +52,10 @@ func (f FileSource) Parse() (Config, mapstructure.Metadata, error) {
err = fmt.Errorf("invalid format: %s", f.Format) err = fmt.Errorf("invalid format: %s", f.Format)
} }
if err != nil { if err != nil {
return Config{}, md, err return Config{}, m, err
} }
var c Config var target decodeTarget
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc( DecodeHook: mapstructure.ComposeDecodeHookFunc(
// decode.HookWeakDecodeFromSlice is only necessary when reading from // decode.HookWeakDecodeFromSlice is only necessary when reading from
@ -66,16 +67,30 @@ func (f FileSource) Parse() (Config, mapstructure.Metadata, error) {
decode.HookTranslateKeys, decode.HookTranslateKeys,
), ),
Metadata: &md, Metadata: &md,
Result: &c, Result: &target,
}) })
if err != nil { if err != nil {
return Config{}, md, err return Config{}, m, err
} }
if err := d.Decode(raw); err != nil { if err := d.Decode(raw); err != nil {
return Config{}, md, err return Config{}, m, err
} }
return c, md, nil c, warns := applyDeprecatedConfig(&target)
m.Unused = md.Unused
m.Keys = md.Keys
m.Warnings = warns
return c, m, nil
}
// Metadata created by Source.Parse
type Metadata struct {
// Keys used in the config file.
Keys []string
// Unused keys that did not match any struct fields.
Unused []string
// Warnings caused by deprecated fields
Warnings []string
} }
// LiteralSource implements Source and returns an existing Config struct. // LiteralSource implements Source and returns an existing Config struct.
@ -88,8 +103,13 @@ func (l LiteralSource) Source() string {
return l.Name return l.Name
} }
func (l LiteralSource) Parse() (Config, mapstructure.Metadata, error) { func (l LiteralSource) Parse() (Config, Metadata, error) {
return l.Config, mapstructure.Metadata{}, nil return l.Config, Metadata{}, nil
}
type decodeTarget struct {
DeprecatedConfig `mapstructure:",squash"`
Config `mapstructure:",squash"`
} }
// Cache configuration for the agent/cache. // Cache configuration for the agent/cache.
@ -110,12 +130,8 @@ type Cache struct {
// configuration it should be treated as an external API which cannot be // configuration it should be treated as an external API which cannot be
// changed and refactored at will since this will break existing setups. // changed and refactored at will since this will break existing setups.
type Config struct { type Config struct {
// DEPRECATED (ACL-Legacy-Compat) - moved into the "acl.tokens" stanza
ACLAgentMasterToken *string `mapstructure:"acl_agent_master_token"`
// DEPRECATED (ACL-Legacy-Compat) - moved into the "acl.tokens" stanza // DEPRECATED (ACL-Legacy-Compat) - moved into the "acl.tokens" stanza
ACLAgentToken *string `mapstructure:"acl_agent_token"` ACLAgentToken *string `mapstructure:"acl_agent_token"`
// DEPRECATED (ACL-Legacy-Compat) - moved to "primary_datacenter"
ACLDatacenter *string `mapstructure:"acl_datacenter"`
// DEPRECATED (ACL-Legacy-Compat) - moved into the "acl" stanza // DEPRECATED (ACL-Legacy-Compat) - moved into the "acl" stanza
ACLDefaultPolicy *string `mapstructure:"acl_default_policy"` ACLDefaultPolicy *string `mapstructure:"acl_default_policy"`
// DEPRECATED (ACL-Legacy-Compat) - moved into the "acl" stanza // DEPRECATED (ACL-Legacy-Compat) - moved into the "acl" stanza

View File

@ -0,0 +1,42 @@
package config
import "fmt"
type DeprecatedConfig struct {
// DEPRECATED (ACL-Legacy-Compat) - moved into the "acl.tokens" stanza
ACLAgentMasterToken *string `mapstructure:"acl_agent_master_token"`
// DEPRECATED (ACL-Legacy-Compat) - moved to "primary_datacenter"
ACLDatacenter *string `mapstructure:"acl_datacenter"`
}
func applyDeprecatedConfig(d *decodeTarget) (Config, []string) {
dep := d.DeprecatedConfig
var warns []string
if dep.ACLAgentMasterToken != nil {
if d.Config.ACL.Tokens.AgentMaster == nil {
d.Config.ACL.Tokens.AgentMaster = dep.ACLAgentMasterToken
}
warns = append(warns, deprecationWarning("acl_agent_master_token", "acl.tokens.agent_master"))
}
if dep.ACLDatacenter != nil {
if d.Config.PrimaryDatacenter == nil {
d.Config.PrimaryDatacenter = dep.ACLDatacenter
}
// when the acl_datacenter config is used it implicitly enables acls
d.Config.ACL.Enabled = pBool(true)
warns = append(warns, deprecationWarning("acl_datacenter", "primary_datacenter"))
}
return d.Config, warns
}
func deprecationWarning(old, new string) string {
return fmt.Sprintf("The '%v' field is deprecated. Use the '%v' field instead.", old, new)
}
func pBool(v bool) *bool {
return &v
}

View File

@ -52,7 +52,6 @@ func TestMerge(t *testing.T) {
} }
} }
func pBool(v bool) *bool { return &v }
func pInt(v int) *int { return &v } func pInt(v int) *int { return &v }
func pString(v string) *string { return &v } func pString(v string) *string { return &v }
func pDuration(v time.Duration) *string { s := v.String(); return &s } func pDuration(v time.Duration) *string { s := v.String(); return &s }

View File

@ -5903,6 +5903,7 @@ func TestLoad_FullConfig(t *testing.T) {
expectedWarns := []string{ expectedWarns := []string{
`The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`, `The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`,
`The 'acl_agent_master_token' field is deprecated. Use the 'acl.tokens.agent_master' field instead.`,
`bootstrap_expect > 0: expecting 53 servers`, `bootstrap_expect > 0: expecting 53 servers`,
} }
expectedWarns = append(expectedWarns, enterpriseConfigKeyWarnings...) expectedWarns = append(expectedWarns, enterpriseConfigKeyWarnings...)

View File

@ -73,12 +73,28 @@ func translationsForType(to reflect.Type) map[string]string {
translations := map[string]string{} translations := map[string]string{}
for i := 0; i < to.NumField(); i++ { for i := 0; i < to.NumField(); i++ {
field := to.Field(i) field := to.Field(i)
tags := fieldTags(field)
if tags.squash {
embedded := field.Type
if embedded.Kind() == reflect.Ptr {
embedded = embedded.Elem()
}
if embedded.Kind() != reflect.Struct {
// mapstructure will handle reporting this error
continue
}
for k, v := range translationsForType(embedded) {
translations[k] = v
}
continue
}
tag, ok := field.Tag.Lookup("alias") tag, ok := field.Tag.Lookup("alias")
if !ok { if !ok {
continue continue
} }
canonKey := strings.ToLower(tags.name)
canonKey := strings.ToLower(canonicalFieldKey(field))
for _, alias := range strings.Split(tag, ",") { for _, alias := range strings.Split(tag, ",") {
translations[strings.ToLower(alias)] = canonKey translations[strings.ToLower(alias)] = canonKey
} }
@ -86,19 +102,31 @@ func translationsForType(to reflect.Type) map[string]string {
return translations return translations
} }
func canonicalFieldKey(field reflect.StructField) string { func fieldTags(field reflect.StructField) mapstructureFieldTags {
tag, ok := field.Tag.Lookup("mapstructure") tag, ok := field.Tag.Lookup("mapstructure")
if !ok { if !ok {
return field.Name return mapstructureFieldTags{name: field.Name}
} }
parts := strings.SplitN(tag, ",", 2)
switch { tags := mapstructureFieldTags{name: field.Name}
case len(parts) < 1: parts := strings.Split(tag, ",")
return field.Name if len(parts) == 0 {
case parts[0] == "": return tags
return field.Name
} }
return parts[0] if parts[0] != "" {
tags.name = parts[0]
}
for _, part := range parts[1:] {
if part == "squash" {
tags.squash = true
}
}
return tags
}
type mapstructureFieldTags struct {
name string
squash bool
} }
// HookWeakDecodeFromSlice looks for []map[string]interface{} and []interface{} // HookWeakDecodeFromSlice looks for []map[string]interface{} and []interface{}

View File

@ -1,6 +1,7 @@
package decode package decode
import ( import (
"fmt"
"reflect" "reflect"
"testing" "testing"
@ -210,16 +211,29 @@ type translateExample struct {
FieldWithMapstructureTag string `alias:"second" mapstructure:"field_with_mapstruct_tag"` FieldWithMapstructureTag string `alias:"second" mapstructure:"field_with_mapstruct_tag"`
FieldWithMapstructureTagOmit string `mapstructure:"field_with_mapstruct_omit,omitempty" alias:"third"` FieldWithMapstructureTagOmit string `mapstructure:"field_with_mapstruct_omit,omitempty" alias:"third"`
FieldWithEmptyTag string `mapstructure:"" alias:"forth"` FieldWithEmptyTag string `mapstructure:"" alias:"forth"`
EmbeddedStruct `mapstructure:",squash"`
*PtrEmbeddedStruct `mapstructure:",squash"`
BadField string `mapstructure:",squash"`
}
type EmbeddedStruct struct {
NextField string `alias:"next"`
}
type PtrEmbeddedStruct struct {
OtherNextField string `alias:"othernext"`
} }
func TestTranslationsForType(t *testing.T) { func TestTranslationsForType(t *testing.T) {
to := reflect.TypeOf(translateExample{}) to := reflect.TypeOf(translateExample{})
actual := translationsForType(to) actual := translationsForType(to)
expected := map[string]string{ expected := map[string]string{
"first": "fielddefaultcanonical", "first": "fielddefaultcanonical",
"second": "field_with_mapstruct_tag", "second": "field_with_mapstruct_tag",
"third": "field_with_mapstruct_omit", "third": "field_with_mapstruct_omit",
"forth": "fieldwithemptytag", "forth": "fieldwithemptytag",
"next": "nextfield",
"othernext": "othernextfield",
} }
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
} }
@ -389,3 +403,35 @@ service {
} }
require.Equal(t, target, expected) require.Equal(t, target, expected)
} }
func TestFieldTags(t *testing.T) {
type testCase struct {
tags string
expected mapstructureFieldTags
}
fn := func(t *testing.T, tc testCase) {
tag := fmt.Sprintf(`mapstructure:"%v"`, tc.tags)
field := reflect.StructField{
Tag: reflect.StructTag(tag),
Name: "Original",
}
actual := fieldTags(field)
require.Equal(t, tc.expected, actual)
}
var testCases = []testCase{
{tags: "", expected: mapstructureFieldTags{name: "Original"}},
{tags: "just-a-name", expected: mapstructureFieldTags{name: "just-a-name"}},
{tags: "name,squash", expected: mapstructureFieldTags{name: "name", squash: true}},
{tags: ",squash", expected: mapstructureFieldTags{name: "Original", squash: true}},
{tags: ",omitempty,squash", expected: mapstructureFieldTags{name: "Original", squash: true}},
{tags: "named,omitempty,squash", expected: mapstructureFieldTags{name: "named", squash: true}},
}
for _, tc := range testCases {
t.Run(tc.tags, func(t *testing.T) {
fn(t, tc)
})
}
}