Merge pull request #7963 from hashicorp/dnephin/replace-lib-translate-keys

Replace lib.TranslateKeys with a mapstructure decode hook
This commit is contained in:
Daniel Nephin 2020-05-27 16:51:26 -04:00 committed by GitHub
commit 4f2bff174d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 418 additions and 166 deletions

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl"
"github.com/mitchellh/mapstructure"
@ -108,32 +109,11 @@ func Parse(data string, format string) (c Config, keys []string, err error) {
"config_entries.bootstrap", // completely ignore this tree (fixed elsewhere)
})
// There is a difference of representation of some fields depending on
// where they are used. The HTTP API uses CamelCase whereas the config
// files use snake_case and between the two there is no automatic mapping.
// While the JSON and HCL parsers match keys without case (both `id` and
// `ID` are mapped to an ID field) the same thing does not happen between
// CamelCase and snake_case. Since changing either format would break
// existing setups we have to support both and slowly transition to one of
// the formats. Also, there is at least one case where we use the "wrong"
// key and want to map that to the new key to support deprecation -
// see [GH-3179]. TranslateKeys maps potentially CamelCased values to the
// snake_case that is used in the config file parser. If both the CamelCase
// and snake_case values are set the snake_case value is used and the other
// value is discarded.
lib.TranslateKeys(m, map[string]string{
"deregistercriticalserviceafter": "deregister_critical_service_after",
"dockercontainerid": "docker_container_id",
"scriptargs": "args",
"serviceid": "service_id",
"tlsskipverify": "tls_skip_verify",
"config_entries.bootstrap": "",
})
var md mapstructure.Metadata
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Metadata: &md,
Result: &c,
DecodeHook: decode.HookTranslateKeys,
Metadata: &md,
Result: &c,
})
if err != nil {
return Config{}, nil, err
@ -430,10 +410,10 @@ type CheckDefinition struct {
ID *string `json:"id,omitempty" hcl:"id" mapstructure:"id"`
Name *string `json:"name,omitempty" hcl:"name" mapstructure:"name"`
Notes *string `json:"notes,omitempty" hcl:"notes" mapstructure:"notes"`
ServiceID *string `json:"service_id,omitempty" hcl:"service_id" mapstructure:"service_id"`
ServiceID *string `json:"service_id,omitempty" hcl:"service_id" mapstructure:"service_id" alias:"serviceid"`
Token *string `json:"token,omitempty" hcl:"token" mapstructure:"token"`
Status *string `json:"status,omitempty" hcl:"status" mapstructure:"status"`
ScriptArgs []string `json:"args,omitempty" hcl:"args" mapstructure:"args"`
ScriptArgs []string `json:"args,omitempty" hcl:"args" mapstructure:"args" alias:"scriptargs"`
HTTP *string `json:"http,omitempty" hcl:"http" mapstructure:"http"`
Header map[string][]string `json:"header,omitempty" hcl:"header" mapstructure:"header"`
Method *string `json:"method,omitempty" hcl:"method" mapstructure:"method"`
@ -441,18 +421,18 @@ type CheckDefinition struct {
OutputMaxSize *int `json:"output_max_size,omitempty" hcl:"output_max_size" mapstructure:"output_max_size"`
TCP *string `json:"tcp,omitempty" hcl:"tcp" mapstructure:"tcp"`
Interval *string `json:"interval,omitempty" hcl:"interval" mapstructure:"interval"`
DockerContainerID *string `json:"docker_container_id,omitempty" hcl:"docker_container_id" mapstructure:"docker_container_id"`
DockerContainerID *string `json:"docker_container_id,omitempty" hcl:"docker_container_id" mapstructure:"docker_container_id" alias:"dockercontainerid"`
Shell *string `json:"shell,omitempty" hcl:"shell" mapstructure:"shell"`
GRPC *string `json:"grpc,omitempty" hcl:"grpc" mapstructure:"grpc"`
GRPCUseTLS *bool `json:"grpc_use_tls,omitempty" hcl:"grpc_use_tls" mapstructure:"grpc_use_tls"`
TLSSkipVerify *bool `json:"tls_skip_verify,omitempty" hcl:"tls_skip_verify" mapstructure:"tls_skip_verify"`
TLSSkipVerify *bool `json:"tls_skip_verify,omitempty" hcl:"tls_skip_verify" mapstructure:"tls_skip_verify" alias:"tlsskipverify"`
AliasNode *string `json:"alias_node,omitempty" hcl:"alias_node" mapstructure:"alias_node"`
AliasService *string `json:"alias_service,omitempty" hcl:"alias_service" mapstructure:"alias_service"`
Timeout *string `json:"timeout,omitempty" hcl:"timeout" mapstructure:"timeout"`
TTL *string `json:"ttl,omitempty" hcl:"ttl" mapstructure:"ttl"`
SuccessBeforePassing *int `json:"success_before_passing,omitempty" hcl:"success_before_passing" mapstructure:"success_before_passing"`
FailuresBeforeCritical *int `json:"failures_before_critical,omitempty" hcl:"failures_before_critical" mapstructure:"failures_before_critical"`
DeregisterCriticalServiceAfter *string `json:"deregister_critical_service_after,omitempty" hcl:"deregister_critical_service_after" mapstructure:"deregister_critical_service_after"`
DeregisterCriticalServiceAfter *string `json:"deregister_critical_service_after,omitempty" hcl:"deregister_critical_service_after" mapstructure:"deregister_critical_service_after" alias:"deregistercriticalserviceafter"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
}

View File

@ -9,6 +9,7 @@ import (
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/decode"
"github.com/mitchellh/mapstructure"
)
@ -90,9 +91,9 @@ func (s *HTTPServer) DiscoveryChainRead(resp http.ResponseWriter, req *http.Requ
// discoveryChainReadRequest is the API variation of structs.DiscoveryChainRequest
type discoveryChainReadRequest struct {
OverrideMeshGateway structs.MeshGatewayConfig
OverrideProtocol string
OverrideConnectTimeout time.Duration
OverrideMeshGateway structs.MeshGatewayConfig `alias:"override_mesh_gateway"`
OverrideProtocol string `alias:"override_protocol"`
OverrideConnectTimeout time.Duration `alias:"override_connect_timeout"`
}
// discoveryChainReadResponse is the API variation of structs.DiscoveryChainResponse
@ -105,15 +106,12 @@ func decodeDiscoveryChainReadRequest(raw map[string]interface{}) (*discoveryChai
// to do this part first.
raw = lib.PatchSliceOfMaps(raw, nil, nil)
lib.TranslateKeys(raw, map[string]string{
"override_mesh_gateway": "overridemeshgateway",
"override_protocol": "overrideprotocol",
"override_connect_timeout": "overrideconnecttimeout",
})
var apiReq discoveryChainReadRequest
decodeConf := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
DecodeHook: mapstructure.ComposeDecodeHookFunc(
decode.HookTranslateKeys,
mapstructure.StringToTimeDurationHookFunc(),
),
Result: &apiReq,
WeaklyTypedInput: true,
}

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/hashstructure"
@ -53,10 +54,10 @@ type ServiceConfigEntry struct {
Kind string
Name string
Protocol string
MeshGateway MeshGatewayConfig `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
ExternalSNI string `json:",omitempty"`
ExternalSNI string `json:",omitempty" alias:"external_sni"`
// TODO(banks): enable this once we have upstreams supported too. Enabling
// sidecars actually makes no sense and adds complications when you don't
@ -134,7 +135,7 @@ type ProxyConfigEntry struct {
Kind string
Name string
Config map[string]interface{}
MeshGateway MeshGatewayConfig `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
@ -283,7 +284,7 @@ func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
return nil, fmt.Errorf("Kind value in payload is not a string")
}
skipWhenPatching, translateKeysDict, err := ConfigEntryDecodeRulesForKind(entry.GetKind())
skipWhenPatching, err := ConfigEntryDecodeRulesForKind(entry.GetKind())
if err != nil {
return nil, err
}
@ -292,11 +293,12 @@ func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
// to do this part first.
raw = lib.PatchSliceOfMaps(raw, skipWhenPatching, nil)
lib.TranslateKeys(raw, translateKeysDict)
var md mapstructure.Metadata
decodeConf := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
DecodeHook: mapstructure.ComposeDecodeHookFunc(
decode.HookTranslateKeys,
mapstructure.StringToTimeDurationHookFunc(),
),
Metadata: &md,
Result: &entry,
WeaklyTypedInput: true,
@ -327,80 +329,48 @@ func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
// ConfigEntryDecodeRulesForKind returns rules for 'fixing' config entry key
// formats by kind. This is shared between the 'structs' and 'api' variations
// of config entries.
func ConfigEntryDecodeRulesForKind(kind string) (skipWhenPatching []string, translateKeysDict map[string]string, err error) {
func ConfigEntryDecodeRulesForKind(kind string) (skipWhenPatching []string, err error) {
switch kind {
case ProxyDefaults:
return []string{
"expose.paths",
"Expose.Paths",
}, map[string]string{
"local_path_port": "localpathport",
"listener_port": "listenerport",
"mesh_gateway": "meshgateway",
"config": "",
}, nil
"expose.paths",
"Expose.Paths",
}, nil
case ServiceDefaults:
return []string{
"expose.paths",
"Expose.Paths",
}, map[string]string{
"local_path_port": "localpathport",
"listener_port": "listenerport",
"mesh_gateway": "meshgateway",
"external_sni": "externalsni",
}, nil
"expose.paths",
"Expose.Paths",
}, nil
case ServiceRouter:
return []string{
"routes",
"Routes",
"routes.match.http.header",
"Routes.Match.HTTP.Header",
"routes.match.http.query_param",
"Routes.Match.HTTP.QueryParam",
}, map[string]string{
"num_retries": "numretries",
"path_exact": "pathexact",
"path_prefix": "pathprefix",
"path_regex": "pathregex",
"prefix_rewrite": "prefixrewrite",
"query_param": "queryparam",
"request_timeout": "requesttimeout",
"retry_on_connect_failure": "retryonconnectfailure",
"retry_on_status_codes": "retryonstatuscodes",
"service_subset": "servicesubset",
}, nil
"routes",
"Routes",
"routes.match.http.header",
"Routes.Match.HTTP.Header",
"routes.match.http.query_param",
"Routes.Match.HTTP.QueryParam",
}, nil
case ServiceSplitter:
return []string{
"splits",
"Splits",
}, map[string]string{
"service_subset": "servicesubset",
}, nil
case ServiceResolver:
return nil, map[string]string{
"connect_timeout": "connecttimeout",
"default_subset": "defaultsubset",
"only_passing": "onlypassing",
"service_subset": "servicesubset",
"splits",
"Splits",
}, nil
case ServiceResolver:
return nil, nil
case IngressGateway:
return []string{
"listeners",
"Listeners",
"listeners.services",
"Listeners.Services",
}, nil, nil
}, nil
case TerminatingGateway:
return []string{
"services",
"Services",
}, map[string]string{
"ca_file": "cafile",
"cert_file": "certfile",
"key_file": "keyfile",
}, nil
"services",
"Services",
}, nil
default:
return nil, nil, fmt.Errorf("kind %q should be explicitly handled here", kind)
return nil, fmt.Errorf("kind %q should be explicitly handled here", kind)
}
}

View File

@ -260,12 +260,12 @@ func (m *ServiceRouteMatch) IsEmpty() bool {
// ServiceRouteHTTPMatch is a set of http-specific match criteria.
type ServiceRouteHTTPMatch struct {
PathExact string `json:",omitempty"`
PathPrefix string `json:",omitempty"`
PathRegex string `json:",omitempty"`
PathExact string `json:",omitempty" alias:"path_exact"`
PathPrefix string `json:",omitempty" alias:"path_prefix"`
PathRegex string `json:",omitempty" alias:"path_regex"`
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty" alias:"query_param"`
Methods []string `json:",omitempty"`
}
@ -308,7 +308,7 @@ type ServiceRouteDestination struct {
//
// If this field is specified then this route is ineligible for further
// splitting.
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
// Namespace is the namespace to resolve the service from instead of the
// current namespace. If empty the current namespace is assumed.
@ -320,24 +320,24 @@ type ServiceRouteDestination struct {
// PrefixRewrite allows for the proxied request to have its matching path
// prefix modified before being sent to the destination. Described more
// below in the envoy implementation section.
PrefixRewrite string `json:",omitempty"`
PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"`
// RequestTimeout is the total amount of time permitted for the entire
// downstream request (and retries) to be processed.
RequestTimeout time.Duration `json:",omitempty"`
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
// NumRetries is the number of times to retry the request when a retryable
// result occurs. This seems fairly proxy agnostic.
NumRetries uint32 `json:",omitempty"`
NumRetries uint32 `json:",omitempty" alias:"num_retries"`
// RetryOnConnectFailure allows for connection failure errors to trigger a
// retry. This should be expressible in other proxies as it's just a layer
// 4 failure bubbling up to layer 7.
RetryOnConnectFailure bool `json:",omitempty"`
RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"`
// RetryOnStatusCodes is a flat list of http response status codes that are
// eligible for retry. This again should be feasible in any sane proxy.
RetryOnStatusCodes []uint32 `json:",omitempty"`
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
}
func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
@ -576,7 +576,7 @@ type ServiceSplit struct {
//
// If this field is specified then this route is ineligible for further
// splitting.
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
// Namespace is the namespace to resolve the service from instead of the
// current namespace. If empty the current namespace is assumed (optional).
@ -604,7 +604,7 @@ type ServiceResolverConfigEntry struct {
// DefaultSubset is the subset to use when no explicit subset is
// requested. If empty the unnamed subset is used.
DefaultSubset string `json:",omitempty"`
DefaultSubset string `json:",omitempty" alias:"default_subset"`
// Subsets is a map of subset name to subset definition for all
// usable named subsets of this service. The map key is the name
@ -637,7 +637,7 @@ type ServiceResolverConfigEntry struct {
// ConnectTimeout is the timeout for establishing new network connections
// to this service.
ConnectTimeout time.Duration `json:",omitempty"`
ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex
@ -884,7 +884,7 @@ type ServiceResolverSubset struct {
// to true, only instances with checks in the passing state will be
// returned. (behaves identically to the similarly named field on prepared
// queries).
OnlyPassing bool `json:",omitempty"`
OnlyPassing bool `json:",omitempty" alias:"only_passing"`
}
type ServiceResolverRedirect struct {
@ -898,7 +898,7 @@ type ServiceResolverRedirect struct {
//
// If this is specified at least one of Service, Datacenter, or Namespace
// should be configured.
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
// Namespace is the namespace to resolve the service from instead of the
// current one (optional).
@ -926,7 +926,7 @@ type ServiceResolverFailover struct {
// requested service is used (optional).
//
// This is a DESTINATION during failover.
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
// Namespace is the namespace to resolve the requested service from to form
// the failover group of instances. If empty the current namespace is used

View File

@ -248,15 +248,15 @@ type LinkedService struct {
// CAFile is the optional path to a CA certificate to use for TLS connections
// from the gateway to the linked service
CAFile string `json:",omitempty"`
CAFile string `json:",omitempty" alias:"ca_file"`
// CertFile is the optional path to a client certificate to use for TLS connections
// from the gateway to the linked service
CertFile string `json:",omitempty"`
CertFile string `json:",omitempty" alias:"cert_file"`
// KeyFile is the optional path to a private key to use for TLS connections
// from the gateway to the linked service
KeyFile string `json:",omitempty"`
KeyFile string `json:",omitempty" alias:"key_file"`
// SNI is the optional name to specify during the TLS handshake with a linked service
SNI string `json:",omitempty"`

View File

@ -114,7 +114,7 @@ type ConnectProxyConfig struct {
Upstreams Upstreams `json:",omitempty"`
// MeshGateway defines the mesh gateway configuration for this upstream
MeshGateway MeshGatewayConfig `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
// Expose defines whether checks or paths are exposed through the proxy
Expose ExposeConfig `json:",omitempty"`
@ -410,13 +410,13 @@ type ExposeConfig struct {
type ExposePath struct {
// ListenerPort defines the port of the proxy's listener for exposed paths.
ListenerPort int `json:",omitempty"`
ListenerPort int `json:",omitempty" alias:"listener_port"`
// ExposePath is the path to expose through the proxy, ie. "/metrics."
Path string `json:",omitempty"`
// LocalPathPort is the port that the service is listening on for the given path.
LocalPathPort int `json:",omitempty"`
LocalPathPort int `json:",omitempty" alias:"local_path_port"`
// Protocol describes the upstream's service protocol.
// Valid values are "http" and "http2", defaults to "http"

View File

@ -7,7 +7,7 @@ import (
envoycluster "github.com/envoyproxy/go-control-plane/envoy/api/v2/cluster"
"github.com/gogo/protobuf/types"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/decode"
"github.com/mitchellh/mapstructure"
)
@ -79,14 +79,14 @@ type GatewayConfig struct {
// for those addresses or where an external entity maps that IP to the Envoy
// (like AWS EC2 mapping a public IP to the private interface) then this
// cannot be used. See the BindAddresses config instead
BindTaggedAddresses bool `mapstructure:"envoy_gateway_bind_tagged_addresses"`
BindTaggedAddresses bool `mapstructure:"envoy_gateway_bind_tagged_addresses" alias:"envoy_mesh_gateway_bind_tagged_addresses"`
// BindAddresses additional bind addresses to configure listeners for
BindAddresses map[string]structs.ServiceAddress `mapstructure:"envoy_gateway_bind_addresses"`
BindAddresses map[string]structs.ServiceAddress `mapstructure:"envoy_gateway_bind_addresses" alias:"envoy_mesh_gateway_bind_addresses"`
// NoDefaultBind indicates that we should not bind to the default address of the
// gateway service
NoDefaultBind bool `mapstructure:"envoy_gateway_no_default_bind"`
NoDefaultBind bool `mapstructure:"envoy_gateway_no_default_bind" alias:"envoy_mesh_gateway_no_default_bind"`
// ConnectTimeoutMs is the number of milliseconds to timeout making a new
// connection to this upstream. Defaults to 5000 (5 seconds) if not set.
@ -97,15 +97,18 @@ type GatewayConfig struct {
// error occurs during parsing, it is returned along with the default config. This
// allows the caller to choose whether and how to report the error
func ParseGatewayConfig(m map[string]interface{}) (GatewayConfig, error) {
// Fixup for deprecated mesh gateway names
lib.TranslateKeys(m, map[string]string{
"envoy_mesh_gateway_bind_tagged_addresses": "envoy_gateway_bind_tagged_addresses",
"envoy_mesh_gateway_bind_addresses": "envoy_gateway_bind_addresses",
"envoy_mesh_gateway_no_default_bind": "envoy_gateway_no_default_bind",
})
var cfg GatewayConfig
err := mapstructure.WeakDecode(m, &cfg)
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: decode.HookTranslateKeys,
Result: &cfg,
WeaklyTypedInput: true,
})
if err != nil {
return cfg, err
}
if err := d.Decode(m); err != nil {
return cfg, err
}
if cfg.ConnectTimeoutMs < 1 {
cfg.ConnectTimeoutMs = 5000

View File

@ -71,13 +71,13 @@ type ExposeConfig struct {
type ExposePath struct {
// ListenerPort defines the port of the proxy's listener for exposed paths.
ListenerPort int `json:",omitempty"`
ListenerPort int `json:",omitempty" alias:"listener_port"`
// Path is the path to expose through the proxy, ie. "/metrics."
Path string `json:",omitempty"`
// LocalPathPort is the port that the service is listening on for the given path.
LocalPathPort int `json:",omitempty"`
LocalPathPort int `json:",omitempty" alias:"local_path_port"`
// Protocol describes the upstream's service protocol.
// Valid values are "http" and "http2", defaults to "http"
@ -92,9 +92,9 @@ type ServiceConfigEntry struct {
Name string
Namespace string `json:",omitempty"`
Protocol string `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
ExternalSNI string `json:",omitempty"`
ExternalSNI string `json:",omitempty" alias:"external_sni"`
CreateIndex uint64
ModifyIndex uint64
}
@ -120,7 +120,7 @@ type ProxyConfigEntry struct {
Name string
Namespace string `json:",omitempty"`
Config map[string]interface{} `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64

View File

@ -31,12 +31,12 @@ type ServiceRouteMatch struct {
}
type ServiceRouteHTTPMatch struct {
PathExact string `json:",omitempty"`
PathPrefix string `json:",omitempty"`
PathRegex string `json:",omitempty"`
PathExact string `json:",omitempty" alias:"path_exact"`
PathPrefix string `json:",omitempty" alias:"path_prefix"`
PathRegex string `json:",omitempty" alias:"path_regex"`
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty" alias:"query_param"`
Methods []string `json:",omitempty"`
}
@ -59,13 +59,13 @@ type ServiceRouteHTTPMatchQueryParam struct {
type ServiceRouteDestination struct {
Service string `json:",omitempty"`
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
Namespace string `json:",omitempty"`
PrefixRewrite string `json:",omitempty"`
RequestTimeout time.Duration `json:",omitempty"`
NumRetries uint32 `json:",omitempty"`
RetryOnConnectFailure bool `json:",omitempty"`
RetryOnStatusCodes []uint32 `json:",omitempty"`
PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"`
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
NumRetries uint32 `json:",omitempty" alias:"num_retries"`
RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"`
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
}
func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
@ -123,7 +123,7 @@ func (e *ServiceSplitterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIn
type ServiceSplit struct {
Weight float32
Service string `json:",omitempty"`
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
Namespace string `json:",omitempty"`
}
@ -132,11 +132,11 @@ type ServiceResolverConfigEntry struct {
Name string
Namespace string `json:",omitempty"`
DefaultSubset string `json:",omitempty"`
DefaultSubset string `json:",omitempty" alias:"default_subset"`
Subsets map[string]ServiceResolverSubset `json:",omitempty"`
Redirect *ServiceResolverRedirect `json:",omitempty"`
Failover map[string]ServiceResolverFailover `json:",omitempty"`
ConnectTimeout time.Duration `json:",omitempty"`
ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"`
CreateIndex uint64
ModifyIndex uint64
@ -185,19 +185,19 @@ func (e *ServiceResolverConfigEntry) GetModifyIndex() uint64 { return e.ModifyIn
type ServiceResolverSubset struct {
Filter string `json:",omitempty"`
OnlyPassing bool `json:",omitempty"`
OnlyPassing bool `json:",omitempty" alias:"only_passing"`
}
type ServiceResolverRedirect struct {
Service string `json:",omitempty"`
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
Namespace string `json:",omitempty"`
Datacenter string `json:",omitempty"`
}
type ServiceResolverFailover struct {
Service string `json:",omitempty"`
ServiceSubset string `json:",omitempty"`
ServiceSubset string `json:",omitempty" alias:"service_subset"`
Namespace string `json:",omitempty"`
Datacenters []string `json:",omitempty"`
}

View File

@ -139,15 +139,15 @@ type LinkedService struct {
// CAFile is the optional path to a CA certificate to use for TLS connections
// from the gateway to the linked service
CAFile string `json:",omitempty"`
CAFile string `json:",omitempty" alias:"ca_file"`
// CertFile is the optional path to a client certificate to use for TLS connections
// from the gateway to the linked service
CertFile string `json:",omitempty"`
CertFile string `json:",omitempty" alias:"cert_file"`
// KeyFile is the optional path to a private key to use for TLS connections
// from the gateway to the linked service
KeyFile string `json:",omitempty"`
KeyFile string `json:",omitempty" alias:"key_file"`
// SNI is the optional name to specify during the TLS handshake with a linked service
SNI string `json:",omitempty"`

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/helpers"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/cli"
"github.com/mitchellh/mapstructure"
@ -132,7 +133,7 @@ func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
return nil, fmt.Errorf("Kind value in payload is not a string")
}
skipWhenPatching, translateKeysDict, err := structs.ConfigEntryDecodeRulesForKind(entry.GetKind())
skipWhenPatching, err := structs.ConfigEntryDecodeRulesForKind(entry.GetKind())
if err != nil {
return nil, err
}
@ -141,14 +142,12 @@ func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
// to do this part first.
raw = lib.PatchSliceOfMaps(raw, skipWhenPatching, nil)
// CamelCase is the canonical form for these, since this translation
// happens in the `consul config write` command and the JSON form is sent
// off to the server.
lib.TranslateKeys(raw, translateKeysDict)
var md mapstructure.Metadata
decodeConf := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
DecodeHook: mapstructure.ComposeDecodeHookFunc(
decode.HookTranslateKeys,
mapstructure.StringToTimeDurationHookFunc(),
),
Metadata: &md,
Result: &entry,
WeaklyTypedInput: true,

93
lib/decode/decode.go Normal file
View File

@ -0,0 +1,93 @@
/*
Package decode provides tools for customizing the decoding of configuration,
into structures using mapstructure.
*/
package decode
import (
"reflect"
"strings"
)
// HookTranslateKeys is a mapstructure decode hook which translates keys in a
// map to their canonical value.
//
// Any struct field with a field tag of `alias` may be loaded from any of the
// values keyed by any of the aliases. A field may have one or more alias.
// Aliases must be lowercase, as keys are compared case-insensitive.
//
// Example alias tag:
// MyField []string `alias:"old_field_name,otherfieldname"`
//
// This hook should ONLY be used to maintain backwards compatibility with
// deprecated keys. For new structures use mapstructure struct tags to set the
// desired serialization key.
//
// IMPORTANT: This function assumes that mapstructure is being used with the
// default struct field tag of `mapstructure`. If mapstructure.DecoderConfig.TagName
// is set to a different value this function will need to be parameterized with
// that value to correctly find the canonical data key.
func HookTranslateKeys(_, to reflect.Type, data interface{}) (interface{}, error) {
// Return immediately if target is not a struct, as only structs can have
// field tags. If the target is a pointer to a struct, mapstructure will call
// the hook again with the struct.
if to.Kind() != reflect.Struct {
return data, nil
}
// Avoid doing any work if data is not a map
source, ok := data.(map[string]interface{})
if !ok {
return data, nil
}
rules := translationsForType(to)
for k, v := range source {
lowerK := strings.ToLower(k)
canonKey, ok := rules[lowerK]
if !ok {
continue
}
delete(source, k)
// if there is a value for the canonical key then keep it
if _, ok := source[canonKey]; ok {
continue
}
source[canonKey] = v
}
return source, nil
}
// TODO: could be cached if it is too slow
func translationsForType(to reflect.Type) map[string]string {
translations := map[string]string{}
for i := 0; i < to.NumField(); i++ {
field := to.Field(i)
tag, ok := field.Tag.Lookup("alias")
if !ok {
continue
}
canonKey := strings.ToLower(canonicalFieldKey(field))
for _, alias := range strings.Split(tag, ",") {
translations[strings.ToLower(alias)] = canonKey
}
}
return translations
}
func canonicalFieldKey(field reflect.StructField) string {
tag, ok := field.Tag.Lookup("mapstructure")
if !ok {
return field.Name
}
parts := strings.SplitN(tag, ",", 2)
switch {
case len(parts) < 1:
return field.Name
case parts[0] == "":
return field.Name
}
return parts[0]
}

207
lib/decode/decode_test.go Normal file
View File

@ -0,0 +1,207 @@
package decode
import (
"reflect"
"testing"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
)
func TestHookTranslateKeys(t *testing.T) {
var testcases = []struct {
name string
data interface{}
expected interface{}
}{
{
name: "target of type struct, with struct receiver",
data: map[string]interface{}{
"S": map[string]interface{}{
"None": "no translation",
"OldOne": "value1",
"oldtwo": "value2",
},
},
expected: Config{
S: TypeStruct{
One: "value1",
Two: "value2",
None: "no translation",
},
},
},
{
name: "target of type ptr, with struct receiver",
data: map[string]interface{}{
"PS": map[string]interface{}{
"None": "no translation",
"OldOne": "value1",
"oldtwo": "value2",
},
},
expected: Config{
PS: &TypeStruct{
One: "value1",
Two: "value2",
None: "no translation",
},
},
},
{
name: "target of type ptr, with ptr receiver",
data: map[string]interface{}{
"PTR": map[string]interface{}{
"None": "no translation",
"old_THREE": "value3",
"oldfour": "value4",
},
},
expected: Config{
PTR: &TypePtrToStruct{
Three: "value3",
Four: "value4",
None: "no translation",
},
},
},
{
name: "target of type ptr, with struct receiver",
data: map[string]interface{}{
"PTRS": map[string]interface{}{
"None": "no translation",
"old_THREE": "value3",
"old_four": "value4",
},
},
expected: Config{
PTRS: TypePtrToStruct{
Three: "value3",
Four: "value4",
None: "no translation",
},
},
},
{
name: "target of type map",
data: map[string]interface{}{
"Blob": map[string]interface{}{
"one": 1,
"two": 2,
},
},
expected: Config{
Blob: map[string]interface{}{
"one": 1,
"two": 2,
},
},
},
{
name: "value already exists for canonical key",
data: map[string]interface{}{
"PS": map[string]interface{}{
"OldOne": "value1",
"One": "original1",
"oldTWO": "value2",
"two": "original2",
},
},
expected: Config{
PS: &TypeStruct{
One: "original1",
Two: "original2",
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
cfg := Config{}
md := new(mapstructure.Metadata)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookTranslateKeys,
Metadata: md,
Result: &cfg,
})
require.NoError(t, err)
require.NoError(t, decoder.Decode(tc.data))
require.Equal(t, cfg, tc.expected, "decode metadata: %#v", md)
})
}
}
type Config struct {
S TypeStruct
PS *TypeStruct
PTR *TypePtrToStruct
PTRS TypePtrToStruct
Blob map[string]interface{}
}
type TypeStruct struct {
One string `alias:"oldone"`
Two string `alias:"oldtwo"`
None string
}
type TypePtrToStruct struct {
Three string `alias:"old_three"`
Four string `alias:"old_four,oldfour"`
None string
}
func TestHookTranslateKeys_TargetStructHasPointerReceiver(t *testing.T) {
target := &TypePtrToStruct{}
md := new(mapstructure.Metadata)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookTranslateKeys,
Metadata: md,
Result: target,
})
require.NoError(t, err)
data := map[string]interface{}{
"None": "no translation",
"Old_Three": "value3",
"OldFour": "value4",
}
expected := &TypePtrToStruct{
None: "no translation",
Three: "value3",
Four: "value4",
}
require.NoError(t, decoder.Decode(data))
require.Equal(t, expected, target, "decode metadata: %#v", md)
}
type translateExample struct {
FieldDefaultCanonical string `alias:"first"`
FieldWithMapstructureTag string `alias:"second" mapstructure:"field_with_mapstruct_tag"`
FieldWithMapstructureTagOmit string `mapstructure:"field_with_mapstruct_omit,omitempty" alias:"third"`
FieldWithEmptyTag string `mapstructure:"" alias:"forth"`
}
func TestTranslationsForType(t *testing.T) {
to := reflect.TypeOf(translateExample{})
actual := translationsForType(to)
expected := map[string]string{
"first": "fielddefaultcanonical",
"second": "field_with_mapstruct_tag",
"third": "field_with_mapstruct_omit",
"forth": "fieldwithemptytag",
}
require.Equal(t, expected, actual)
}
type nested struct {
O map[string]interface{}
Slice []Item
Item Item
}
type Item struct {
Name string
}

View File

@ -34,6 +34,8 @@ import (
// // item's config field
// "widgets.config": "",
// })
//
// Deprecated: Use lib/decode.HookTranslateKeys instead.
func TranslateKeys(v map[string]interface{}, dict map[string]string) {
// Convert all dict keys for exclusions to lower. so we can match against them
// unambiguously with a single lookup.