consul/agent/xds/validateupstream_test.go

358 lines
11 KiB
Go

package xds
import (
"io"
"os"
"testing"
envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
"github.com/hashicorp/consul/agent/structs"
testinf "github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/xds/proxysupport"
"github.com/hashicorp/consul/agent/xds/xdscommon"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil"
)
// TestValidateUpstreams only tests validation for listeners, routes, and clusters. Endpoints validation is done in a
// top level test that can parse the output of the /clusters endpoint.
func TestValidateUpstreams(t *testing.T) {
sni := "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
listenerName := "db:127.0.0.1:9191"
httpServiceDefaults := &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "db",
Protocol: "http",
}
dbUID := proxycfg.NewUpstreamID(&structs.Upstream{
DestinationName: "db",
LocalBindPort: 9191,
})
nodes := proxycfg.TestUpstreamNodes(t, "db")
tests := []struct {
name string
create func(t testinf.T) *proxycfg.ConfigSnapshot
patcher func(*xdscommon.IndexedResources) *xdscommon.IndexedResources
err string
peer string
serviceName *api.CompoundServiceName
vip string
}{
{
name: "tcp-success",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nil, nil)
},
},
{
name: "tcp-missing-listener",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nil, nil)
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
delete(ir.Index[xdscommon.ListenerType], listenerName)
return ir
},
err: "no listener",
},
{
name: "tcp-missing-cluster",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nil, nil)
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
delete(ir.Index[xdscommon.ClusterType], sni)
return ir
},
err: "no cluster",
},
{
name: "http-success",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nil, nil, httpServiceDefaults)
},
},
{
name: "http-rds-success",
// RDS, Envoy's Route Discovery Service, is only used for HTTP services with a customized discovery chain, so we
// need to use the test snapshot and add L7 config entries.
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nil, []proxycfg.UpdateEvent{
// The events ensure there are endpoints for the v1 and v2 subsets.
{
CorrelationID: "upstream-target:v1.db.default.default.dc1:" + dbUID.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: nodes,
},
},
{
CorrelationID: "upstream-target:v2.db.default.default.dc1:" + dbUID.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: nodes,
},
},
}, configEntriesForDBSplits()...)
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
return ir
},
},
{
name: "http-rds-missing-route",
// RDS, Envoy's Route Discovery Service, is only used for HTTP services with a customized discovery chain, so we
// need to use the test snapshot and add L7 config entries.
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nil, []proxycfg.UpdateEvent{
// The events ensure there are endpoints for the v1 and v2 subsets.
{
CorrelationID: "upstream-target:v1.db.default.default.dc1:" + dbUID.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: nodes,
},
},
{
CorrelationID: "upstream-target:v2.db.default.default.dc1:" + dbUID.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: nodes,
},
},
}, configEntriesForDBSplits()...)
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
delete(ir.Index[xdscommon.RouteType], "db")
return ir
},
err: "no route",
},
{
name: "redirect",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "redirect-to-cluster-peer", nil, nil)
},
},
{
name: "failover",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "failover", nil, nil)
},
},
{
name: "failover-to-cluster-peer",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "failover-to-cluster-peer", nil, nil)
},
},
{
name: "non-eds",
create: proxycfg.TestConfigSnapshotPeering,
serviceName: &api.CompoundServiceName{Name: "payments"},
peer: "cloud",
},
{
name: "tproxy-success",
vip: "240.0.0.1",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotTransparentProxyHTTPUpstream(t)
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
return ir
},
},
{
name: "tproxy-http-missing-cluster",
vip: "240.0.0.1",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotTransparentProxyHTTPUpstream(t)
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
sni := "google.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
delete(ir.Index[xdscommon.ClusterType], sni)
return ir
},
err: "no cluster",
},
{
name: "tproxy-http-redirect-success",
vip: "240.0.0.1",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotTransparentProxyHTTPUpstream(t, configEntriesForGoogleRedirect()...)
},
serviceName: &api.CompoundServiceName{
Name: "google",
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
return ir
},
},
{
name: "tproxy-http-split-success",
vip: "240.0.0.1",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotTransparentProxyHTTPUpstream(t, configEntriesForGoogleSplits()...)
},
serviceName: &api.CompoundServiceName{
Name: "google",
},
patcher: func(ir *xdscommon.IndexedResources) *xdscommon.IndexedResources {
return ir
},
},
}
latestEnvoyVersion := proxysupport.EnvoyVersions[0]
sf, err := determineSupportedProxyFeaturesFromString(latestEnvoyVersion)
require.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Sanity check default with no overrides first
snap := tt.create(t)
// We need to replace the TLS certs with deterministic ones to make golden
// files workable. Note we don't update these otherwise they'd change
// golden files for every test case and so not be any use!
setupTLSRootsAndLeaf(t, snap)
g := newResourceGenerator(testutil.Logger(t), nil, false)
g.ProxyFeatures = sf
res, err := g.allResourcesFromSnapshot(snap)
require.NoError(t, err)
indexedResources := indexResources(g.Logger, res)
if tt.patcher != nil {
indexedResources = tt.patcher(indexedResources)
}
serviceName := tt.serviceName
if serviceName == nil {
serviceName = &api.CompoundServiceName{
Name: "db",
}
}
peer := tt.peer
// This only tests validation for listeners, routes, and clusters. Endpoints validation is done in a top
// level test that can parse the output of the /clusters endpoint. So for this test, we set clusters to nil.
err = Validate(indexedResources, *serviceName, peer, tt.vip, false, nil)
if len(tt.err) == 0 {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tt.err)
}
})
}
}
// TODO make config.json and clusters.json use an http upstream with L7 config entries for more confidence.
func TestValidate(t *testing.T) {
indexedResources := getConfig(t)
clusters := getClusters(t)
err := Validate(indexedResources, service, "", "", true, clusters)
require.NoError(t, err)
}
// TODO: Manually inspect the config and clusters files and hardcode the list of expected resource names for higher
// confidence in these functions.
func getConfig(t *testing.T) *xdscommon.IndexedResources {
file, err := os.Open("testdata/validateupstream/config.json")
require.NoError(t, err)
jsonBytes, err := io.ReadAll(file)
require.NoError(t, err)
indexedResources, err := ParseConfigDump(jsonBytes)
require.NoError(t, err)
return indexedResources
}
func getClusters(t *testing.T) *envoy_admin_v3.Clusters {
file, err := os.Open("testdata/validateupstream/clusters.json")
require.NoError(t, err)
jsonBytes, err := io.ReadAll(file)
require.NoError(t, err)
clusters, err := ParseClusters(jsonBytes)
require.NoError(t, err)
return clusters
}
var service = api.CompoundServiceName{
Name: "backend",
}
func configEntriesForDBSplits() []structs.ConfigEntry {
httpServiceDefaults := &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "db",
Protocol: "http",
}
splitter := &structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter,
Name: "db",
Splits: []structs.ServiceSplit{
{
Weight: 50,
Service: "db",
ServiceSubset: "v1",
},
{
Weight: 50,
Service: "db",
ServiceSubset: "v2",
},
},
}
resolver := &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "db",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
"v2": {Filter: "Service.Meta.version == v2"},
},
}
return []structs.ConfigEntry{httpServiceDefaults, splitter, resolver}
}
func configEntriesForGoogleSplits() []structs.ConfigEntry {
splitter := &structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter,
Name: "google",
Splits: []structs.ServiceSplit{
{
Weight: 50,
Service: "google",
ServiceSubset: "v1",
},
{
Weight: 50,
Service: "google",
ServiceSubset: "v2",
},
},
}
resolver := &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "google",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
"v2": {Filter: "Service.Meta.version == v2"},
},
}
return []structs.ConfigEntry{splitter, resolver}
}
func configEntriesForGoogleRedirect() []structs.ConfigEntry {
redirectGoogle := &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "google",
Redirect: &structs.ServiceResolverRedirect{
Service: "google-v2",
},
}
return []structs.ConfigEntry{redirectGoogle}
}