Consul is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2846 lines
72 KiB

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
1 year ago
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/acl"
)
func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) {
// This test tests both of these because they are related functions.
newAuthz := func(t *testing.T, src string) acl.Authorizer {
policy, err := acl.NewPolicyFromSource(src, nil, nil)
require.NoError(t, err)
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
require.NoError(t, err)
return authorizer
}
newServiceACL := func(t *testing.T, canRead, canWrite []string) acl.Authorizer {
var buf bytes.Buffer
for _, s := range canRead {
buf.WriteString(fmt.Sprintf("service %q { policy = %q }\n", s, "read"))
}
for _, s := range canWrite {
buf.WriteString(fmt.Sprintf("service %q { policy = %q }\n", s, "write"))
}
policy, err := acl.NewPolicyFromSource(buf.String(), nil, nil)
require.NoError(t, err)
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
require.NoError(t, err)
return authorizer
}
newServiceAndOperatorACL := func(t *testing.T, service, operator string) acl.Authorizer {
switch {
case service != "" && operator != "":
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q } operator = %q`, service, operator))
case service == "" && operator != "":
return newAuthz(t, fmt.Sprintf(`operator = %q`, operator))
case service != "" && operator == "":
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q }`, service))
default:
t.Fatalf("one of these should be set")
return nil
}
}
newServiceAndMeshACL := func(t *testing.T, service, mesh string) acl.Authorizer {
switch {
case service != "" && mesh != "":
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q } mesh = %q`, service, mesh))
case service == "" && mesh != "":
return newAuthz(t, fmt.Sprintf(`mesh = %q`, mesh))
case service != "" && mesh == "":
return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q }`, service))
default:
t.Fatalf("one of these should be set")
return nil
}
}
type testACL = configEntryTestACL
type testcase = configEntryACLTestCase
defaultDenyCase := testACL{
name: "deny",
authorizer: newServiceACL(t, nil, nil),
canRead: false,
canWrite: false,
}
readTestCase := testACL{
name: "can read test",
authorizer: newServiceACL(t, []string{"test"}, nil),
canRead: true,
canWrite: false,
}
writeTestCase := testACL{
name: "can write test",
authorizer: newServiceACL(t, nil, []string{"test"}),
canRead: true,
canWrite: true,
}
writeTestCaseDenied := testACL{
name: "cannot write test",
authorizer: newServiceACL(t, nil, []string{"test"}),
canRead: true,
canWrite: false,
}
cases := []testcase{
{
name: "resolver: self",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
},
expectServices: nil,
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCase,
},
},
{
name: "resolver: redirect",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
Service: "other",
},
},
expectServices: []ServiceID{NewServiceID("other", nil)},
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCaseDenied,
{
name: "can write test (with other:read)",
authorizer: newServiceACL(t, []string{"other"}, []string{"test"}),
canRead: true,
canWrite: true,
},
},
},
{
name: "resolver: failover",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
"foo": {OnlyPassing: true},
"bar": {OnlyPassing: true},
},
Failover: map[string]ServiceResolverFailover{
"foo": {
Service: "other1",
},
"bar": {
Service: "other2",
},
},
},
expectServices: []ServiceID{NewServiceID("other1", nil), NewServiceID("other2", nil)},
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCaseDenied,
{
name: "can write test (with other1:read and other2:read)",
authorizer: newServiceACL(t, []string{"other1", "other2"}, []string{"test"}),
canRead: true,
canWrite: true,
},
},
},
{
name: "resolver: failover with targets",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Targets: []ServiceResolverFailoverTarget{
{Service: "other1"},
{Datacenter: "dc2"},
{Peer: "cluster-01"},
},
},
},
},
expectServices: []ServiceID{NewServiceID("other1", nil)},
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCaseDenied,
{
name: "can write test (with other1:read)",
authorizer: newServiceACL(t, []string{"other1"}, []string{"test"}),
canRead: true,
canWrite: true,
},
},
},
{
name: "splitter: self",
entry: &ServiceSplitterConfigEntry{
Kind: ServiceSplitter,
Name: "test",
Splits: []ServiceSplit{
{Weight: 100},
},
},
expectServices: nil,
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCase,
},
},
{
name: "splitter: some",
entry: &ServiceSplitterConfigEntry{
Kind: ServiceSplitter,
Name: "test",
Splits: []ServiceSplit{
{Weight: 25, Service: "b"},
{Weight: 25, Service: "a"},
{Weight: 50, Service: "c"},
},
},
expectServices: []ServiceID{NewServiceID("a", nil), NewServiceID("b", nil), NewServiceID("c", nil)},
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCaseDenied,
{
name: "can write test (with a:read, b:read, and c:read)",
authorizer: newServiceACL(t, []string{"a", "b", "c"}, []string{"test"}),
canRead: true,
canWrite: true,
},
},
},
{
name: "router: self",
entry: &ServiceRouterConfigEntry{
Kind: ServiceRouter,
Name: "test",
},
expectServices: []ServiceID{NewServiceID("test", nil)},
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCase,
},
},
{
name: "router: some",
entry: &ServiceRouterConfigEntry{
Kind: ServiceRouter,
Name: "test",
Routes: []ServiceRoute{
{
Match: &ServiceRouteMatch{HTTP: &ServiceRouteHTTPMatch{
PathPrefix: "/foo",
}},
Destination: &ServiceRouteDestination{
Service: "foo",
},
},
{
Match: &ServiceRouteMatch{HTTP: &ServiceRouteHTTPMatch{
PathPrefix: "/bar",
}},
Destination: &ServiceRouteDestination{
Service: "bar",
},
},
},
},
expectServices: []ServiceID{NewServiceID("bar", nil), NewServiceID("foo", nil), NewServiceID("test", nil)},
expectACLs: []testACL{
defaultDenyCase,
readTestCase,
writeTestCaseDenied,
{
name: "can write test (with foo:read and bar:read)",
authorizer: newServiceACL(t, []string{"foo", "bar"}, []string{"test"}),
canRead: true,
canWrite: true,
},
},
},
{
name: "ingress-gateway",
entry: &IngressGatewayConfigEntry{Name: "test"},
expectACLs: []testACL{
{
name: "no-authz",
authorizer: newAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator deny",
authorizer: newServiceAndOperatorACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator deny",
authorizer: newServiceAndOperatorACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator deny",
authorizer: newServiceAndOperatorACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh deny",
authorizer: newServiceAndMeshACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh deny",
authorizer: newServiceAndMeshACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh deny",
authorizer: newServiceAndMeshACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator read",
authorizer: newServiceAndOperatorACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator read",
authorizer: newServiceAndOperatorACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator read",
authorizer: newServiceAndOperatorACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator write",
authorizer: newServiceAndOperatorACL(t, "deny", "write"),
canRead: false,
canWrite: true,
},
{
name: "service read and operator write",
authorizer: newServiceAndOperatorACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and operator write",
authorizer: newServiceAndOperatorACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
{
name: "service deny and mesh read",
authorizer: newServiceAndMeshACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh read",
authorizer: newServiceAndMeshACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh read",
authorizer: newServiceAndMeshACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh write",
authorizer: newServiceAndMeshACL(t, "deny", "write"),
canRead: false,
canWrite: true,
},
{
name: "service read and mesh write",
authorizer: newServiceAndMeshACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and mesh write",
authorizer: newServiceAndMeshACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
},
},
Native API Gateway Config Entries (#15897) * Stub Config Entries for Consul Native API Gateway (#15644) * Add empty InlineCertificate struct and protobuf * apigateway stubs * Stub HTTPRoute in api pkg * Stub HTTPRoute in structs pkg * Simplify api.APIGatewayConfigEntry to be consistent w/ other entries * Update makeConfigEntry switch, add docstring for HTTPRouteConfigEntry * Add TCPRoute to MakeConfigEntry, return unique Kind * Stub BoundAPIGatewayConfigEntry in agent * Add RaftIndex to APIGatewayConfigEntry stub * Add new config entry kinds to validation allow-list * Add RaftIndex to other added config entry stubs * Update usage metrics assertions to include new cfg entries * Add Meta and acl.EnterpriseMeta to all new ConfigEntry types * Remove unnecessary Services field from added config entry types * Implement GetMeta(), GetEnterpriseMeta() for added config entry types * Add meta field to proto, name consistently w/ existing config entries * Format config_entry.proto * Add initial implementation of CanRead + CanWrite for new config entry types * Add unit tests for decoding of new config entry types * Add unit tests for parsing of new config entry types * Add unit tests for API Gateway config entry ACLs * Return typed PermissionDeniedError on BoundAPIGateway CanWrite * Add unit tests for added config entry ACLs * Add BoundAPIGateway type to AllConfigEntryKinds * Return proper kind from BoundAPIGateway * Add docstrings for new config entry types * Add missing config entry kinds to proto def * Update usagemetrics_oss_test.go * Use utility func for returning PermissionDeniedError * EventPublisher subscriptions for Consul Native API Gateway (#15757) * Create new event topics in subscribe proto * Add tests for PBSubscribe func * Make configs singular, add all configs to PBToStreamSubscribeRequest * Add snapshot methods * Add config_entry_events tests * Add config entry kind to topic for new configs * Add unit tests for snapshot methods * Start adding integration test * Test using the new controller code * Update agent/consul/state/config_entry_events.go * Check value of error * Add controller stubs for API Gateway (#15837) * update initial stub implementation * move files, clean up mutex references * Remove embed, use idiomatic names for constructors * Remove stray file introduced in merge * Add APIGateway validation (#15847) * Add APIGateway validation * Add additional validations * Add cert ref validation * Add protobuf definitions * Fix up field types * Add API structs * Move struct fields around a bit * APIGateway InlineCertificate validation (#15856) * Add APIGateway validation * Add additional validations * Add protobuf definitions * Tabs to spaces * Add API structs * Move struct fields around a bit * Add validation for InlineCertificate * Fix ACL test * APIGateway BoundAPIGateway validation (#15858) * Add APIGateway validation * Add additional validations * Add cert ref validation * Add protobuf definitions * Fix up field types * Add API structs * Move struct fields around a bit * Add validation for BoundAPIGateway * APIGateway TCPRoute validation (#15855) * Add APIGateway validation * Add additional validations * Add cert ref validation * Add protobuf definitions * Fix up field types * Add API structs * Add TCPRoute normalization and validation * Add forgotten Status * Add some more field docs in api package * Fix test * Format imports * Rename snapshot test variable names * Add plumbing for Native API GW Subscriptions (#16003) Co-authored-by: Sarah Alsmiller <sarah.alsmiller@hashicorp.com> Co-authored-by: Nathan Coleman <nathan.coleman@hashicorp.com> Co-authored-by: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Co-authored-by: Andrew Stucki <andrew.stucki@hashicorp.com>
2 years ago
{
name: "api-gateway",
entry: &APIGatewayConfigEntry{
Name: "test",
Listeners: []APIGatewayListener{
{
Name: "test",
Port: 100,
Protocol: "http",
},
},
},
Native API Gateway Config Entries (#15897) * Stub Config Entries for Consul Native API Gateway (#15644) * Add empty InlineCertificate struct and protobuf * apigateway stubs * Stub HTTPRoute in api pkg * Stub HTTPRoute in structs pkg * Simplify api.APIGatewayConfigEntry to be consistent w/ other entries * Update makeConfigEntry switch, add docstring for HTTPRouteConfigEntry * Add TCPRoute to MakeConfigEntry, return unique Kind * Stub BoundAPIGatewayConfigEntry in agent * Add RaftIndex to APIGatewayConfigEntry stub * Add new config entry kinds to validation allow-list * Add RaftIndex to other added config entry stubs * Update usage metrics assertions to include new cfg entries * Add Meta and acl.EnterpriseMeta to all new ConfigEntry types * Remove unnecessary Services field from added config entry types * Implement GetMeta(), GetEnterpriseMeta() for added config entry types * Add meta field to proto, name consistently w/ existing config entries * Format config_entry.proto * Add initial implementation of CanRead + CanWrite for new config entry types * Add unit tests for decoding of new config entry types * Add unit tests for parsing of new config entry types * Add unit tests for API Gateway config entry ACLs * Return typed PermissionDeniedError on BoundAPIGateway CanWrite * Add unit tests for added config entry ACLs * Add BoundAPIGateway type to AllConfigEntryKinds * Return proper kind from BoundAPIGateway * Add docstrings for new config entry types * Add missing config entry kinds to proto def * Update usagemetrics_oss_test.go * Use utility func for returning PermissionDeniedError * EventPublisher subscriptions for Consul Native API Gateway (#15757) * Create new event topics in subscribe proto * Add tests for PBSubscribe func * Make configs singular, add all configs to PBToStreamSubscribeRequest * Add snapshot methods * Add config_entry_events tests * Add config entry kind to topic for new configs * Add unit tests for snapshot methods * Start adding integration test * Test using the new controller code * Update agent/consul/state/config_entry_events.go * Check value of error * Add controller stubs for API Gateway (#15837) * update initial stub implementation * move files, clean up mutex references * Remove embed, use idiomatic names for constructors * Remove stray file introduced in merge * Add APIGateway validation (#15847) * Add APIGateway validation * Add additional validations * Add cert ref validation * Add protobuf definitions * Fix up field types * Add API structs * Move struct fields around a bit * APIGateway InlineCertificate validation (#15856) * Add APIGateway validation * Add additional validations * Add protobuf definitions * Tabs to spaces * Add API structs * Move struct fields around a bit * Add validation for InlineCertificate * Fix ACL test * APIGateway BoundAPIGateway validation (#15858) * Add APIGateway validation * Add additional validations * Add cert ref validation * Add protobuf definitions * Fix up field types * Add API structs * Move struct fields around a bit * Add validation for BoundAPIGateway * APIGateway TCPRoute validation (#15855) * Add APIGateway validation * Add additional validations * Add cert ref validation * Add protobuf definitions * Fix up field types * Add API structs * Add TCPRoute normalization and validation * Add forgotten Status * Add some more field docs in api package * Fix test * Format imports * Rename snapshot test variable names * Add plumbing for Native API GW Subscriptions (#16003) Co-authored-by: Sarah Alsmiller <sarah.alsmiller@hashicorp.com> Co-authored-by: Nathan Coleman <nathan.coleman@hashicorp.com> Co-authored-by: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Co-authored-by: Andrew Stucki <andrew.stucki@hashicorp.com>
2 years ago
expectACLs: []testACL{
{
name: "no-authz",
authorizer: newAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator deny",
authorizer: newServiceAndOperatorACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator deny",
authorizer: newServiceAndOperatorACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator deny",
authorizer: newServiceAndOperatorACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh deny",
authorizer: newServiceAndMeshACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh deny",
authorizer: newServiceAndMeshACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh deny",
authorizer: newServiceAndMeshACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator read",
authorizer: newServiceAndOperatorACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator read",
authorizer: newServiceAndOperatorACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator read",
authorizer: newServiceAndOperatorACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator write",
authorizer: newServiceAndOperatorACL(t, "deny", "write"),
canRead: false,
canWrite: true,
},
{
name: "service read and operator write",
authorizer: newServiceAndOperatorACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and operator write",
authorizer: newServiceAndOperatorACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
{
name: "service deny and mesh read",
authorizer: newServiceAndMeshACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh read",
authorizer: newServiceAndMeshACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh read",
authorizer: newServiceAndMeshACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh write",
authorizer: newServiceAndMeshACL(t, "deny", "write"),
canRead: false,
canWrite: true,
},
{
name: "service read and mesh write",
authorizer: newServiceAndMeshACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and mesh write",
authorizer: newServiceAndMeshACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
},
},
{
name: "inline-certificate",
entry: &InlineCertificateConfigEntry{Name: "test", Certificate: validCertificate, PrivateKey: validPrivateKey},
expectACLs: []testACL{
{
name: "no-authz",
authorizer: newAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator deny",
authorizer: newServiceAndOperatorACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator deny",
authorizer: newServiceAndOperatorACL(t, "read", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service write and operator deny",
authorizer: newServiceAndOperatorACL(t, "write", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service deny and mesh deny",
authorizer: newServiceAndMeshACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh deny",
authorizer: newServiceAndMeshACL(t, "read", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service write and mesh deny",
authorizer: newServiceAndMeshACL(t, "write", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator read",
authorizer: newServiceAndOperatorACL(t, "deny", "read"),
canRead: true,
canWrite: false,
},
{
name: "service read and operator read",
authorizer: newServiceAndOperatorACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator read",
authorizer: newServiceAndOperatorACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator write",
authorizer: newServiceAndOperatorACL(t, "deny", "write"),
canRead: true,
canWrite: true,
},
{
name: "service read and operator write",
authorizer: newServiceAndOperatorACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and operator write",
authorizer: newServiceAndOperatorACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
{
name: "service deny and mesh read",
authorizer: newServiceAndMeshACL(t, "deny", "read"),
canRead: true,
canWrite: false,
},
{
name: "service read and mesh read",
authorizer: newServiceAndMeshACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh read",
authorizer: newServiceAndMeshACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh write",
authorizer: newServiceAndMeshACL(t, "deny", "write"),
canRead: true,
canWrite: true,
},
{
name: "service read and mesh write",
authorizer: newServiceAndMeshACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and mesh write",
authorizer: newServiceAndMeshACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
},
},
{
name: "http-route",
entry: &HTTPRouteConfigEntry{Name: "test"},
expectACLs: []testACL{
{
name: "no-authz",
authorizer: newAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator deny",
authorizer: newServiceAndOperatorACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator deny",
authorizer: newServiceAndOperatorACL(t, "read", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service write and operator deny",
authorizer: newServiceAndOperatorACL(t, "write", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service deny and mesh deny",
authorizer: newServiceAndMeshACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh deny",
authorizer: newServiceAndMeshACL(t, "read", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service write and mesh deny",
authorizer: newServiceAndMeshACL(t, "write", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator read",
authorizer: newServiceAndOperatorACL(t, "deny", "read"),
canRead: true,
canWrite: false,
},
{
name: "service read and operator read",
authorizer: newServiceAndOperatorACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator read",
authorizer: newServiceAndOperatorACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator write",
authorizer: newServiceAndOperatorACL(t, "deny", "write"),
canRead: true,
canWrite: true,
},
{
name: "service read and operator write",
authorizer: newServiceAndOperatorACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and operator write",
authorizer: newServiceAndOperatorACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
{
name: "service deny and mesh read",
authorizer: newServiceAndMeshACL(t, "deny", "read"),
canRead: true,
canWrite: false,
},
{
name: "service read and mesh read",
authorizer: newServiceAndMeshACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh read",
authorizer: newServiceAndMeshACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh write",
authorizer: newServiceAndMeshACL(t, "deny", "write"),
canRead: true,
canWrite: true,
},
{
name: "service read and mesh write",
authorizer: newServiceAndMeshACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and mesh write",
authorizer: newServiceAndMeshACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
},
},
{
name: "tcp-route",
entry: &TCPRouteConfigEntry{Name: "test"},
expectACLs: []testACL{
{
name: "no-authz",
authorizer: newAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator deny",
authorizer: newServiceAndOperatorACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator deny",
authorizer: newServiceAndOperatorACL(t, "read", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service write and operator deny",
authorizer: newServiceAndOperatorACL(t, "write", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service deny and mesh deny",
authorizer: newServiceAndMeshACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh deny",
authorizer: newServiceAndMeshACL(t, "read", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service write and mesh deny",
authorizer: newServiceAndMeshACL(t, "write", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator read",
authorizer: newServiceAndOperatorACL(t, "deny", "read"),
canRead: true,
canWrite: false,
},
{
name: "service read and operator read",
authorizer: newServiceAndOperatorACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator read",
authorizer: newServiceAndOperatorACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator write",
authorizer: newServiceAndOperatorACL(t, "deny", "write"),
canRead: true,
canWrite: true,
},
{
name: "service read and operator write",
authorizer: newServiceAndOperatorACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and operator write",
authorizer: newServiceAndOperatorACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
{
name: "service deny and mesh read",
authorizer: newServiceAndMeshACL(t, "deny", "read"),
canRead: true,
canWrite: false,
},
{
name: "service read and mesh read",
authorizer: newServiceAndMeshACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh read",
authorizer: newServiceAndMeshACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh write",
authorizer: newServiceAndMeshACL(t, "deny", "write"),
canRead: true,
canWrite: true,
},
{
name: "service read and mesh write",
authorizer: newServiceAndMeshACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and mesh write",
authorizer: newServiceAndMeshACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
},
},
{
name: "bound-api-gateway",
entry: &BoundAPIGatewayConfigEntry{Name: "test"},
expectACLs: []testACL{
{
name: "no-authz",
authorizer: newAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator deny",
authorizer: newServiceAndOperatorACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator deny",
authorizer: newServiceAndOperatorACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator deny",
authorizer: newServiceAndOperatorACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh deny",
authorizer: newServiceAndMeshACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh deny",
authorizer: newServiceAndMeshACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh deny",
authorizer: newServiceAndMeshACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator read",
authorizer: newServiceAndOperatorACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator read",
authorizer: newServiceAndOperatorACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator read",
authorizer: newServiceAndOperatorACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator write",
authorizer: newServiceAndOperatorACL(t, "deny", "write"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator write",
authorizer: newServiceAndOperatorACL(t, "read", "write"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator write",
authorizer: newServiceAndOperatorACL(t, "write", "write"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh read",
authorizer: newServiceAndMeshACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh read",
authorizer: newServiceAndMeshACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh read",
authorizer: newServiceAndMeshACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh write",
authorizer: newServiceAndMeshACL(t, "deny", "write"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh write",
authorizer: newServiceAndMeshACL(t, "read", "write"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh write",
authorizer: newServiceAndMeshACL(t, "write", "write"),
canRead: true,
canWrite: false,
},
},
},
{
name: "terminating-gateway",
entry: &TerminatingGatewayConfigEntry{Name: "test"},
expectACLs: []testACL{
{
name: "no-authz",
authorizer: newAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "service deny and operator deny",
authorizer: newServiceAndOperatorACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator deny",
authorizer: newServiceAndOperatorACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator deny",
authorizer: newServiceAndOperatorACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh deny",
authorizer: newServiceAndMeshACL(t, "deny", "deny"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh deny",
authorizer: newServiceAndMeshACL(t, "read", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh deny",
authorizer: newServiceAndMeshACL(t, "write", "deny"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator read",
authorizer: newServiceAndOperatorACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and operator read",
authorizer: newServiceAndOperatorACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and operator read",
authorizer: newServiceAndOperatorACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and operator write",
authorizer: newServiceAndOperatorACL(t, "deny", "write"),
canRead: false,
canWrite: true,
},
{
name: "service read and operator write",
authorizer: newServiceAndOperatorACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and operator write",
authorizer: newServiceAndOperatorACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
{
name: "service deny and mesh read",
authorizer: newServiceAndMeshACL(t, "deny", "read"),
canRead: false,
canWrite: false,
},
{
name: "service read and mesh read",
authorizer: newServiceAndMeshACL(t, "read", "read"),
canRead: true,
canWrite: false,
},
{
name: "service write and mesh read",
authorizer: newServiceAndMeshACL(t, "write", "read"),
canRead: true,
canWrite: false,
},
{
name: "service deny and mesh write",
authorizer: newServiceAndMeshACL(t, "deny", "write"),
canRead: false,
canWrite: true,
},
{
name: "service read and mesh write",
authorizer: newServiceAndMeshACL(t, "read", "write"),
canRead: true,
canWrite: true,
},
{
name: "service write and mesh write",
authorizer: newServiceAndMeshACL(t, "write", "write"),
canRead: true,
canWrite: true,
},
},
},
}
testConfigEntries_ListRelatedServices_AndACLs(t, cases)
}
func TestServiceResolverConfigEntry(t *testing.T) {
type testcase struct {
name string
entry *ServiceResolverConfigEntry
normalizeErr string
validateErr string
// check is called between normalize and validate
check func(t *testing.T, entry *ServiceResolverConfigEntry)
}
cases := []testcase{
{
name: "nil",
entry: nil,
normalizeErr: "config entry is nil",
},
{
name: "no name",
entry: &ServiceResolverConfigEntry{},
validateErr: "Name is required",
},
{
name: "empty",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
},
},
{
name: "empty subset name",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
"": {OnlyPassing: true},
},
},
validateErr: "Subset defined with empty name",
},
{
name: "invalid boolean expression subset filter",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "random string"},
},
},
validateErr: `Filter for subset "v1" is not a valid expression`,
},
{
name: "default subset does not exist",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
DefaultSubset: "gone",
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
},
},
validateErr: `DefaultSubset "gone" is not a valid subset`,
},
{
name: "default subset does exist",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
DefaultSubset: "v1",
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
},
},
},
{
name: "empty redirect",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{},
},
validateErr: "Redirect is empty",
},
{
name: "empty redirect",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{},
},
validateErr: "Redirect is empty",
},
{
name: "redirect subset with no service",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
ServiceSubset: "next",
},
},
validateErr: "Redirect.ServiceSubset defined without Redirect.Service",
},
{
name: "self redirect with invalid subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
Service: "test",
ServiceSubset: "gone",
},
},
validateErr: `Redirect.ServiceSubset "gone" is not a valid subset of "test"`,
},
{
name: "redirect with peer and subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
Peer: "cluster-01",
ServiceSubset: "gone",
},
},
validateErr: `Redirect.Peer cannot be set with Redirect.ServiceSubset`,
},
{
name: "redirect with peer and datacenter",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
Peer: "cluster-01",
Datacenter: "dc2",
},
},
validateErr: `Redirect.Peer cannot be set with Redirect.Datacenter`,
},
{
name: "redirect with peer and datacenter",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
Peer: "cluster-01",
},
},
validateErr: `Redirect.Peer defined without Redirect.Service`,
},
{
name: "self redirect with valid subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
Service: "test",
ServiceSubset: "v1",
},
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
},
},
},
{
name: "redirect to peer",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Redirect: &ServiceResolverRedirect{
Service: "other",
Peer: "cluster-01",
},
},
},
{
name: "simple wildcard failover",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Datacenters: []string{"dc2"},
},
},
},
},
{
name: "failover for missing subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"gone": {
Datacenters: []string{"dc2"},
},
},
},
validateErr: `Bad Failover["gone"]: not a valid subset`,
},
{
name: "failover for present subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
},
Failover: map[string]ServiceResolverFailover{
"v1": {
Datacenters: []string{"dc2"},
},
},
},
},
{
name: "failover empty",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
},
Failover: map[string]ServiceResolverFailover{
"v1": {},
},
},
validateErr: `Bad Failover["v1"]: one of Service, ServiceSubset, Namespace, Targets, SamenessGroup, or Datacenters is required`,
},
{
name: "failover to self using invalid subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
},
Failover: map[string]ServiceResolverFailover{
"v1": {
Service: "test",
ServiceSubset: "gone",
},
},
},
validateErr: `Bad Failover["v1"]: ServiceSubset "gone" is not a valid subset of "test"`,
},
{
name: "failover to self using valid subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
"v1": {Filter: "Service.Meta.version == v1"},
"v2": {Filter: "Service.Meta.version == v2"},
},
Failover: map[string]ServiceResolverFailover{
"v1": {
Service: "test",
ServiceSubset: "v2",
},
},
},
},
{
name: "failover with empty datacenters in list",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Service: "backup",
Datacenters: []string{"", "dc2", "dc3"},
},
},
},
validateErr: `Bad Failover["*"].Datacenters: found empty datacenter`,
},
{
name: "failover target with an invalid subset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Targets: []ServiceResolverFailoverTarget{{ServiceSubset: "subset"}},
},
},
},
validateErr: `Bad Failover["*"].Targets[0]: ServiceSubset "subset" is not a valid subset of "test"`,
},
{
name: "failover targets can't have Peer and ServiceSubset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01", ServiceSubset: "subset"}},
},
},
},
validateErr: `Bad Failover["*"].Targets[0]: Peer cannot be set with ServiceSubset`,
},
{
name: "failover targets can't have Peer and Datacenter",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01", Datacenter: "dc1"}},
},
},
},
validateErr: `Bad Failover["*"].Targets[0]: Peer cannot be set with Datacenter`,
},
{
name: "failover Targets cannot be set with Datacenters",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Datacenters: []string{"a"},
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}},
},
},
},
validateErr: `Bad Failover["*"]: Targets cannot be set with Datacenters`,
},
{
name: "failover Targets cannot be set with ServiceSubset",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
ServiceSubset: "v2",
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}},
},
},
Subsets: map[string]ServiceResolverSubset{
"v2": {Filter: "Service.Meta.version == v2"},
},
},
validateErr: `Bad Failover["*"]: Targets cannot be set with ServiceSubset`,
},
{
name: "failover Targets cannot be set with Service",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Service: "another-service",
Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}},
},
},
Subsets: map[string]ServiceResolverSubset{
"v2": {Filter: "Service.Meta.version == v2"},
},
},
validateErr: `Bad Failover["*"]: Targets cannot be set with Service`,
},
{
name: "complicated failover targets",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Failover: map[string]ServiceResolverFailover{
"*": {
Targets: []ServiceResolverFailoverTarget{
{Peer: "cluster-01", Service: "test-v2"},
{Service: "test-v2", ServiceSubset: "test"},
{Datacenter: "dc2"},
},
},
},
},
},
{
name: "bad connect timeout",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
ConnectTimeout: -1 * time.Second,
},
validateErr: "Bad ConnectTimeout",
},
{
name: "bad request timeout",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
RequestTimeout: -1 * time.Second,
},
validateErr: "Bad RequestTimeout",
},
}
// Bulk add a bunch of similar validation cases.
for _, invalidSubset := range invalidSubsetNames {
tc := testcase{
name: "invalid subset name: " + invalidSubset,
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
invalidSubset: {OnlyPassing: true},
},
},
validateErr: fmt.Sprintf("Subset %q is invalid", invalidSubset),
}
cases = append(cases, tc)
}
for _, goodSubset := range validSubsetNames {
tc := testcase{
name: "valid subset name: " + goodSubset,
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
Subsets: map[string]ServiceResolverSubset{
goodSubset: {OnlyPassing: true},
},
},
}
cases = append(cases, tc)
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
err := tc.entry.Normalize()
if tc.normalizeErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.normalizeErr)
return
}
require.NoError(t, err)
if tc.check != nil {
tc.check(t, tc.entry)
}
err = tc.entry.Validate()
if tc.validateErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.validateErr)
return
}
require.NoError(t, err)
})
}
}
func TestServiceResolverConfigEntry_LoadBalancer(t *testing.T) {
type testcase struct {
name string
entry *ServiceResolverConfigEntry
normalizeErr string
validateErr string
// check is called between normalize and validate
check func(t *testing.T, entry *ServiceResolverConfigEntry)
}
cases := []testcase{
{
name: "empty policy is valid",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: "",
},
},
},
{
name: "supported policy",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyRandom,
},
},
},
{
name: "unsupported policy",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: "fake-policy",
},
},
validateErr: `"fake-policy" is not supported`,
},
{
name: "bad policy for least request config",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyRingHash,
LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 10},
},
},
validateErr: `LeastRequestConfig specified for incompatible load balancing policy`,
},
{
name: "bad policy for ring hash config",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyLeastRequest,
RingHashConfig: &RingHashConfig{MinimumRingSize: 1024},
},
},
validateErr: `RingHashConfig specified for incompatible load balancing policy`,
},
{
name: "good policy for ring hash config",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyRingHash,
RingHashConfig: &RingHashConfig{MinimumRingSize: 1024},
},
},
},
{
name: "good policy for least request config",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyLeastRequest,
LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 2},
},
},
},
{
name: "empty policy is not defaulted",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: "",
},
},
check: func(t *testing.T, entry *ServiceResolverConfigEntry) {
require.Equal(t, "", entry.LoadBalancer.Policy)
},
},
{
name: "empty policy with hash policy",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: "",
HashPolicies: []HashPolicy{
{
SourceIP: true,
},
},
},
},
validateErr: `HashPolicies specified for non-hash-based Policy`,
},
{
name: "cookie config with header policy",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
Field: HashPolicyHeader,
FieldValue: "x-user-id",
CookieConfig: &CookieConfig{
TTL: 10 * time.Second,
Path: "/root",
},
},
},
},
},
validateErr: `cookie_config provided for "header"`,
},
{
name: "cannot generate session cookie with ttl",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
Field: HashPolicyCookie,
FieldValue: "good-cookie",
CookieConfig: &CookieConfig{
Session: true,
TTL: 10 * time.Second,
},
},
},
},
},
validateErr: `a session cookie cannot have an associated TTL`,
},
{
name: "valid cookie policy",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
Field: HashPolicyCookie,
FieldValue: "good-cookie",
CookieConfig: &CookieConfig{
TTL: 10 * time.Second,
Path: "/oven",
},
},
},
},
},
},
{
name: "supported match field",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
Field: "header",
FieldValue: "X-Consul-Token",
},
},
},
},
},
{
name: "unsupported match field",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
Field: "fake-field",
},
},
},
},
validateErr: `"fake-field" is not a supported field`,
},
{
name: "cannot match on source address and custom field",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
Field: "header",
SourceIP: true,
},
},
},
},
validateErr: `A single hash policy cannot hash both a source address and a "header"`,
},
{
name: "matchvalue not compatible with source address",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
FieldValue: "X-Consul-Token",
SourceIP: true,
},
},
},
},
validateErr: `A FieldValue cannot be specified when hashing SourceIP`,
},
{
name: "field without match value",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
Field: "header",
},
},
},
},
validateErr: `Field "header" was specified without a FieldValue`,
},
{
name: "field without match value",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyMaglev,
HashPolicies: []HashPolicy{
{
FieldValue: "my-cookie",
},
},
},
},
validateErr: `FieldValue requires a Field to apply to`,
},
{
name: "ring hash kitchen sink",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyRingHash,
RingHashConfig: &RingHashConfig{MaximumRingSize: 10, MinimumRingSize: 2},
HashPolicies: []HashPolicy{
{
Field: "cookie",
FieldValue: "my-cookie",
},
{
Field: "header",
FieldValue: "alt-header",
Terminal: true,
},
},
},
},
},
{
name: "least request kitchen sink",
entry: &ServiceResolverConfigEntry{
Kind: ServiceResolver,
Name: "test",
LoadBalancer: &LoadBalancer{
Policy: LBPolicyLeastRequest,
LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 20},
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
err := tc.entry.Normalize()
if tc.normalizeErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.normalizeErr)
return
}
require.NoError(t, err)
if tc.check != nil {
tc.check(t, tc.entry)
}
err = tc.entry.Validate()
if tc.validateErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.validateErr)
return
}
require.NoError(t, err)
})
}
}
func TestServiceSplitterConfigEntry(t *testing.T) {
makesplitter := func(splits ...ServiceSplit) *ServiceSplitterConfigEntry {
return &ServiceSplitterConfigEntry{
Kind: ServiceSplitter,
Name: "test",
Splits: splits,
}
}
makesplit := func(weight float32, service, serviceSubset, namespace string) ServiceSplit {
return ServiceSplit{
Weight: weight,
Service: service,
ServiceSubset: serviceSubset,
Namespace: namespace,
}
}
for _, tc := range []struct {
name string
entry *ServiceSplitterConfigEntry
normalizeErr string
validateErr string
// check is called between normalize and validate
check func(t *testing.T, entry *ServiceSplitterConfigEntry)
}{
{
name: "nil",
entry: nil,
normalizeErr: "config entry is nil",
},
{
name: "no name",
entry: &ServiceSplitterConfigEntry{},
validateErr: "Name is required",
},
{
name: "empty",
entry: makesplitter(),
validateErr: "no splits configured",
},
{
name: "1 split",
entry: makesplitter(
makesplit(100, "test", "", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(100), entry.Splits[0].Weight)
},
},
{
name: "1 split not enough weight",
entry: makesplitter(
makesplit(99.99, "test", "", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(99.99), entry.Splits[0].Weight)
},
validateErr: "the sum of all split weights must be 100",
},
{
name: "1 split too much weight",
entry: makesplitter(
makesplit(100.01, "test", "", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(100.01), entry.Splits[0].Weight)
},
validateErr: "the sum of all split weights must be 100",
},
{
name: "2 splits",
entry: makesplitter(
makesplit(99, "test", "v1", ""),
makesplit(1, "test", "v2", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(99), entry.Splits[0].Weight)
require.Equal(t, float32(1), entry.Splits[1].Weight)
},
},
{
name: "2 splits - rounded up to smallest units",
entry: makesplitter(
makesplit(99.999, "test", "v1", ""),
makesplit(0.001, "test", "v2", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(100), entry.Splits[0].Weight)
require.Equal(t, float32(0), entry.Splits[1].Weight)
},
},
{
name: "2 splits not enough weight",
entry: makesplitter(
makesplit(99.98, "test", "v1", ""),
makesplit(0.01, "test", "v2", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(99.98), entry.Splits[0].Weight)
require.Equal(t, float32(0.01), entry.Splits[1].Weight)
},
validateErr: "the sum of all split weights must be 100",
},
{
name: "2 splits too much weight",
entry: makesplitter(
makesplit(100, "test", "v1", ""),
makesplit(0.01, "test", "v2", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(100), entry.Splits[0].Weight)
require.Equal(t, float32(0.01), entry.Splits[1].Weight)
},
validateErr: "the sum of all split weights must be 100",
},
{
name: "3 splits",
entry: makesplitter(
makesplit(34, "test", "v1", ""),
makesplit(33, "test", "v2", ""),
makesplit(33, "test", "v3", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(34), entry.Splits[0].Weight)
require.Equal(t, float32(33), entry.Splits[1].Weight)
require.Equal(t, float32(33), entry.Splits[2].Weight)
},
},
{
name: "3 splits one duplicated same weights",
entry: makesplitter(
makesplit(34, "test", "v1", ""),
makesplit(33, "test", "v2", ""),
makesplit(33, "test", "v2", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(34), entry.Splits[0].Weight)
require.Equal(t, float32(33), entry.Splits[1].Weight)
require.Equal(t, float32(33), entry.Splits[2].Weight)
},
validateErr: "split destination occurs more than once",
},
{
name: "3 splits one duplicated diff weights",
entry: makesplitter(
makesplit(34, "test", "v1", ""),
makesplit(33, "test", "v2", ""),
makesplit(33, "test", "v1", ""),
),
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
require.Equal(t, float32(34), entry.Splits[0].Weight)
require.Equal(t, float32(33), entry.Splits[1].Weight)
require.Equal(t, float32(33), entry.Splits[2].Weight)
},
validateErr: "split destination occurs more than once",
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
err := tc.entry.Normalize()
if tc.normalizeErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.normalizeErr)
return
}
require.NoError(t, err)
if tc.check != nil {
tc.check(t, tc.entry)
}
err = tc.entry.Validate()
if tc.validateErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.validateErr)
return
}
require.NoError(t, err)
})
}
}
func TestServiceSplitMergeParent(t *testing.T) {
type testCase struct {
name string
split, parent, want *ServiceSplit
wantErr string
}
run := func(t *testing.T, tc testCase) {
got, err := tc.split.MergeParent(tc.parent)
if tc.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.wantErr)
} else {
require.NoError(t, err)
require.Equal(t, tc.want, got)
}
}
testCases := []testCase{
{
name: "all header manip fields set",
split: &ServiceSplit{
Weight: 50.0,
Service: "foo",
RequestHeaders: &HTTPHeaderModifiers{
Add: map[string]string{
"child-only": "1",
"both-want-child": "2",
},
Set: map[string]string{
"child-only": "3",
"both-want-child": "4",
},
Remove: []string{"child-only-req", "both-req"},
},
ResponseHeaders: &HTTPHeaderModifiers{
Add: map[string]string{
"child-only": "5",
"both-want-parent": "6",
},
Set: map[string]string{
"child-only": "7",
"both-want-parent": "8",
},
Remove: []string{"child-only-resp", "both-resp"},
},
},
parent: &ServiceSplit{
Weight: 25.0,
Service: "bar",
RequestHeaders: &HTTPHeaderModifiers{
Add: map[string]string{
"parent-only": "9",
"both-want-child": "10",
},
Set: map[string]string{
"parent-only": "11",
"both-want-child": "12",
},
Remove: []string{"parent-only-req", "both-req"},
},
ResponseHeaders: &HTTPHeaderModifiers{
Add: map[string]string{
"parent-only": "13",
"both-want-parent": "14",
},
Set: map[string]string{
"parent-only": "15",
"both-want-parent": "16",
},
Remove: []string{"parent-only-resp", "both-resp"},
},
},
want: &ServiceSplit{
Weight: 50.0,
Service: "foo",
RequestHeaders: &HTTPHeaderModifiers{
Add: map[string]string{
"child-only": "1",
"both-want-child": "2",
"parent-only": "9",
},
Set: map[string]string{
"child-only": "3",
"both-want-child": "4",
"parent-only": "11",
},
Remove: []string{"parent-only-req", "both-req", "child-only-req"},
},
ResponseHeaders: &HTTPHeaderModifiers{
Add: map[string]string{
"child-only": "5",
"parent-only": "13",
"both-want-parent": "14",
},
Set: map[string]string{
"child-only": "7",
"parent-only": "15",
"both-want-parent": "16",
},
Remove: []string{"child-only-resp", "both-resp", "parent-only-resp"},
},
},
},
{
name: "no header manip",
split: &ServiceSplit{
Weight: 50,
Service: "foo",
},
parent: &ServiceSplit{
Weight: 50,
Service: "bar",
},
want: &ServiceSplit{
Weight: 50,
Service: "foo",
},
},
{
name: "nil parent",
split: &ServiceSplit{
Weight: 50,
Service: "foo",
},
parent: nil,
want: &ServiceSplit{
Weight: 50,
Service: "foo",
},
},
{
name: "nil child",
split: nil,
parent: &ServiceSplit{
Weight: 50,
Service: "foo",
},
want: &ServiceSplit{
Weight: 50,
Service: "foo",
},
},
{
name: "both nil",
split: nil,
parent: nil,
want: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
run(t, tc)
})
}
}
func TestServiceRouterConfigEntry(t *testing.T) {
httpMatch := func(http *ServiceRouteHTTPMatch) *ServiceRouteMatch {
return &ServiceRouteMatch{HTTP: http}
}
httpMatchHeader := func(headers ...ServiceRouteHTTPMatchHeader) *ServiceRouteMatch {
return httpMatch(&ServiceRouteHTTPMatch{
Header: headers,
})
}
httpMatchParam := func(params ...ServiceRouteHTTPMatchQueryParam) *ServiceRouteMatch {
return httpMatch(&ServiceRouteHTTPMatch{
QueryParam: params,
})
}
toService := func(svc string) *ServiceRouteDestination {
return &ServiceRouteDestination{Service: svc}
}
routeMatch := func(match *ServiceRouteMatch) ServiceRoute {
return ServiceRoute{
Match: match,
Destination: toService("other"),
}
}
makerouter := func(routes ...ServiceRoute) *ServiceRouterConfigEntry {
return &ServiceRouterConfigEntry{
Kind: ServiceRouter,
Name: "test",
Routes: routes,
}
}
type testcase struct {
name string
entry *ServiceRouterConfigEntry
normalizeErr string
validateErr string
// check is called between normalize and validate
check func(t *testing.T, entry *ServiceRouterConfigEntry)
}
cases := []testcase{
{
name: "nil",
entry: nil,
normalizeErr: "config entry is nil",
},
{
name: "no name",
entry: &ServiceRouterConfigEntry{},
validateErr: "Name is required",
},
{
name: "empty",
entry: makerouter(),
},
{
name: "1 empty route",
entry: makerouter(
ServiceRoute{},
),
},
{
name: "route with path exact",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathExact: "/exact",
}))),
},
{
name: "route with bad path exact",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathExact: "no-leading-slash",
}))),
validateErr: "PathExact doesn't start with '/'",
},
{
name: "route with path prefix",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathPrefix: "/prefix",
}))),
},
{
name: "route with bad path prefix",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathPrefix: "no-leading-slash",
}))),
validateErr: "PathPrefix doesn't start with '/'",
},
{
name: "route with path regex",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathRegex: "/regex",
}))),
},
{
name: "route with path exact and prefix",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathExact: "/exact",
PathPrefix: "/prefix",
}))),
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
},
{
name: "route with path exact and regex",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathExact: "/exact",
PathRegex: "/regex",
}))),
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
},
{
name: "route with path prefix and regex",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathPrefix: "/prefix",
PathRegex: "/regex",
}))),
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
},
{
name: "route with path exact, prefix, and regex",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathExact: "/exact",
PathPrefix: "/prefix",
PathRegex: "/regex",
}))),
validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
},
{
name: "route with no name header",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Present: true,
}))),
validateErr: "missing required Name field",
},
{
name: "route with header present",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Present: true,
}))),
},
{
name: "route with header not present",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Present: true,
Invert: true,
}))),
},
{
name: "route with header exact",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Exact: "bar",
}))),
},
{
name: "route with header regex",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Regex: "bar",
}))),
},
{
name: "route with header prefix",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Prefix: "bar",
}))),
},
{
name: "route with header suffix",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Suffix: "bar",
}))),
},
{
name: "route with header present and exact",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Present: true,
Exact: "bar",
}))),
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
},
{
name: "route with header present and regex",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Present: true,
Regex: "bar",
}))),
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
},
{
name: "route with header present and prefix",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Present: true,
Prefix: "bar",
}))),
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
},
{
name: "route with header present and suffix",
entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Present: true,
Suffix: "bar",
}))),
validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
},
// NOTE: Some combinatoric cases for header operators (some 5 choose 2,
// all 5 choose 3, all 5 choose 4, all 5 choose 5) are omitted from
// testing.
////////////////
{
name: "route with no name query param",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Exact: "foo",
}))),
validateErr: "missing required Name field",
},
{
name: "route with query param exact match",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Exact: "bar",
}))),
},
{
name: "route with query param regex match",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Regex: "bar",
}))),
},
{
name: "route with query param present match",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Present: true,
}))),
},
{
name: "route with query param exact and regex match",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Exact: "bar",
Regex: "bar",
}))),
validateErr: "should only contain one of Present, Exact, or Regex",
},
{
name: "route with query param exact and present match",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Exact: "bar",
Present: true,
}))),
validateErr: "should only contain one of Present, Exact, or Regex",
},
{
name: "route with query param regex and present match",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Regex: "bar",
Present: true,
}))),
validateErr: "should only contain one of Present, Exact, or Regex",
},
{
name: "route with query param exact, regex, and present match",
entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Exact: "bar",
Regex: "bar",
Present: true,
}))),
validateErr: "should only contain one of Present, Exact, or Regex",
},
////////////////
{
name: "route with no match and prefix rewrite",
entry: makerouter(ServiceRoute{
Match: nil,
Destination: &ServiceRouteDestination{
Service: "other",
PrefixRewrite: "/new",
},
}),
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
},
{
name: "route with path prefix match and prefix rewrite",
entry: makerouter(ServiceRoute{
Match: httpMatch(&ServiceRouteHTTPMatch{
PathPrefix: "/api",
}),
Destination: &ServiceRouteDestination{
Service: "other",
PrefixRewrite: "/new",
},
}),
},
{
name: "route with path exact match and prefix rewrite",
entry: makerouter(ServiceRoute{
Match: httpMatch(&ServiceRouteHTTPMatch{
PathExact: "/api",
}),
Destination: &ServiceRouteDestination{
Service: "other",
PrefixRewrite: "/new",
},
}),
},
{
name: "route with path regex match and prefix rewrite",
entry: makerouter(ServiceRoute{
Match: httpMatch(&ServiceRouteHTTPMatch{
PathRegex: "/api",
}),
Destination: &ServiceRouteDestination{
Service: "other",
PrefixRewrite: "/new",
},
}),
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
},
{
name: "route with header match and prefix rewrite",
entry: makerouter(ServiceRoute{
Match: httpMatchHeader(ServiceRouteHTTPMatchHeader{
Name: "foo",
Exact: "bar",
}),
Destination: &ServiceRouteDestination{
Service: "other",
PrefixRewrite: "/new",
},
}),
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
},
{
name: "route with header match and prefix rewrite",
entry: makerouter(ServiceRoute{
Match: httpMatchParam(ServiceRouteHTTPMatchQueryParam{
Name: "foo",
Exact: "bar",
}),
Destination: &ServiceRouteDestination{
Service: "other",
PrefixRewrite: "/new",
},
}),
validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
},
////////////////
{
name: "route with method matches",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
Methods: []string{
"get", "POST", "dElEtE",
},
}))),
check: func(t *testing.T, entry *ServiceRouterConfigEntry) {
m := entry.Routes[0].Match.HTTP.Methods
require.Equal(t, []string{"GET", "POST", "DELETE"}, m)
},
},
{
name: "route with method matches repeated",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
Methods: []string{
"GET", "DELETE", "get",
},
}))),
validateErr: "Methods contains \"GET\" more than once",
},
////////////////
{
name: "route with no match with retry condition",
entry: makerouter(ServiceRoute{
Match: nil,
Destination: &ServiceRouteDestination{
Service: "other",
RetryOn: []string{
"5xx",
"gateway-error",
"reset",
"connect-failure",
"envoy-ratelimited",
"retriable-4xx",
"refused-stream",
"cancelled",
"deadline-exceeded",
"internal",
"resource-exhausted",
"unavailable",
},
},
}),
},
{
name: "route with no match with invalid retry condition",
entry: makerouter(ServiceRoute{
Match: nil,
Destination: &ServiceRouteDestination{
Service: "other",
RetryOn: []string{
"invalid-retry-condition",
},
},
}),
validateErr: "contains an invalid retry condition: \"invalid-retry-condition\"",
},
////////////////
{
name: "default route with case insensitive match",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
CaseInsensitive: true,
}))),
},
{
name: "route with path prefix and case insensitive match /apI",
entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
PathPrefix: "/apI",
CaseInsensitive: true,
}))),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
err := tc.entry.Normalize()
if tc.normalizeErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.normalizeErr)
return
}
require.NoError(t, err)
if tc.check != nil {
tc.check(t, tc.entry)
}
err = tc.entry.Validate()
if tc.validateErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.validateErr)
return
}
require.NoError(t, err)
})
}
}
var validSubsetNames = []string{
"a", "aa", "2a", "a2", "a2a", "a22a",
"1", "11", "10", "01",
"a-a", "a--a", "a--a--a",
"0-0", "0--0", "0--0--0",
strings.Repeat("a", 63),
}
var invalidSubsetNames = []string{
"A", "AA", "2A", "A2", "A2A", "A22A",
"A-A", "A--A", "A--A--A",
" ", " a", "a ", "a a",
"_", "_a", "a_", "a_a",
".", ".a", "a.", "a.a",
"-", "-a", "a-",
strings.Repeat("a", 64),
}
func TestValidateServiceSubset(t *testing.T) {
for _, name := range validSubsetNames {
t.Run(name, func(t *testing.T) {
require.NoError(t, validateServiceSubset(name))
})
}
for _, name := range invalidSubsetNames {
t.Run(name, func(t *testing.T) {
require.Error(t, validateServiceSubset(name))
})
}
}
func TestIsProtocolHTTPLike(t *testing.T) {
assert.False(t, IsProtocolHTTPLike(""))
assert.False(t, IsProtocolHTTPLike("tcp"))
assert.True(t, IsProtocolHTTPLike("http"))
assert.True(t, IsProtocolHTTPLike("http2"))
assert.True(t, IsProtocolHTTPLike("grpc"))
}
func TestIsValidRetryCondition(t *testing.T) {
assert.False(t, isValidRetryCondition(""))
assert.False(t, isValidRetryCondition("retriable-headers"))
assert.False(t, isValidRetryCondition("retriable-status-codes"))
assert.True(t, isValidRetryCondition("5xx"))
assert.True(t, isValidRetryCondition("gateway-error"))
assert.True(t, isValidRetryCondition("reset"))
assert.True(t, isValidRetryCondition("connect-failure"))
assert.True(t, isValidRetryCondition("envoy-ratelimited"))
assert.True(t, isValidRetryCondition("retriable-4xx"))
assert.True(t, isValidRetryCondition("refused-stream"))
assert.True(t, isValidRetryCondition("cancelled"))
assert.True(t, isValidRetryCondition("deadline-exceeded"))
assert.True(t, isValidRetryCondition("internal"))
assert.True(t, isValidRetryCondition("resource-exhausted"))
assert.True(t, isValidRetryCondition("unavailable"))
}