mirror of
https://github.com/status-im/consul.git
synced 2025-02-27 04:40:41 +00:00
* 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>
305 lines
7.2 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|