368 lines
8.7 KiB
Go
368 lines
8.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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/api"
|
|
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
|
|
"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: "No healthy endpoints for cluster \"db-sni\" for upstream \"db\"",
|
|
},
|
|
"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: "No healthy endpoints for aggregate cluster \"db-sni\" for upstream \"db\"",
|
|
},
|
|
"success: passthrough cluster doesn't error even though there are zero endpoints": {
|
|
validate: func() *Validate {
|
|
return &Validate{
|
|
envoyID: "db",
|
|
snis: map[string]struct{}{
|
|
"passthrough~db-sni": {},
|
|
},
|
|
listener: true,
|
|
usesRDS: true,
|
|
route: true,
|
|
resources: map[string]*resource{
|
|
"passthrough~db-sni": {
|
|
required: true,
|
|
cluster: true,
|
|
},
|
|
},
|
|
}
|
|
},
|
|
endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) {
|
|
r.loadAssignment = true
|
|
},
|
|
},
|
|
}
|
|
|
|
for n, tc := range cases {
|
|
t.Run(n, func(t *testing.T) {
|
|
v := tc.validate()
|
|
messages := v.GetMessages(true, tc.endpointValidator, nil)
|
|
|
|
var outputErrors string
|
|
for _, msgError := range messages.Errors() {
|
|
outputErrors += msgError.Message
|
|
for _, action := range msgError.PossibleActions {
|
|
outputErrors += action
|
|
}
|
|
}
|
|
if tc.err == "" {
|
|
require.True(t, messages.Success())
|
|
} else {
|
|
require.Contains(t, outputErrors, 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",
|
|
},
|
|
snis: map[string]struct{}{
|
|
"sni1": {},
|
|
"sni2": {},
|
|
},
|
|
extensionName: "bad",
|
|
ok: false,
|
|
},
|
|
"empty envoy ID": {
|
|
arguments: map[string]interface{}{"envoyID": ""},
|
|
snis: map[string]struct{}{
|
|
"sni1": {},
|
|
"sni2": {},
|
|
},
|
|
ok: false,
|
|
},
|
|
"missing snis": {
|
|
arguments: map[string]interface{}{
|
|
"envoyID": "id",
|
|
},
|
|
expected: &Validate{
|
|
envoyID: "id",
|
|
resources: map[string]*resource{},
|
|
snis: map[string]struct{}{
|
|
"sni1": {},
|
|
"sni2": {},
|
|
},
|
|
},
|
|
ok: false,
|
|
},
|
|
"valid everything": {
|
|
arguments: map[string]interface{}{
|
|
"envoyID": "id",
|
|
},
|
|
snis: map[string]struct{}{
|
|
"sni1": {},
|
|
"sni2": {},
|
|
},
|
|
expected: &Validate{
|
|
envoyID: "id",
|
|
resources: map[string]*resource{},
|
|
snis: map[string]struct{}{
|
|
"sni1": {},
|
|
"sni2": {},
|
|
},
|
|
},
|
|
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 := extensioncommon.RuntimeConfig{
|
|
ServiceName: svc,
|
|
EnvoyExtension: api.EnvoyExtension{
|
|
Name: extensionName,
|
|
Arguments: tc.arguments,
|
|
},
|
|
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
|
|
svc: {
|
|
SNI: tc.snis,
|
|
},
|
|
},
|
|
}
|
|
|
|
patcher, err := MakeValidate(ext)
|
|
|
|
if tc.ok {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expected, patcher)
|
|
} else {
|
|
require.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|