Nitya Dhanushkodi 8728a4496c
troubleshoot: service to service validation (#16096)
* Add Tproxy support to Envoy Extensions (this is needed for service to service validation)

* Add validation for Envoy configuration for an upstream service

* Use both /config_dump and /cluster to validate Envoy configuration
This is because of a bug in Envoy where the EndpointsConfigDump does not
include a cluster_name, making it impossible to match an endpoint to
verify it exists.

This removes endpoints support for builtin extensions since only the
validate plugin was using it, and it is no longer used. It also removes
test cases for endpoint validation. Endpoints validation now only occurs
in the top level test from config_dump and clusters json files.

Co-authored-by: Eric <eric@haberkorn.co>
2023-01-27 11:43:16 -08:00

305 lines
7.2 KiB
Go

package validate
import (
"testing"
envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_aggregate_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3"
"github.com/hashicorp/consul/agent/xds/xdscommon"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/anypb"
)
func TestErrors(t *testing.T) {
cases := map[string]struct {
validate func() *Validate
endpointValidator EndpointValidator
err string
}{
"success": {
validate: func() *Validate {
return &Validate{
envoyID: "db",
snis: map[string]struct{}{
"db-sni": {},
},
listener: true,
usesRDS: true,
route: true,
resources: map[string]*resource{
"db-sni": {
required: true,
cluster: true,
},
},
}
},
endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
r.loadAssignment = true
r.endpoints = 1
},
},
"no clusters for listener or route": {
validate: func() *Validate {
return &Validate{
envoyID: "db",
snis: map[string]struct{}{
"db-sni": {},
},
listener: true,
usesRDS: true,
route: true,
resources: map[string]*resource{
"db-sni": {
required: false,
cluster: true,
},
},
}
},
endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
r.loadAssignment = true
r.endpoints = 1
},
err: "no clusters found on route or listener",
},
"no healthy endpoints": {
validate: func() *Validate {
return &Validate{
envoyID: "db",
snis: map[string]struct{}{
"db-sni": {},
},
listener: true,
usesRDS: true,
route: true,
resources: map[string]*resource{
"db-sni": {
required: true,
cluster: true,
},
},
}
},
endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
r.loadAssignment = true
},
err: "zero healthy endpoints",
},
"success: aggregate cluster with one target with endpoints": {
validate: func() *Validate {
return &Validate{
envoyID: "db",
snis: map[string]struct{}{
"db-sni": {},
"db-fail-1-sni": {},
"db-fail-2-sni": {},
},
listener: true,
usesRDS: true,
route: true,
resources: map[string]*resource{
"db-sni": {
required: true,
cluster: true,
aggregateCluster: true,
aggregateClusterChildren: []string{
"db-fail-1-sni",
"db-fail-2-sni",
},
},
"db-fail-1-sni": {
required: true,
cluster: true,
parentCluster: "db-sni",
// This doesn't usually get set here, but this tests that at least one child cluster has
// healthy endpoints case.
endpoints: 1,
},
"db-fail-2-sni": {
required: true,
cluster: true,
parentCluster: "db-sni",
},
},
}
},
endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
r.loadAssignment = true
},
},
"aggregate cluster no healthy endpoints": {
validate: func() *Validate {
return &Validate{
envoyID: "db",
snis: map[string]struct{}{
"db-sni": {},
"db-fail-1-sni": {},
"db-fail-2-sni": {},
},
listener: true,
usesRDS: true,
route: true,
resources: map[string]*resource{
"db-sni": {
required: true,
cluster: true,
aggregateCluster: true,
aggregateClusterChildren: []string{
"db-fail-1-sni",
"db-fail-2-sni",
},
},
"db-fail-1-sni": {
required: true,
cluster: true,
parentCluster: "db-sni",
},
"db-fail-2-sni": {
required: true,
cluster: true,
parentCluster: "db-sni",
},
},
}
},
endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
r.loadAssignment = true
r.endpoints = 0
},
err: "zero healthy endpoints for aggregate cluster",
},
}
for n, tc := range cases {
t.Run(n, func(t *testing.T) {
v := tc.validate()
err := v.Errors(true, tc.endpointValidator, nil)
if len(tc.err) == 0 {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.err)
}
})
}
}
func TestIsAggregateCluster(t *testing.T) {
aggregateClusterConfig, err := anypb.New(&envoy_aggregate_cluster_v3.ClusterConfig{
Clusters: []string{"c1", "c2"},
})
require.NoError(t, err)
cases := map[string]struct {
input *envoy_cluster_v3.Cluster
expectedAggregateCluster *envoy_aggregate_cluster_v3.ClusterConfig
expectedOk bool
}{
"non-aggregate cluster": {
input: &envoy_cluster_v3.Cluster{
Name: "foo",
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_LOGICAL_DNS},
},
expectedOk: false,
},
"valid aggregate cluster": {
input: &envoy_cluster_v3.Cluster{
Name: "foo",
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{
ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{
Name: "foo",
TypedConfig: aggregateClusterConfig,
},
},
},
expectedOk: true,
expectedAggregateCluster: &envoy_aggregate_cluster_v3.ClusterConfig{Clusters: []string{"c1", "c2"}},
},
}
for n, tc := range cases {
t.Run(n, func(t *testing.T) {
ac, ok := isAggregateCluster(tc.input)
require.Equal(t, tc.expectedOk, ok)
if tc.expectedOk {
require.Equal(t, tc.expectedAggregateCluster.Clusters, ac.Clusters)
}
})
}
}
func TestMakeValidate(t *testing.T) {
cases := map[string]struct {
extensionName string
arguments map[string]interface{}
expected *Validate
snis map[string]struct{}
ok bool
}{
"with no arguments": {
arguments: nil,
ok: false,
},
"with an invalid name": {
arguments: map[string]interface{}{
"envoyID": "id",
},
extensionName: "bad",
ok: false,
},
"empty envoy ID": {
arguments: map[string]interface{}{"envoyID": ""},
ok: false,
},
"valid everything": {
arguments: map[string]interface{}{
"envoyID": "id",
},
snis: map[string]struct{}{
"sni1": {},
"sni2": {},
},
expected: &Validate{
envoyID: "id",
resources: map[string]*resource{},
},
ok: true,
},
}
for n, tc := range cases {
t.Run(n, func(t *testing.T) {
extensionName := builtinValidateExtension
if tc.extensionName != "" {
extensionName = tc.extensionName
}
svc := api.CompoundServiceName{Name: "svc"}
ext := xdscommon.ExtensionConfiguration{
ServiceName: svc,
EnvoyExtension: api.EnvoyExtension{
Name: extensionName,
Arguments: tc.arguments,
},
}
patcher, err := MakeValidate(ext)
if tc.ok {
require.NoError(t, err)
require.Equal(t, tc.expected, patcher)
} else {
require.Error(t, err)
}
})
}
}