agent: make the json/hcl decoding of ConnectProxyConfig fully work with CamelCase and snake_case (#8741)

Fixes #7418
This commit is contained in:
R.B. Boyer 2020-09-24 13:58:52 -05:00 committed by GitHub
parent 6467eb08dd
commit 4af8c78f1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 340 additions and 39 deletions

3
.changelog/8741.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
agent: make the json/hcl decoding of ConnectProxyConfig fully work with CamelCase and snake_case
```

View File

@ -83,7 +83,7 @@ func (c *MeshGatewayConfig) ToAPI() api.MeshGatewayConfig {
type ConnectProxyConfig struct {
// DestinationServiceName is required and is the name of the service to accept
// traffic for.
DestinationServiceName string `json:",omitempty"`
DestinationServiceName string `json:",omitempty" alias:"destination_service_name"`
// DestinationServiceID is optional and should only be specified for
// "side-car" style proxies where the proxy is in front of just a single
@ -91,19 +91,19 @@ type ConnectProxyConfig struct {
// being represented which must be registered to the same agent. It's valid to
// provide a service ID that does not yet exist to avoid timing issues when
// bootstrapping a service with a proxy.
DestinationServiceID string `json:",omitempty"`
DestinationServiceID string `json:",omitempty" alias:"destination_service_id"`
// LocalServiceAddress is the address of the local service instance. It is
// optional and should only be specified for "side-car" style proxies. It will
// default to 127.0.0.1 if the proxy is a "side-car" (DestinationServiceID is
// set) but otherwise will be ignored.
LocalServiceAddress string `json:",omitempty"`
LocalServiceAddress string `json:",omitempty" alias:"local_service_address"`
// LocalServicePort is the port of the local service instance. It is optional
// and should only be specified for "side-car" style proxies. It will default
// to the registered port for the instance if the proxy is a "side-car"
// (DestinationServiceID is set) but otherwise will be ignored.
LocalServicePort int `json:",omitempty"`
LocalServicePort int `json:",omitempty" alias:"local_service_port"`
// Config is the arbitrary configuration data provided with the proxy
// registration.
@ -127,6 +127,7 @@ func (t *ConnectProxyConfig) UnmarshalJSON(data []byte) (err error) {
DestinationServiceIDSnake string `json:"destination_service_id"`
LocalServiceAddressSnake string `json:"local_service_address"`
LocalServicePortSnake int `json:"local_service_port"`
MeshGatewaySnake MeshGatewayConfig `json:"mesh_gateway"`
*Alias
}{
@ -147,6 +148,9 @@ func (t *ConnectProxyConfig) UnmarshalJSON(data []byte) (err error) {
if t.LocalServicePort == 0 {
t.LocalServicePort = aux.LocalServicePortSnake
}
if t.MeshGateway.Mode == "" {
t.MeshGateway.Mode = aux.MeshGatewaySnake.Mode
}
return nil
@ -223,9 +227,9 @@ type Upstream struct {
// DestinationType would be better as an int constant but even with custom
// JSON marshallers it causes havoc with all the mapstructure mangling we do
// on service definitions in various places.
DestinationType string
DestinationNamespace string `json:",omitempty"`
DestinationName string
DestinationType string `alias:"destination_type"`
DestinationNamespace string `json:",omitempty" alias:"destination_namespace"`
DestinationName string `alias:"destination_name"`
// Datacenter that the service discovery request should be run against. Note
// for prepared queries, the actual results might be from a different
@ -234,19 +238,19 @@ type Upstream struct {
// LocalBindAddress is the ip address a side-car proxy should listen on for
// traffic destined for this upstream service. Default if empty is 127.0.0.1.
LocalBindAddress string `json:",omitempty"`
LocalBindAddress string `json:",omitempty" alias:"local_bind_address"`
// LocalBindPort is the ip address a side-car proxy should listen on for traffic
// destined for this upstream service. Required.
LocalBindPort int
LocalBindPort int `alias:"local_bind_port"`
// Config is an opaque config that is specific to the proxy process being run.
// It can be used to pass arbitrary configuration for this specific upstream
// to the proxy.
Config map[string]interface{} `bexpr:"-"`
Config map[string]interface{} `json:",omitempty" bexpr:"-"`
// MeshGateway is the configuration for mesh gateway usage of this upstream
MeshGateway MeshGatewayConfig `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
// IngressHosts are a list of hosts that should route to this upstream from
// an ingress gateway. This cannot and should not be set by a user, it is
@ -260,8 +264,11 @@ func (t *Upstream) UnmarshalJSON(data []byte) (err error) {
DestinationTypeSnake string `json:"destination_type"`
DestinationNamespaceSnake string `json:"destination_namespace"`
DestinationNameSnake string `json:"destination_name"`
LocalBindPortSnake int `json:"local_bind_port"`
LocalBindAddressSnake string `json:"local_bind_address"`
LocalBindPortSnake int `json:"local_bind_port"`
MeshGatewaySnake MeshGatewayConfig `json:"mesh_gateway"`
*Alias
}{
@ -279,11 +286,14 @@ func (t *Upstream) UnmarshalJSON(data []byte) (err error) {
if t.DestinationName == "" {
t.DestinationName = aux.DestinationNameSnake
}
if t.LocalBindAddress == "" {
t.LocalBindAddress = aux.LocalBindAddressSnake
}
if t.LocalBindPort == 0 {
t.LocalBindPort = aux.LocalBindPortSnake
}
if t.LocalBindAddress == "" {
t.LocalBindAddress = aux.LocalBindAddressSnake
if t.MeshGateway.Mode == "" {
t.MeshGateway.Mode = aux.MeshGatewaySnake.Mode
}
return nil
@ -412,7 +422,7 @@ type ExposePath struct {
// ListenerPort defines the port of the proxy's listener for exposed paths.
ListenerPort int `json:",omitempty" alias:"listener_port"`
// ExposePath is the path to expose through the proxy, ie. "/metrics."
// 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.
@ -423,14 +433,15 @@ type ExposePath struct {
Protocol string `json:",omitempty"`
// ParsedFromCheck is set if this path was parsed from a registered check
ParsedFromCheck bool
ParsedFromCheck bool `json:",omitempty" alias:"parsed_from_check"`
}
func (t *ExposePath) UnmarshalJSON(data []byte) (err error) {
type Alias ExposePath
aux := &struct {
LocalPathPortSnake int `json:"local_path_port"`
ListenerPortSnake int `json:"listener_port"`
LocalPathPortSnake int `json:"local_path_port"`
ParsedFromCheckSnake bool `json:"parsed_from_check"`
*Alias
}{
@ -445,6 +456,9 @@ func (t *ExposePath) UnmarshalJSON(data []byte) (err error) {
if t.ListenerPort == 0 {
t.ListenerPort = aux.ListenerPortSnake
}
if aux.ParsedFromCheckSnake {
t.ParsedFromCheck = true
}
return nil
}

View File

@ -108,8 +108,7 @@ func TestUpstream_MarshalJSON(t *testing.T) {
"DestinationName": "foo",
"Datacenter": "dc1",
"LocalBindPort": 1234,
"MeshGateway": {},
"Config": null
"MeshGateway": {}
}`,
wantErr: false,
},
@ -126,8 +125,7 @@ func TestUpstream_MarshalJSON(t *testing.T) {
"DestinationName": "foo",
"Datacenter": "dc1",
"LocalBindPort": 1234,
"MeshGateway": {},
"Config": null
"MeshGateway": {}
}`,
wantErr: false,
},
@ -150,6 +148,7 @@ func TestUpstream_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
json string
jsonSnake string
want Upstream
wantErr bool
}{
@ -197,18 +196,303 @@ func TestUpstream_UnmarshalJSON(t *testing.T) {
},
wantErr: true,
},
{
name: "kitchen sink",
json: `
{
"DestinationType": "service",
"DestinationNamespace": "default",
"DestinationName": "bar1",
"Datacenter": "dc1",
"LocalBindAddress": "127.0.0.2",
"LocalBindPort": 6060,
"Config": {
"x": "y",
"z": -2
},
"MeshGateway": {
"Mode": "local"
}
}
`,
jsonSnake: `
{
"destination_type": "service",
"destination_namespace": "default",
"destination_name": "bar1",
"datacenter": "dc1",
"local_bind_address": "127.0.0.2",
"local_bind_port": 6060,
"config": {
"x": "y",
"z": -2
},
"mesh_gateway": {
"mode": "local"
}
}
`,
want: Upstream{
DestinationType: UpstreamDestTypeService,
DestinationNamespace: "default",
DestinationName: "bar1",
Datacenter: "dc1",
LocalBindAddress: "127.0.0.2",
LocalBindPort: 6060,
Config: map[string]interface{}{
"x": "y",
"z": float64(-2),
},
MeshGateway: MeshGatewayConfig{
Mode: "local",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
t.Run("camel", func(t *testing.T) {
var got Upstream
err := json.Unmarshal([]byte(tt.json), &got)
if tt.wantErr {
require.Error(err)
return
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got, "%+v", got)
}
})
if tt.jsonSnake != "" {
t.Run("snake", func(t *testing.T) {
var got Upstream
err := json.Unmarshal([]byte(tt.jsonSnake), &got)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
}
})
}
}
func TestConnectProxyConfig_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
json string
jsonSnake string
want ConnectProxyConfig
wantErr bool
}{
{
name: "kitchen sink",
json: `
{
"DestinationServiceName": "foo-name",
"DestinationServiceID": "foo-id",
"LocalServiceAddress": "127.0.0.1",
"LocalServicePort": 5050,
"Config": {
"a": "b",
"v": 42
},
"Upstreams": [
{
"DestinationType": "service",
"DestinationNamespace": "default",
"DestinationName": "bar1",
"Datacenter": "dc1",
"LocalBindAddress": "127.0.0.2",
"LocalBindPort": 6060,
"Config": {
"x": "y",
"z": -2
},
"MeshGateway": {
"Mode": "local"
}
},
{
"DestinationType": "service",
"DestinationNamespace": "default",
"DestinationName": "bar2",
"Datacenter": "dc2",
"LocalBindAddress": "127.0.0.2",
"LocalBindPort": 6161
}
],
"MeshGateway": {
"Mode": "remote"
},
"Expose": {
"Checks": true,
"Paths": [
{
"ListenerPort": 8080,
"Path": "/foo",
"LocalPathPort": 7070,
"Protocol": "http2",
"ParsedFromCheck": true
},
{
"ListenerPort": 8181,
"Path": "/foo2",
"LocalPathPort": 7171,
"Protocol": "http",
"ParsedFromCheck": false
}
]
}
}
`,
jsonSnake: `
{
"destination_service_name": "foo-name",
"destination_service_id": "foo-id",
"local_service_address": "127.0.0.1",
"local_service_port": 5050,
"config": {
"a": "b",
"v": 42
},
"upstreams": [
{
"destination_type": "service",
"destination_namespace": "default",
"destination_name": "bar1",
"datacenter": "dc1",
"local_bind_address": "127.0.0.2",
"local_bind_port": 6060,
"config": {
"x": "y",
"z": -2
},
"mesh_gateway": {
"mode": "local"
}
},
{
"destination_type": "service",
"destination_namespace": "default",
"destination_name": "bar2",
"datacenter": "dc2",
"local_bind_address": "127.0.0.2",
"local_bind_port": 6161
}
],
"mesh_gateway": {
"mode": "remote"
},
"expose": {
"checks": true,
"paths": [
{
"listener_port": 8080,
"path": "/foo",
"local_path_port": 7070,
"protocol": "http2",
"parsed_from_check": true
},
{
"listener_port": 8181,
"path": "/foo2",
"local_path_port": 7171,
"protocol": "http",
"parsed_from_check": false
}
]
}
}
`,
want: ConnectProxyConfig{
DestinationServiceName: "foo-name",
DestinationServiceID: "foo-id",
LocalServiceAddress: "127.0.0.1",
LocalServicePort: 5050,
Config: map[string]interface{}{
"a": "b",
"v": float64(42),
},
Upstreams: []Upstream{
{
DestinationType: UpstreamDestTypeService,
DestinationNamespace: "default",
DestinationName: "bar1",
Datacenter: "dc1",
LocalBindAddress: "127.0.0.2",
LocalBindPort: 6060,
Config: map[string]interface{}{
"x": "y",
"z": float64(-2),
},
MeshGateway: MeshGatewayConfig{
Mode: "local",
},
},
{
DestinationType: UpstreamDestTypeService,
DestinationNamespace: "default",
DestinationName: "bar2",
Datacenter: "dc2",
LocalBindAddress: "127.0.0.2",
LocalBindPort: 6161,
},
},
MeshGateway: MeshGatewayConfig{
Mode: "remote",
},
Expose: ExposeConfig{
Checks: true,
Paths: []ExposePath{
{
ListenerPort: 8080,
Path: "/foo",
LocalPathPort: 7070,
Protocol: "http2",
ParsedFromCheck: true,
},
{
ListenerPort: 8181,
Path: "/foo2",
LocalPathPort: 7171,
Protocol: "http",
ParsedFromCheck: false,
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run("camel", func(t *testing.T) {
//
var got ConnectProxyConfig
err := json.Unmarshal([]byte(tt.json), &got)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
if tt.jsonSnake != "" {
t.Run("snake", func(t *testing.T) {
//
var got ConnectProxyConfig
err := json.Unmarshal([]byte(tt.json), &got)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
}
require.NoError(err)
require.Equal(tt.want, got)
})
}
}