mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1446 lines
32 KiB
1446 lines
32 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
package api |
|
|
|
import ( |
|
"encoding/json" |
|
"testing" |
|
"time" |
|
|
|
"github.com/stretchr/testify/require" |
|
|
|
"github.com/hashicorp/consul/sdk/testutil" |
|
) |
|
|
|
func TestAPI_ConfigEntries(t *testing.T) { |
|
t.Parallel() |
|
c, s := makeClient(t) |
|
defer s.Stop() |
|
|
|
config_entries := c.ConfigEntries() |
|
|
|
t.Run("Proxy Defaults", func(t *testing.T) { |
|
global_proxy := &ProxyConfigEntry{ |
|
Kind: ProxyDefaults, |
|
Name: ProxyConfigGlobal, |
|
Config: map[string]interface{}{ |
|
"foo": "bar", |
|
"bar": 1.0, |
|
}, |
|
MutualTLSMode: MutualTLSModeStrict, |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
} |
|
|
|
// set it |
|
_, wm, err := config_entries.Set(global_proxy, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
// get it |
|
entry, qm, err := config_entries.Get(ProxyDefaults, ProxyConfigGlobal, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, qm) |
|
require.NotEqual(t, 0, qm.RequestTime) |
|
|
|
// verify it |
|
readProxy, ok := entry.(*ProxyConfigEntry) |
|
require.True(t, ok) |
|
require.Equal(t, global_proxy.Kind, readProxy.Kind) |
|
require.Equal(t, global_proxy.Name, readProxy.Name) |
|
require.Equal(t, global_proxy.Config, readProxy.Config) |
|
require.Equal(t, global_proxy.MutualTLSMode, readProxy.MutualTLSMode) |
|
require.Equal(t, global_proxy.Meta, readProxy.GetMeta()) |
|
require.Equal(t, global_proxy.Meta, readProxy.GetMeta()) |
|
|
|
global_proxy.Config["baz"] = true |
|
// CAS update fail |
|
written, _, err := config_entries.CAS(global_proxy, 0, nil) |
|
require.NoError(t, err) |
|
require.False(t, written) |
|
|
|
// CAS update success |
|
written, wm, err = config_entries.CAS(global_proxy, readProxy.ModifyIndex, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
require.NoError(t, err) |
|
require.True(t, written) |
|
|
|
// Non CAS update |
|
global_proxy.Config["baz"] = "baz" |
|
_, wm, err = config_entries.Set(global_proxy, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
// list it |
|
entries, qm, err := config_entries.List(ProxyDefaults, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, qm) |
|
require.NotEqual(t, 0, qm.RequestTime) |
|
require.Len(t, entries, 1) |
|
readProxy, ok = entries[0].(*ProxyConfigEntry) |
|
require.True(t, ok) |
|
require.Equal(t, global_proxy.Kind, readProxy.Kind) |
|
require.Equal(t, global_proxy.Name, readProxy.Name) |
|
require.Equal(t, global_proxy.Config, readProxy.Config) |
|
|
|
// delete it |
|
wm, err = config_entries.Delete(ProxyDefaults, ProxyConfigGlobal, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
_, _, err = config_entries.Get(ProxyDefaults, ProxyConfigGlobal, nil) |
|
require.Error(t, err) |
|
}) |
|
|
|
t.Run("Service Defaults", func(t *testing.T) { |
|
service := &ServiceConfigEntry{ |
|
Kind: ServiceDefaults, |
|
Name: "foo", |
|
Protocol: "http", |
|
MutualTLSMode: MutualTLSModeStrict, |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
MaxInboundConnections: 5, |
|
BalanceInboundConnections: "exact_balance", |
|
LocalConnectTimeoutMs: 5000, |
|
LocalRequestTimeoutMs: 7000, |
|
} |
|
|
|
dest := &DestinationConfig{ |
|
Addresses: []string{"my.example.com"}, |
|
Port: 80, |
|
} |
|
|
|
service2 := &ServiceConfigEntry{ |
|
Kind: ServiceDefaults, |
|
Name: "bar", |
|
Protocol: "http", |
|
Destination: dest, |
|
} |
|
|
|
// set it |
|
_, wm, err := config_entries.Set(service, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
// also set the second one |
|
_, wm, err = config_entries.Set(service2, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
// get it |
|
entry, qm, err := config_entries.Get(ServiceDefaults, "foo", nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, qm) |
|
require.NotEqual(t, 0, qm.RequestTime) |
|
|
|
// verify it |
|
readService, ok := entry.(*ServiceConfigEntry) |
|
require.True(t, ok) |
|
require.Equal(t, service.Kind, readService.Kind) |
|
require.Equal(t, service.Name, readService.Name) |
|
require.Equal(t, service.Protocol, readService.Protocol) |
|
require.Equal(t, service.MutualTLSMode, readService.MutualTLSMode) |
|
require.Equal(t, service.Meta, readService.Meta) |
|
require.Equal(t, service.Meta, readService.GetMeta()) |
|
require.Equal(t, service.MaxInboundConnections, readService.MaxInboundConnections) |
|
require.Equal(t, service.BalanceInboundConnections, readService.BalanceInboundConnections) |
|
require.Equal(t, service.LocalConnectTimeoutMs, readService.LocalConnectTimeoutMs) |
|
require.Equal(t, service.LocalRequestTimeoutMs, readService.LocalRequestTimeoutMs) |
|
|
|
// update it |
|
service.Protocol = "tcp" |
|
|
|
// CAS fail |
|
written, _, err := config_entries.CAS(service, 0, nil) |
|
require.NoError(t, err) |
|
require.False(t, written) |
|
|
|
// CAS success |
|
written, wm, err = config_entries.CAS(service, readService.ModifyIndex, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
require.True(t, written) |
|
|
|
// update no cas |
|
service.Protocol = "tcp" |
|
|
|
_, wm, err = config_entries.Set(service, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
// list them |
|
entries, qm, err := config_entries.List(ServiceDefaults, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, qm) |
|
require.NotEqual(t, 0, qm.RequestTime) |
|
require.Len(t, entries, 2) |
|
|
|
for _, entry = range entries { |
|
switch entry.GetName() { |
|
case "foo": |
|
// this also verifies that the update value was persisted and |
|
// the updated values are seen |
|
readService, ok = entry.(*ServiceConfigEntry) |
|
require.True(t, ok) |
|
require.Equal(t, service.Kind, readService.Kind) |
|
require.Equal(t, service.Name, readService.Name) |
|
require.Equal(t, service.Protocol, readService.Protocol) |
|
case "bar": |
|
readService, ok = entry.(*ServiceConfigEntry) |
|
require.True(t, ok) |
|
require.Equal(t, service2.Kind, readService.Kind) |
|
require.Equal(t, service2.Name, readService.Name) |
|
require.Equal(t, service2.Protocol, readService.Protocol) |
|
require.Equal(t, dest, readService.Destination) |
|
} |
|
} |
|
|
|
// delete it |
|
wm, err = config_entries.Delete(ServiceDefaults, "foo", nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
// verify deletion |
|
_, _, err = config_entries.Get(ServiceDefaults, "foo", nil) |
|
require.Error(t, err) |
|
}) |
|
|
|
t.Run("Mesh", func(t *testing.T) { |
|
mesh := &MeshConfigEntry{ |
|
TransparentProxy: TransparentProxyMeshConfig{MeshDestinationsOnly: true}, |
|
AllowEnablingPermissiveMutualTLS: true, |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
Partition: defaultPartition, |
|
Namespace: defaultNamespace, |
|
} |
|
ce := c.ConfigEntries() |
|
|
|
testutil.RunStep(t, "set and get", func(t *testing.T) { |
|
_, wm, err := ce.Set(mesh, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
entry, qm, err := ce.Get(MeshConfig, MeshConfigMesh, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, qm) |
|
require.NotEqual(t, 0, qm.RequestTime) |
|
|
|
result, ok := entry.(*MeshConfigEntry) |
|
require.True(t, ok) |
|
|
|
// ignore indexes |
|
result.CreateIndex = 0 |
|
result.ModifyIndex = 0 |
|
require.Equal(t, mesh, result) |
|
}) |
|
|
|
testutil.RunStep(t, "list", func(t *testing.T) { |
|
entries, qm, err := ce.List(MeshConfig, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, qm) |
|
require.NotEqual(t, 0, qm.RequestTime) |
|
require.Len(t, entries, 1) |
|
}) |
|
|
|
testutil.RunStep(t, "delete", func(t *testing.T) { |
|
wm, err := ce.Delete(MeshConfig, MeshConfigMesh, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, wm) |
|
require.NotEqual(t, 0, wm.RequestTime) |
|
|
|
// verify deletion |
|
_, _, err = ce.Get(MeshConfig, MeshConfigMesh, nil) |
|
require.Error(t, err) |
|
}) |
|
}) |
|
|
|
t.Run("CAS deletion", func(t *testing.T) { |
|
|
|
entry := &ProxyConfigEntry{ |
|
Kind: ProxyDefaults, |
|
Name: ProxyConfigGlobal, |
|
Config: map[string]interface{}{ |
|
"foo": "bar", |
|
}, |
|
} |
|
|
|
// Create a config entry. |
|
created, _, err := config_entries.Set(entry, nil) |
|
require.NoError(t, err) |
|
require.True(t, created, "entry should have been created") |
|
|
|
// Read it back to get the ModifyIndex. |
|
result, _, err := config_entries.Get(entry.Kind, entry.Name, nil) |
|
require.NoError(t, err) |
|
require.NotNil(t, entry) |
|
|
|
// Attempt a deletion with an invalid index. |
|
deleted, _, err := config_entries.DeleteCAS(entry.Kind, entry.Name, result.GetModifyIndex()-1, nil) |
|
require.NoError(t, err) |
|
require.False(t, deleted, "entry should not have been deleted") |
|
|
|
// Attempt a deletion with a valid index. |
|
deleted, _, err = config_entries.DeleteCAS(entry.Kind, entry.Name, result.GetModifyIndex(), nil) |
|
require.NoError(t, err) |
|
require.True(t, deleted, "entry should have been deleted") |
|
}) |
|
} |
|
|
|
func TestDecodeConfigEntry(t *testing.T) { |
|
t.Parallel() |
|
|
|
for _, tc := range []struct { |
|
name string |
|
body string |
|
expect ConfigEntry |
|
expectErr string |
|
}{ |
|
{ |
|
name: "expose-paths: kitchen sink proxy", |
|
body: ` |
|
{ |
|
"Kind": "proxy-defaults", |
|
"Name": "global", |
|
"Expose": { |
|
"Checks": true, |
|
"Paths": [ |
|
{ |
|
"LocalPathPort": 8080, |
|
"ListenerPort": 21500, |
|
"Path": "/healthz", |
|
"Protocol": "http2" |
|
} |
|
] |
|
} |
|
} |
|
`, |
|
expect: &ProxyConfigEntry{ |
|
Kind: "proxy-defaults", |
|
Name: "global", |
|
Expose: ExposeConfig{ |
|
Checks: true, |
|
Paths: []ExposePath{ |
|
{ |
|
LocalPathPort: 8080, |
|
ListenerPort: 21500, |
|
Path: "/healthz", |
|
Protocol: "http2", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "expose-paths: kitchen sink service default", |
|
body: ` |
|
{ |
|
"Kind": "service-defaults", |
|
"Name": "global", |
|
"Expose": { |
|
"Checks": true, |
|
"Paths": [ |
|
{ |
|
"LocalPathPort": 8080, |
|
"ListenerPort": 21500, |
|
"Path": "/healthz", |
|
"Protocol": "http2" |
|
} |
|
] |
|
} |
|
} |
|
`, |
|
expect: &ServiceConfigEntry{ |
|
Kind: "service-defaults", |
|
Name: "global", |
|
Expose: ExposeConfig{ |
|
Checks: true, |
|
Paths: []ExposePath{ |
|
{ |
|
LocalPathPort: 8080, |
|
ListenerPort: 21500, |
|
Path: "/healthz", |
|
Protocol: "http2", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "proxy-defaults", |
|
body: ` |
|
{ |
|
"Kind": "proxy-defaults", |
|
"Name": "main", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"Config": { |
|
"foo": 19, |
|
"bar": "abc", |
|
"moreconfig": { |
|
"moar": "config" |
|
} |
|
}, |
|
"MeshGateway": { |
|
"Mode": "remote" |
|
}, |
|
"Mode": "transparent", |
|
"TransparentProxy": { |
|
"OutboundListenerPort": 808, |
|
"DialedDirectly": true |
|
}, |
|
"AccessLogs": { |
|
"Enabled": true, |
|
"DisableListenerLogs": true, |
|
"Type": "file", |
|
"Path": "/tmp/logs.txt", |
|
"TextFormat": "[%START_TIME%]" |
|
}, |
|
"FailoverPolicy": { |
|
"Mode": "default" |
|
} |
|
} |
|
`, |
|
expect: &ProxyConfigEntry{ |
|
Kind: "proxy-defaults", |
|
Name: "main", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
Config: map[string]interface{}{ |
|
"foo": float64(19), |
|
"bar": "abc", |
|
"moreconfig": map[string]interface{}{ |
|
"moar": "config", |
|
}, |
|
}, |
|
MeshGateway: MeshGatewayConfig{ |
|
Mode: MeshGatewayModeRemote, |
|
}, |
|
Mode: ProxyModeTransparent, |
|
TransparentProxy: &TransparentProxyConfig{ |
|
OutboundListenerPort: 808, |
|
DialedDirectly: true, |
|
}, |
|
AccessLogs: &AccessLogsConfig{ |
|
Enabled: true, |
|
DisableListenerLogs: true, |
|
Type: FileLogSinkType, |
|
Path: "/tmp/logs.txt", |
|
TextFormat: "[%START_TIME%]", |
|
}, |
|
FailoverPolicy: &ServiceResolverFailoverPolicy{ |
|
Mode: "default", |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-defaults", |
|
body: ` |
|
{ |
|
"Kind": "service-defaults", |
|
"Name": "main", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"Protocol": "http", |
|
"ExternalSNI": "abc-123", |
|
"MeshGateway": { |
|
"Mode": "remote" |
|
}, |
|
"Mode": "transparent", |
|
"TransparentProxy": { |
|
"OutboundListenerPort": 808, |
|
"DialedDirectly": true |
|
}, |
|
"BalanceInboundConnections": "exact_balance", |
|
"UpstreamConfig": { |
|
"Overrides": [ |
|
{ |
|
"Name": "redis", |
|
"PassiveHealthCheck": { |
|
"MaxFailures": 3, |
|
"Interval": "2s", |
|
"EnforcingConsecutive5xx": 60, |
|
"MaxEjectionPercent": 4, |
|
"BaseEjectionTime": "5s" |
|
}, |
|
"BalanceOutboundConnections": "exact_balance" |
|
}, |
|
{ |
|
"Name": "finance--billing", |
|
"MeshGateway": { |
|
"Mode": "remote" |
|
} |
|
} |
|
], |
|
"Defaults": { |
|
"EnvoyClusterJSON": "zip", |
|
"EnvoyListenerJSON": "zop", |
|
"ConnectTimeoutMs": 5000, |
|
"Protocol": "http", |
|
"Limits": { |
|
"MaxConnections": 3, |
|
"MaxPendingRequests": 4, |
|
"MaxConcurrentRequests": 5 |
|
}, |
|
"PassiveHealthCheck": { |
|
"MaxFailures": 5, |
|
"Interval": "4s", |
|
"EnforcingConsecutive5xx": 61, |
|
"MaxEjectionPercent": 5, |
|
"BaseEjectionTime": "6s" |
|
|
|
} |
|
} |
|
} |
|
} |
|
`, |
|
expect: &ServiceConfigEntry{ |
|
Kind: "service-defaults", |
|
Name: "main", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
Protocol: "http", |
|
ExternalSNI: "abc-123", |
|
MeshGateway: MeshGatewayConfig{ |
|
Mode: MeshGatewayModeRemote, |
|
}, |
|
Mode: ProxyModeTransparent, |
|
TransparentProxy: &TransparentProxyConfig{ |
|
OutboundListenerPort: 808, |
|
DialedDirectly: true, |
|
}, |
|
BalanceInboundConnections: "exact_balance", |
|
UpstreamConfig: &UpstreamConfiguration{ |
|
Overrides: []*UpstreamConfig{ |
|
{ |
|
Name: "redis", |
|
PassiveHealthCheck: &PassiveHealthCheck{ |
|
MaxFailures: 3, |
|
Interval: 2 * time.Second, |
|
EnforcingConsecutive5xx: uint32Pointer(60), |
|
MaxEjectionPercent: uint32Pointer(4), |
|
BaseEjectionTime: durationPointer(5 * time.Second), |
|
}, |
|
BalanceOutboundConnections: "exact_balance", |
|
}, |
|
{ |
|
Name: "finance--billing", |
|
MeshGateway: MeshGatewayConfig{Mode: "remote"}, |
|
}, |
|
}, |
|
Defaults: &UpstreamConfig{ |
|
EnvoyClusterJSON: "zip", |
|
EnvoyListenerJSON: "zop", |
|
Protocol: "http", |
|
ConnectTimeoutMs: 5000, |
|
Limits: &UpstreamLimits{ |
|
MaxConnections: intPointer(3), |
|
MaxPendingRequests: intPointer(4), |
|
MaxConcurrentRequests: intPointer(5), |
|
}, |
|
PassiveHealthCheck: &PassiveHealthCheck{ |
|
MaxFailures: 5, |
|
Interval: 4 * time.Second, |
|
EnforcingConsecutive5xx: uint32Pointer(61), |
|
MaxEjectionPercent: uint32Pointer(5), |
|
BaseEjectionTime: durationPointer(6 * time.Second), |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-defaults-endpoint", |
|
body: ` |
|
{ |
|
"Kind": "service-defaults", |
|
"Name": "external", |
|
"Protocol": "http", |
|
"Destination": { |
|
"Addresses": [ |
|
"1.2.3.4" |
|
], |
|
"Port": 443 |
|
} |
|
} |
|
`, |
|
expect: &ServiceConfigEntry{ |
|
Kind: "service-defaults", |
|
Name: "external", |
|
Protocol: "http", |
|
Destination: &DestinationConfig{ |
|
Addresses: []string{"1.2.3.4"}, |
|
Port: 443, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-router: kitchen sink", |
|
body: ` |
|
{ |
|
"Kind": "service-router", |
|
"Name": "main", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"Routes": [ |
|
{ |
|
"Match": { |
|
"HTTP": { |
|
"PathExact": "/foo", |
|
"Header": [ |
|
{ |
|
"Name": "debug1", |
|
"Present": true |
|
}, |
|
{ |
|
"Name": "debug2", |
|
"Present": false, |
|
"Invert": true |
|
}, |
|
{ |
|
"Name": "debug3", |
|
"Exact": "1" |
|
}, |
|
{ |
|
"Name": "debug4", |
|
"Prefix": "aaa" |
|
}, |
|
{ |
|
"Name": "debug5", |
|
"Suffix": "bbb" |
|
}, |
|
{ |
|
"Name": "debug6", |
|
"Regex": "a.*z" |
|
} |
|
] |
|
} |
|
}, |
|
"Destination": { |
|
"Service": "carrot", |
|
"ServiceSubset": "kale", |
|
"Namespace": "leek", |
|
"PrefixRewrite": "/alternate", |
|
"RequestTimeout": "99s", |
|
"IdleTimeout": "99s", |
|
"NumRetries": 12345, |
|
"RetryOnConnectFailure": true, |
|
"RetryOnStatusCodes": [401, 209] |
|
} |
|
}, |
|
{ |
|
"Match": { |
|
"HTTP": { |
|
"PathPrefix": "/foo", |
|
"Methods": [ "GET", "DELETE" ], |
|
"QueryParam": [ |
|
{ |
|
"Name": "hack1", |
|
"Present": true |
|
}, |
|
{ |
|
"Name": "hack2", |
|
"Exact": "1" |
|
}, |
|
{ |
|
"Name": "hack3", |
|
"Regex": "a.*z" |
|
} |
|
] |
|
} |
|
} |
|
}, |
|
{ |
|
"Match": { |
|
"HTTP": { |
|
"PathRegex": "/foo" |
|
} |
|
} |
|
} |
|
] |
|
} |
|
`, |
|
expect: &ServiceRouterConfigEntry{ |
|
Kind: "service-router", |
|
Name: "main", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
Routes: []ServiceRoute{ |
|
{ |
|
Match: &ServiceRouteMatch{ |
|
HTTP: &ServiceRouteHTTPMatch{ |
|
PathExact: "/foo", |
|
Header: []ServiceRouteHTTPMatchHeader{ |
|
{ |
|
Name: "debug1", |
|
Present: true, |
|
}, |
|
{ |
|
Name: "debug2", |
|
Present: false, |
|
Invert: true, |
|
}, |
|
{ |
|
Name: "debug3", |
|
Exact: "1", |
|
}, |
|
{ |
|
Name: "debug4", |
|
Prefix: "aaa", |
|
}, |
|
{ |
|
Name: "debug5", |
|
Suffix: "bbb", |
|
}, |
|
{ |
|
Name: "debug6", |
|
Regex: "a.*z", |
|
}, |
|
}, |
|
}, |
|
}, |
|
Destination: &ServiceRouteDestination{ |
|
Service: "carrot", |
|
ServiceSubset: "kale", |
|
Namespace: "leek", |
|
PrefixRewrite: "/alternate", |
|
RequestTimeout: 99 * time.Second, |
|
IdleTimeout: 99 * time.Second, |
|
NumRetries: 12345, |
|
RetryOnConnectFailure: true, |
|
RetryOnStatusCodes: []uint32{401, 209}, |
|
}, |
|
}, |
|
{ |
|
Match: &ServiceRouteMatch{ |
|
HTTP: &ServiceRouteHTTPMatch{ |
|
PathPrefix: "/foo", |
|
Methods: []string{"GET", "DELETE"}, |
|
QueryParam: []ServiceRouteHTTPMatchQueryParam{ |
|
{ |
|
Name: "hack1", |
|
Present: true, |
|
}, |
|
{ |
|
Name: "hack2", |
|
Exact: "1", |
|
}, |
|
{ |
|
Name: "hack3", |
|
Regex: "a.*z", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
Match: &ServiceRouteMatch{ |
|
HTTP: &ServiceRouteHTTPMatch{ |
|
PathRegex: "/foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-splitter: kitchen sink", |
|
body: ` |
|
{ |
|
"Kind": "service-splitter", |
|
"Name": "main", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"Splits": [ |
|
{ |
|
"Weight": 99.1, |
|
"ServiceSubset": "v1" |
|
}, |
|
{ |
|
"Weight": 0.9, |
|
"Service": "other", |
|
"Namespace": "alt" |
|
} |
|
] |
|
} |
|
`, |
|
expect: &ServiceSplitterConfigEntry{ |
|
Kind: ServiceSplitter, |
|
Name: "main", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
Splits: []ServiceSplit{ |
|
{ |
|
Weight: 99.1, |
|
ServiceSubset: "v1", |
|
}, |
|
{ |
|
Weight: 0.9, |
|
Service: "other", |
|
Namespace: "alt", |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-resolver: subsets with failover", |
|
body: ` |
|
{ |
|
"Kind": "service-resolver", |
|
"Name": "main", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"DefaultSubset": "v1", |
|
"ConnectTimeout": "15s", |
|
"Subsets": { |
|
"v1": { |
|
"Filter": "Service.Meta.version == v1" |
|
}, |
|
"v2": { |
|
"Filter": "Service.Meta.version == v2", |
|
"OnlyPassing": true |
|
} |
|
}, |
|
"Failover": { |
|
"v2": { |
|
"Service": "failcopy", |
|
"ServiceSubset": "sure", |
|
"Namespace": "neighbor", |
|
"Datacenters": ["dc5", "dc14"] |
|
}, |
|
"*": { |
|
"Datacenters": ["dc7"] |
|
} |
|
} |
|
}`, |
|
expect: &ServiceResolverConfigEntry{ |
|
Kind: "service-resolver", |
|
Name: "main", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
DefaultSubset: "v1", |
|
ConnectTimeout: 15 * time.Second, |
|
Subsets: map[string]ServiceResolverSubset{ |
|
"v1": { |
|
Filter: "Service.Meta.version == v1", |
|
}, |
|
"v2": { |
|
Filter: "Service.Meta.version == v2", |
|
OnlyPassing: true, |
|
}, |
|
}, |
|
Failover: map[string]ServiceResolverFailover{ |
|
"v2": { |
|
Service: "failcopy", |
|
ServiceSubset: "sure", |
|
Namespace: "neighbor", |
|
Datacenters: []string{"dc5", "dc14"}, |
|
}, |
|
"*": { |
|
Datacenters: []string{"dc7"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-resolver: redirect", |
|
body: ` |
|
{ |
|
"Kind": "service-resolver", |
|
"Name": "main", |
|
"Redirect": { |
|
"Service": "other", |
|
"ServiceSubset": "backup", |
|
"Namespace": "alt", |
|
"Datacenter": "dc9" |
|
} |
|
} |
|
`, |
|
expect: &ServiceResolverConfigEntry{ |
|
Kind: "service-resolver", |
|
Name: "main", |
|
Redirect: &ServiceResolverRedirect{ |
|
Service: "other", |
|
ServiceSubset: "backup", |
|
Namespace: "alt", |
|
Datacenter: "dc9", |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-resolver: default", |
|
body: ` |
|
{ |
|
"Kind": "service-resolver", |
|
"Name": "main" |
|
} |
|
`, |
|
expect: &ServiceResolverConfigEntry{ |
|
Kind: "service-resolver", |
|
Name: "main", |
|
}, |
|
}, |
|
{ |
|
name: "service-resolver: envoy hash lb kitchen sink", |
|
body: ` |
|
{ |
|
"Kind": "service-resolver", |
|
"Name": "main", |
|
"LoadBalancer": { |
|
"Policy": "ring_hash", |
|
"RingHashConfig": { |
|
"MinimumRingSize": 1, |
|
"MaximumRingSize": 2 |
|
}, |
|
"HashPolicies": [ |
|
{ |
|
"Field": "cookie", |
|
"FieldValue": "good-cookie", |
|
"CookieConfig": { |
|
"TTL": "1s", |
|
"Path": "/oven" |
|
}, |
|
"Terminal": true |
|
}, |
|
{ |
|
"Field": "cookie", |
|
"FieldValue": "less-good-cookie", |
|
"CookieConfig": { |
|
"Session": true, |
|
"Path": "/toaster" |
|
}, |
|
"Terminal": true |
|
}, |
|
{ |
|
"Field": "header", |
|
"FieldValue": "x-user-id" |
|
}, |
|
{ |
|
"SourceIP": true |
|
} |
|
] |
|
} |
|
} |
|
`, |
|
expect: &ServiceResolverConfigEntry{ |
|
Kind: "service-resolver", |
|
Name: "main", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: "ring_hash", |
|
RingHashConfig: &RingHashConfig{ |
|
MinimumRingSize: 1, |
|
MaximumRingSize: 2, |
|
}, |
|
HashPolicies: []HashPolicy{ |
|
{ |
|
Field: "cookie", |
|
FieldValue: "good-cookie", |
|
CookieConfig: &CookieConfig{ |
|
TTL: 1 * time.Second, |
|
Path: "/oven", |
|
}, |
|
Terminal: true, |
|
}, |
|
{ |
|
Field: "cookie", |
|
FieldValue: "less-good-cookie", |
|
CookieConfig: &CookieConfig{ |
|
Session: true, |
|
Path: "/toaster", |
|
}, |
|
Terminal: true, |
|
}, |
|
{ |
|
Field: "header", |
|
FieldValue: "x-user-id", |
|
}, |
|
{ |
|
SourceIP: true, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-resolver: envoy least request kitchen sink", |
|
body: ` |
|
{ |
|
"Kind": "service-resolver", |
|
"Name": "main", |
|
"LoadBalancer": { |
|
"Policy": "least_request", |
|
"LeastRequestConfig": { |
|
"ChoiceCount": 2 |
|
} |
|
} |
|
} |
|
`, |
|
expect: &ServiceResolverConfigEntry{ |
|
Kind: "service-resolver", |
|
Name: "main", |
|
LoadBalancer: &LoadBalancer{ |
|
Policy: "least_request", |
|
LeastRequestConfig: &LeastRequestConfig{ |
|
ChoiceCount: 2, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
// TODO(rb): test SDS stuff here in both places (global/service) |
|
name: "ingress-gateway", |
|
body: ` |
|
{ |
|
"Kind": "ingress-gateway", |
|
"Name": "ingress-web", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"Tls": { |
|
"Enabled": true, |
|
"TLSMinVersion": "TLSv1_1", |
|
"TLSMaxVersion": "TLSv1_2", |
|
"CipherSuites": [ |
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", |
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" |
|
] |
|
}, |
|
"Listeners": [ |
|
{ |
|
"Port": 8080, |
|
"Protocol": "http", |
|
"Services": [ |
|
{ |
|
"Name": "web", |
|
"Namespace": "foo", |
|
"Partition": "bar" |
|
}, |
|
{ |
|
"Name": "db" |
|
} |
|
] |
|
}, |
|
{ |
|
"Port": 9999, |
|
"Protocol": "tcp", |
|
"Services": [ |
|
{ |
|
"Name": "mysql" |
|
} |
|
] |
|
} |
|
] |
|
} |
|
`, |
|
expect: &IngressGatewayConfigEntry{ |
|
Kind: "ingress-gateway", |
|
Name: "ingress-web", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
TLS: GatewayTLSConfig{ |
|
Enabled: true, |
|
TLSMinVersion: "TLSv1_1", |
|
TLSMaxVersion: "TLSv1_2", |
|
CipherSuites: []string{ |
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", |
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", |
|
}, |
|
}, |
|
Listeners: []IngressListener{ |
|
{ |
|
Port: 8080, |
|
Protocol: "http", |
|
Services: []IngressService{ |
|
{ |
|
Name: "web", |
|
Namespace: "foo", |
|
Partition: "bar", |
|
}, |
|
{ |
|
Name: "db", |
|
}, |
|
}, |
|
}, |
|
{ |
|
Port: 9999, |
|
Protocol: "tcp", |
|
Services: []IngressService{ |
|
{ |
|
Name: "mysql", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "terminating-gateway", |
|
body: ` |
|
{ |
|
"Kind": "terminating-gateway", |
|
"Name": "terminating-west", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"Services": [ |
|
{ |
|
"Namespace": "foo", |
|
"Name": "web", |
|
"CAFile": "/etc/ca.pem", |
|
"CertFile": "/etc/cert.pem", |
|
"KeyFile": "/etc/tls.key", |
|
"SNI": "mydomain" |
|
}, |
|
{ |
|
"Name": "api" |
|
}, |
|
{ |
|
"Namespace": "bar", |
|
"Name": "*" |
|
} |
|
] |
|
}`, |
|
expect: &TerminatingGatewayConfigEntry{ |
|
Kind: "terminating-gateway", |
|
Name: "terminating-west", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
Services: []LinkedService{ |
|
{ |
|
Namespace: "foo", |
|
Name: "web", |
|
CAFile: "/etc/ca.pem", |
|
CertFile: "/etc/cert.pem", |
|
KeyFile: "/etc/tls.key", |
|
SNI: "mydomain", |
|
}, |
|
{ |
|
Name: "api", |
|
}, |
|
{ |
|
Namespace: "bar", |
|
Name: "*", |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-intentions: kitchen sink", |
|
body: ` |
|
{ |
|
"Kind": "service-intentions", |
|
"Name": "web", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"Sources": [ |
|
{ |
|
"Name": "foo", |
|
"Action": "deny", |
|
"Type": "consul", |
|
"Description": "foo desc" |
|
}, |
|
{ |
|
"Name": "bar", |
|
"Action": "allow", |
|
"Description": "bar desc" |
|
}, |
|
{ |
|
"Name": "l7", |
|
"Permissions": [ |
|
{ |
|
"Action": "deny", |
|
"HTTP": { |
|
"PathExact": "/admin", |
|
"Header": [ |
|
{ |
|
"Name": "hdr-present", |
|
"Present": true |
|
}, |
|
{ |
|
"Name": "hdr-exact", |
|
"Exact": "exact" |
|
}, |
|
{ |
|
"Name": "hdr-prefix", |
|
"Prefix": "prefix" |
|
}, |
|
{ |
|
"Name": "hdr-suffix", |
|
"Suffix": "suffix" |
|
}, |
|
{ |
|
"Name": "hdr-regex", |
|
"Regex": "regex" |
|
}, |
|
{ |
|
"Name": "hdr-absent", |
|
"Present": true, |
|
"Invert": true |
|
} |
|
] |
|
} |
|
}, |
|
{ |
|
"Action": "allow", |
|
"HTTP": { |
|
"PathPrefix": "/v3/" |
|
} |
|
}, |
|
{ |
|
"Action": "allow", |
|
"HTTP": { |
|
"PathRegex": "/v[12]/.*", |
|
"Methods": [ |
|
"GET", |
|
"POST" |
|
] |
|
} |
|
} |
|
] |
|
}, |
|
{ |
|
"Name": "*", |
|
"Action": "deny", |
|
"Description": "wild desc" |
|
} |
|
] |
|
} |
|
`, |
|
expect: &ServiceIntentionsConfigEntry{ |
|
Kind: "service-intentions", |
|
Name: "web", |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: "deny", |
|
Type: "consul", |
|
Description: "foo desc", |
|
}, |
|
{ |
|
Name: "bar", |
|
Action: "allow", |
|
Description: "bar desc", |
|
}, |
|
{ |
|
Name: "l7", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: "deny", |
|
HTTP: &IntentionHTTPPermission{ |
|
PathExact: "/admin", |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "hdr-present", |
|
Present: true, |
|
}, |
|
{ |
|
Name: "hdr-exact", |
|
Exact: "exact", |
|
}, |
|
{ |
|
Name: "hdr-prefix", |
|
Prefix: "prefix", |
|
}, |
|
{ |
|
Name: "hdr-suffix", |
|
Suffix: "suffix", |
|
}, |
|
{ |
|
Name: "hdr-regex", |
|
Regex: "regex", |
|
}, |
|
{ |
|
Name: "hdr-absent", |
|
Present: true, |
|
Invert: true, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
Action: "allow", |
|
HTTP: &IntentionHTTPPermission{ |
|
PathPrefix: "/v3/", |
|
}, |
|
}, |
|
{ |
|
Action: "allow", |
|
HTTP: &IntentionHTTPPermission{ |
|
PathRegex: "/v[12]/.*", |
|
Methods: []string{"GET", "POST"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
Name: "*", |
|
Action: "deny", |
|
Description: "wild desc", |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "mesh", |
|
body: ` |
|
{ |
|
"Kind": "mesh", |
|
"Meta" : { |
|
"foo": "bar", |
|
"gir": "zim" |
|
}, |
|
"TransparentProxy": { |
|
"MeshDestinationsOnly": true |
|
}, |
|
"TLS": { |
|
"Incoming": { |
|
"TLSMinVersion": "TLSv1_1", |
|
"TLSMaxVersion": "TLSv1_2", |
|
"CipherSuites": [ |
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", |
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" |
|
] |
|
}, |
|
"Outgoing": { |
|
"TLSMinVersion": "TLSv1_1", |
|
"TLSMaxVersion": "TLSv1_2", |
|
"CipherSuites": [ |
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", |
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" |
|
] |
|
} |
|
}, |
|
"HTTP": { |
|
"SanitizeXForwardedClientCert": true |
|
}, |
|
"Peering": { |
|
"PeerThroughMeshGateways": true |
|
} |
|
} |
|
`, |
|
expect: &MeshConfigEntry{ |
|
Meta: map[string]string{ |
|
"foo": "bar", |
|
"gir": "zim", |
|
}, |
|
TransparentProxy: TransparentProxyMeshConfig{ |
|
MeshDestinationsOnly: true, |
|
}, |
|
TLS: &MeshTLSConfig{ |
|
Incoming: &MeshDirectionalTLSConfig{ |
|
TLSMinVersion: "TLSv1_1", |
|
TLSMaxVersion: "TLSv1_2", |
|
CipherSuites: []string{ |
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", |
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", |
|
}, |
|
}, |
|
Outgoing: &MeshDirectionalTLSConfig{ |
|
TLSMinVersion: "TLSv1_1", |
|
TLSMaxVersion: "TLSv1_2", |
|
CipherSuites: []string{ |
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", |
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", |
|
}, |
|
}, |
|
}, |
|
HTTP: &MeshHTTPConfig{ |
|
SanitizeXForwardedClientCert: true, |
|
}, |
|
Peering: &PeeringMeshConfig{ |
|
PeerThroughMeshGateways: true, |
|
}, |
|
}, |
|
}, |
|
} { |
|
tc := tc |
|
|
|
t.Run(tc.name+": DecodeConfigEntry", func(t *testing.T) { |
|
var raw map[string]interface{} |
|
require.NoError(t, json.Unmarshal([]byte(tc.body), &raw)) |
|
|
|
got, err := DecodeConfigEntry(raw) |
|
require.NoError(t, err) |
|
require.Equal(t, tc.expect, got) |
|
}) |
|
|
|
t.Run(tc.name+": DecodeConfigEntryFromJSON", func(t *testing.T) { |
|
got, err := DecodeConfigEntryFromJSON([]byte(tc.body)) |
|
require.NoError(t, err) |
|
require.Equal(t, tc.expect, got) |
|
}) |
|
|
|
t.Run(tc.name+": DecodeConfigEntrySlice", func(t *testing.T) { |
|
var raw []map[string]interface{} |
|
require.NoError(t, json.Unmarshal([]byte("["+tc.body+"]"), &raw)) |
|
|
|
got, err := decodeConfigEntrySlice(raw) |
|
require.NoError(t, err) |
|
require.Len(t, got, 1) |
|
require.Equal(t, tc.expect, got[0]) |
|
}) |
|
} |
|
} |
|
|
|
func intPointer(v int) *int { |
|
return &v |
|
} |
|
|
|
func uint32Pointer(v uint32) *uint32 { |
|
return &v |
|
} |
|
|
|
func durationPointer(d time.Duration) *time.Duration { |
|
return &d |
|
}
|
|
|