consul/troubleshoot/validate/validate_test.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 := api.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: {
SNIs: tc.snis,
},
},
}
patcher, err := MakeValidate(ext)
if tc.ok {
require.NoError(t, err)
require.Equal(t, tc.expected, patcher)
} else {
require.Error(t, err)
}
})
}
}