// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package structs import ( "encoding/json" "fmt" "math/rand" "reflect" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" fuzz "github.com/google/gofuzz" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/types" ) func TestEncodeDecode(t *testing.T) { arg := &RegisterRequest{ Datacenter: "foo", Node: "bar", Address: "baz", Service: &NodeService{ Service: "test", Address: "127.0.0.2", }, } buf, err := Encode(RegisterRequestType, arg) if err != nil { t.Fatalf("err: %v", err) } var out RegisterRequest err = Decode(buf[1:], &out) if err != nil { t.Fatalf("err: %v", err) } if !reflect.DeepEqual(arg.Service, out.Service) { t.Fatalf("bad: %#v %#v", arg.Service, out.Service) } if !reflect.DeepEqual(arg, &out) { t.Fatalf("bad: %#v %#v", arg, out) } } func TestStructs_Implements(t *testing.T) { var ( _ RPCInfo = &RegisterRequest{} _ RPCInfo = &DeregisterRequest{} _ RPCInfo = &DCSpecificRequest{} _ RPCInfo = &ServiceSpecificRequest{} _ RPCInfo = &NodeSpecificRequest{} _ RPCInfo = &ChecksInStateRequest{} _ RPCInfo = &KVSRequest{} _ RPCInfo = &KeyRequest{} _ RPCInfo = &KeyListRequest{} _ RPCInfo = &SessionRequest{} _ RPCInfo = &SessionSpecificRequest{} _ RPCInfo = &EventFireRequest{} _ RPCInfo = &ACLPolicyBatchGetRequest{} _ RPCInfo = &ACLPolicyGetRequest{} _ RPCInfo = &ACLTokenGetRequest{} _ RPCInfo = &KeyringRequest{} _ CompoundResponse = &KeyringResponses{} ) } func TestStructs_RegisterRequest_ChangesNode(t *testing.T) { node := &Node{ ID: types.NodeID("40e4a748-2192-161a-0510-9bf59fe950b5"), Node: "test", Address: "127.0.0.1", Datacenter: "dc1", TaggedAddresses: make(map[string]string), Meta: map[string]string{ "role": "server", }, } type testcase struct { name string setup func(*RegisterRequest) expect bool } cases := []testcase{ { name: "id", setup: func(r *RegisterRequest) { r.ID = "nope" }, expect: true, }, { name: "name", setup: func(r *RegisterRequest) { r.Node = "nope" }, expect: true, }, { name: "name casing", setup: func(r *RegisterRequest) { r.Node = "TeSt" }, expect: false, }, { name: "address", setup: func(r *RegisterRequest) { r.Address = "127.0.0.2" }, expect: true, }, { name: "dc", setup: func(r *RegisterRequest) { r.Datacenter = "dc2" }, expect: true, }, { name: "tagged addresses", setup: func(r *RegisterRequest) { r.TaggedAddresses["wan"] = "nope" }, expect: true, }, { name: "node meta", setup: func(r *RegisterRequest) { r.NodeMeta["invalid"] = "nope" }, expect: true, }, } run := func(t *testing.T, tc testcase) { req := &RegisterRequest{ ID: types.NodeID("40e4a748-2192-161a-0510-9bf59fe950b5"), Node: "test", Address: "127.0.0.1", Datacenter: "dc1", TaggedAddresses: make(map[string]string), NodeMeta: map[string]string{ "role": "server", }, } if req.ChangesNode(node) { t.Fatalf("should not change") } tc.setup(req) if tc.expect { if !req.ChangesNode(node) { t.Fatalf("should change") } } else { if req.ChangesNode(node) { t.Fatalf("should not change") } } t.Run("skip node update", func(t *testing.T) { req.SkipNodeUpdate = true if req.ChangesNode(node) { t.Fatalf("should skip") } }) } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } // testServiceNode gives a fully filled out ServiceNode instance. func testServiceNode(t *testing.T) *ServiceNode { return &ServiceNode{ ID: types.NodeID("40e4a748-2192-161a-0510-9bf59fe950b5"), Node: "node1", Address: "127.0.0.1", Datacenter: "dc1", TaggedAddresses: map[string]string{ "hello": "world", }, NodeMeta: map[string]string{ "tag": "value", }, ServiceKind: ServiceKindTypical, ServiceID: "service1", ServiceName: "dogs", ServiceTags: []string{"prod", "v1"}, ServiceAddress: "127.0.0.2", ServiceTaggedAddresses: map[string]ServiceAddress{ "lan": { Address: "127.0.0.2", Port: 8080, }, "wan": { Address: "198.18.0.1", Port: 80, }, }, ServicePort: 8080, ServiceMeta: map[string]string{ "service": "metadata", }, ServiceEnableTagOverride: true, RaftIndex: RaftIndex{ CreateIndex: 1, ModifyIndex: 2, }, ServiceProxy: TestConnectProxyConfig(t), ServiceConnect: ServiceConnect{ Native: true, }, } } func TestRegisterRequest_UnmarshalJSON_WithConnectNilDoesNotPanic(t *testing.T) { in := ` { "ID": "", "Node": "k8s-sync", "Address": "127.0.0.1", "TaggedAddresses": null, "NodeMeta": { "external-source": "kubernetes" }, "Datacenter": "", "Service": { "Kind": "", "ID": "test-service-f8fd5f0f4e6c", "Service": "test-service", "Tags": [ "k8s" ], "Meta": { "external-k8s-ns": "", "external-source": "kubernetes", "port-stats": "18080" }, "Port": 8080, "Address": "192.0.2.10", "EnableTagOverride": false, "CreateIndex": 0, "ModifyIndex": 0, "Connect": null }, "Check": null, "SkipNodeUpdate": true } ` var req RegisterRequest err := lib.DecodeJSON(strings.NewReader(in), &req) require.NoError(t, err) } func TestNode_IsSame(t *testing.T) { id := types.NodeID("e62f3b31-9284-4e26-ab14-2a59dea85b55") node := "mynode1" address := "" datacenter := "dc1" n := &Node{ ID: id, Node: node, Datacenter: datacenter, Address: address, TaggedAddresses: make(map[string]string), Meta: make(map[string]string), RaftIndex: RaftIndex{ CreateIndex: 1, ModifyIndex: 2, }, } type testcase struct { name string setup func(*Node) expect bool } cases := []testcase{ { name: "id", setup: func(n *Node) { n.ID = types.NodeID("") }, expect: false, }, { name: "node", setup: func(n *Node) { n.Node = "other" }, expect: false, }, { name: "node casing", setup: func(n *Node) { n.Node = "MyNoDe1" }, expect: true, }, { name: "dc", setup: func(n *Node) { n.Datacenter = "dcX" }, expect: false, }, { name: "address", setup: func(n *Node) { n.Address = "127.0.0.1" }, expect: false, }, { name: "tagged addresses", setup: func(n *Node) { n.TaggedAddresses = map[string]string{"my": "address"} }, expect: false, }, { name: "meta", setup: func(n *Node) { n.Meta = map[string]string{"my": "meta"} }, expect: false, }, } run := func(t *testing.T, tc testcase) { other := &Node{ ID: id, Node: node, Datacenter: datacenter, Address: address, TaggedAddresses: make(map[string]string), Meta: make(map[string]string), RaftIndex: RaftIndex{ CreateIndex: 1, ModifyIndex: 3, }, } if !n.IsSame(other) || !other.IsSame(n) { t.Fatalf("should be the same") } tc.setup(other) if tc.expect { if !n.IsSame(other) || !other.IsSame(n) { t.Fatalf("should be the same") } } else { if n.IsSame(other) || other.IsSame(n) { t.Fatalf("should be different, was %#v VS %#v", n, other) } } } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } func TestStructs_ServiceNode_IsSameService(t *testing.T) { const ( nodeName = "node1" ) type testcase struct { name string setup func(*ServiceNode) expect bool } cases := []testcase{ { name: "ServiceID", setup: func(sn *ServiceNode) { sn.ServiceID = "66fb695a-c782-472f-8d36-4f3edd754b37" }, }, { name: "Node", setup: func(sn *ServiceNode) { sn.Node = "other" }, }, { name: "Node casing", setup: func(sn *ServiceNode) { sn.Node = "NoDe1" }, expect: true, }, { name: "ServiceAddress", setup: func(sn *ServiceNode) { sn.ServiceAddress = "1.2.3.4" }, }, { name: "ServiceEnableTagOverride", setup: func(sn *ServiceNode) { sn.ServiceEnableTagOverride = !sn.ServiceEnableTagOverride }, }, { name: "ServiceKind", setup: func(sn *ServiceNode) { sn.ServiceKind = "newKind" }, }, { name: "ServiceMeta", setup: func(sn *ServiceNode) { sn.ServiceMeta = map[string]string{"my": "meta"} }, }, { name: "ServiceName", setup: func(sn *ServiceNode) { sn.ServiceName = "duck" }, }, { name: "ServicePort", setup: func(sn *ServiceNode) { sn.ServicePort = 65534 }, }, { name: "ServiceTags", setup: func(sn *ServiceNode) { sn.ServiceTags = []string{"new", "tags"} }, }, { name: "ServiceWeights", setup: func(sn *ServiceNode) { sn.ServiceWeights = Weights{Passing: 42, Warning: 41} }, }, { name: "ServiceProxy", setup: func(sn *ServiceNode) { sn.ServiceProxy = ConnectProxyConfig{} }, }, { name: "ServiceConnect", setup: func(sn *ServiceNode) { sn.ServiceConnect = ServiceConnect{} }, }, { name: "ServiceTaggedAddresses", setup: func(sn *ServiceNode) { sn.ServiceTaggedAddresses = nil }, }, } run := func(t *testing.T, tc testcase) { sn := testServiceNode(t) sn.ServiceWeights = Weights{Passing: 2, Warning: 1} n := sn.ToNodeService().ToServiceNode(nodeName) other := sn.ToNodeService().ToServiceNode(nodeName) if !n.IsSameService(other) || !other.IsSameService(n) { t.Fatalf("should be the same") } tc.setup(other) if tc.expect { if !n.IsSameService(other) || !other.IsSameService(n) { t.Fatalf("should be the same") } } else { if n.IsSameService(other) || other.IsSameService(n) { t.Fatalf("should be different, was %#v VS %#v", n, other) } } } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } func TestStructs_ServiceNode_PartialClone(t *testing.T) { sn := testServiceNode(t) clone := sn.PartialClone() // Make sure the parts that weren't supposed to be cloned didn't get // copied over, then zero-value them out so we can do a DeepEqual() on // the rest of the contents. if clone.ID != "" || clone.Address != "" || clone.Datacenter != "" || len(clone.TaggedAddresses) != 0 || len(clone.NodeMeta) != 0 { t.Fatalf("bad: %v", clone) } sn.ID = "" sn.Address = "" sn.Datacenter = "" sn.TaggedAddresses = nil sn.NodeMeta = nil require.Equal(t, sn, clone) sn.ServiceTags = append(sn.ServiceTags, "hello") if reflect.DeepEqual(sn, clone) { t.Fatalf("clone wasn't independent of the original") } revert := make([]string, len(sn.ServiceTags)-1) copy(revert, sn.ServiceTags[0:len(sn.ServiceTags)-1]) sn.ServiceTags = revert if !reflect.DeepEqual(sn, clone) { t.Fatalf("bad: %v VS %v", clone, sn) } oldPassingWeight := clone.ServiceWeights.Passing sn.ServiceWeights.Passing = 1000 if reflect.DeepEqual(sn, clone) { t.Fatalf("clone wasn't independent of the original for Meta") } sn.ServiceWeights.Passing = oldPassingWeight sn.ServiceMeta["new_meta"] = "new_value" if reflect.DeepEqual(sn, clone) { t.Fatalf("clone wasn't independent of the original for Meta") } // ensure that the tagged addresses were copied and not just a pointer to the map sn.ServiceTaggedAddresses["foo"] = ServiceAddress{Address: "consul.is.awesome", Port: 443} require.NotEqual(t, sn, clone) } func TestStructs_ServiceNode_Conversions(t *testing.T) { sn := testServiceNode(t) sn2 := sn.ToNodeService().ToServiceNode("node1") // These two fields get lost in the conversion, so we have to zero-value // them out before we do the compare. sn.ID = "" sn.Address = "" sn.Datacenter = "" sn.TaggedAddresses = nil sn.NodeMeta = nil sn.ServiceWeights = Weights{Passing: 1, Warning: 1} require.Equal(t, sn, sn2) if !sn.IsSameService(sn2) || !sn2.IsSameService(sn) { t.Fatalf("bad: %#v, should be the same %#v", sn2, sn) } // Those fields are lost in conversion, so IsSameService() should not take them into account sn.Address = "y" sn.Datacenter = "z" sn.TaggedAddresses = map[string]string{"one": "1", "two": "2"} sn.NodeMeta = map[string]string{"meta": "data"} if !sn.IsSameService(sn2) || !sn2.IsSameService(sn) { t.Fatalf("bad: %#v, should be the same %#v", sn2, sn) } } func TestStructs_Locality_Validate(t *testing.T) { type testCase struct { locality *Locality err string } cases := map[string]testCase{ "nil": { nil, "", }, "region only": { &Locality{Region: "us-west-1"}, "", }, "region and zone": { &Locality{Region: "us-west-1", Zone: "us-west-1a"}, "", }, "zone only": { &Locality{Zone: "us-west-1a"}, "zone cannot be set without region", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := tc.locality.Validate() if tc.err == "" { require.NoError(t, err) } else { require.Error(t, err) require.Contains(t, err.Error(), tc.err) } }) } } func TestStructs_NodeService_ValidateMeshGateway(t *testing.T) { type testCase struct { Modify func(*NodeService) Err string } cases := map[string]testCase{ "valid": { func(x *NodeService) {}, "", }, "zero-port": { func(x *NodeService) { x.Port = 0 }, "Port must be non-zero", }, "sidecar-service": { func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} }, "cannot have a sidecar service", }, "proxy-destination-name": { func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" }, "Proxy.DestinationServiceName configuration is invalid", }, "proxy-destination-id": { func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" }, "Proxy.DestinationServiceID configuration is invalid", }, "proxy-local-address": { func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" }, "Proxy.LocalServiceAddress configuration is invalid", }, "proxy-local-port": { func(x *NodeService) { x.Proxy.LocalServicePort = 36 }, "Proxy.LocalServicePort configuration is invalid", }, "proxy-upstreams": { func(x *NodeService) { x.Proxy.Upstreams = []Upstream{{}} }, "Proxy.Upstreams configuration is invalid", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ns := TestNodeServiceMeshGateway(t) tc.Modify(ns) err := ns.Validate() if tc.Err == "" { require.NoError(t, err) } else { require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) } }) } } func TestStructs_NodeService_ValidateTerminatingGateway(t *testing.T) { type testCase struct { Modify func(*NodeService) Err string } cases := map[string]testCase{ "valid": { func(x *NodeService) {}, "", }, "sidecar-service": { func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} }, "cannot have a sidecar service", }, "proxy-destination-name": { func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" }, "Proxy.DestinationServiceName configuration is invalid", }, "proxy-destination-id": { func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" }, "Proxy.DestinationServiceID configuration is invalid", }, "proxy-local-address": { func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" }, "Proxy.LocalServiceAddress configuration is invalid", }, "proxy-local-port": { func(x *NodeService) { x.Proxy.LocalServicePort = 36 }, "Proxy.LocalServicePort configuration is invalid", }, "proxy-upstreams": { func(x *NodeService) { x.Proxy.Upstreams = []Upstream{{}} }, "Proxy.Upstreams configuration is invalid", }, "port": { func(x *NodeService) { x.Port = 0 }, "Port must be non-zero", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ns := TestNodeServiceTerminatingGateway(t, "10.0.0.5") tc.Modify(ns) err := ns.Validate() if tc.Err == "" { require.NoError(t, err) } else { require.Error(t, err) require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) } }) } } func TestStructs_NodeService_ValidateIngressGateway(t *testing.T) { type testCase struct { Modify func(*NodeService) Err string } cases := map[string]testCase{ "valid": { func(x *NodeService) {}, "", }, "sidecar-service": { func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} }, "cannot have a sidecar service", }, "proxy-destination-name": { func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" }, "Proxy.DestinationServiceName configuration is invalid", }, "proxy-destination-id": { func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" }, "Proxy.DestinationServiceID configuration is invalid", }, "proxy-local-address": { func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" }, "Proxy.LocalServiceAddress configuration is invalid", }, "proxy-local-port": { func(x *NodeService) { x.Proxy.LocalServicePort = 36 }, "Proxy.LocalServicePort configuration is invalid", }, "proxy-upstreams": { func(x *NodeService) { x.Proxy.Upstreams = []Upstream{{}} }, "Proxy.Upstreams configuration is invalid", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ns := TestNodeServiceIngressGateway(t, "10.0.0.5") tc.Modify(ns) err := ns.Validate() if tc.Err == "" { require.NoError(t, err) } else { require.Error(t, err) require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) } }) } } func TestStructs_NodeService_ValidateExposeConfig(t *testing.T) { type testCase struct { Modify func(*NodeService) Err string } cases := map[string]testCase{ "valid": { Modify: func(x *NodeService) {}, Err: "", }, "empty path": { Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].Path = "" }, Err: "empty path exposed", }, "invalid port negative": { Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].ListenerPort = -1 }, Err: "invalid listener port", }, "invalid port too large": { Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].ListenerPort = 65536 }, Err: "invalid listener port", }, "duplicate paths are allowed": { Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].Path = "/healthz" x.Proxy.Expose.Paths[1].Path = "/healthz" }, Err: "", }, "duplicate listener ports are not allowed": { Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].ListenerPort = 21600 x.Proxy.Expose.Paths[1].ListenerPort = 21600 }, Err: "duplicate listener ports exposed", }, "protocol not supported": { Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].Protocol = "foo" }, Err: "protocol 'foo' not supported for path", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ns := TestNodeServiceExpose(t) tc.Modify(ns) err := ns.Validate() if tc.Err == "" { require.NoError(t, err) } else { require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) } }) } } func TestStructs_NodeService_ValidateConnectProxy(t *testing.T) { cases := []struct { Name string Modify func(*NodeService) Err string }{ { "valid", func(x *NodeService) {}, "", }, { "connect-proxy: invalid opaque config", func(x *NodeService) { x.Proxy.Config = map[string]interface{}{ "envoy_hcp_metrics_bind_socket_dir": "/Consul/is/a/networking/platform/that/enables/securing/your/networking/", } }, "Proxy.Config: envoy_hcp_metrics_bind_socket_dir length 71 exceeds max", }, { "connect-proxy: no Proxy.DestinationServiceName", func(x *NodeService) { x.Proxy.DestinationServiceName = "" }, "Proxy.DestinationServiceName must be", }, { "connect-proxy: whitespace Proxy.DestinationServiceName", func(x *NodeService) { x.Proxy.DestinationServiceName = " " }, "Proxy.DestinationServiceName must be", }, { "connect-proxy: wildcard Proxy.DestinationServiceName", func(x *NodeService) { x.Proxy.DestinationServiceName = "*" }, "Proxy.DestinationServiceName must not be", }, { "connect-proxy: valid Proxy.DestinationServiceName", func(x *NodeService) { x.Proxy.DestinationServiceName = "hello" }, "", }, { "connect-proxy: no port set", func(x *NodeService) { x.Port = 0 }, fmt.Sprintf("Port or SocketPath must be set for a %s", ServiceKindConnectProxy), }, { "connect-proxy: ConnectNative set", func(x *NodeService) { x.Connect.Native = true }, "cannot also be", }, { "connect-proxy: upstream missing type (defaulted)", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{{ DestinationName: "foo", LocalBindPort: 5000, }} }, "", }, { "connect-proxy: upstream invalid type", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{{ DestinationType: "garbage", DestinationName: "foo", LocalBindPort: 5000, }} }, "unknown upstream destination type", }, { "connect-proxy: upstream empty name", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{{ DestinationType: UpstreamDestTypeService, LocalBindPort: 5000, }} }, "upstream destination name cannot be empty", }, { "connect-proxy: upstream wildcard name", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{{ DestinationType: UpstreamDestTypeService, DestinationName: WildcardSpecifier, LocalBindPort: 5000, }} }, "upstream destination name cannot be a wildcard", }, { "connect-proxy: upstream can have wildcard name when centrally configured", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{{ DestinationType: UpstreamDestTypeService, DestinationName: WildcardSpecifier, CentrallyConfigured: true, }} }, "", }, { "connect-proxy: upstream empty bind port", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{{ DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 0, }} }, "upstream local bind port or local socket path must be defined and nonzero", }, { "connect-proxy: upstream bind port and path defined", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{{ DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 1, LocalBindSocketPath: "/tmp/socket", }} }, "only one of upstream local bind port or local socket path can be defined and nonzero", }, { "connect-proxy: Upstreams almost-but-not-quite-duplicated in various ways", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { // baseline DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 5000, }, { // different bind address DestinationType: UpstreamDestTypeService, DestinationName: "bar", LocalBindAddress: "127.0.0.2", LocalBindPort: 5000, }, { // different datacenter DestinationType: UpstreamDestTypeService, DestinationName: "foo", Datacenter: "dc2", LocalBindPort: 5001, }, { // explicit default namespace DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationNamespace: "default", LocalBindPort: 5003, }, { // different namespace DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationNamespace: "alternate", LocalBindPort: 5002, }, { // different type DestinationType: UpstreamDestTypePreparedQuery, DestinationName: "foo", LocalBindPort: 5004, }, } }, "", }, { "connect-proxy: Upstreams non default partition another dc", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { // baseline DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationPartition: "foo", Datacenter: "dc1", LocalBindPort: 5000, }, } }, "upstreams cannot target another datacenter in non default partition", }, { "connect-proxy: Upstreams duplicated by port", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 5000, }, { DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 5000, }, } }, "upstreams cannot contain duplicates", }, { "connect-proxy: Centrally configured upstreams can have duplicate ip/port", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", CentrallyConfigured: true, }, { DestinationType: UpstreamDestTypeService, DestinationName: "bar", CentrallyConfigured: true, }, } }, "", }, { "connect-proxy: Upstreams duplicated by ip and port", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindAddress: "127.0.0.2", LocalBindPort: 5000, }, { DestinationType: UpstreamDestTypeService, DestinationName: "bar", LocalBindAddress: "127.0.0.2", LocalBindPort: 5000, }, } }, "upstreams cannot contain duplicates", }, { "connect-proxy: Upstreams duplicated by ip and port with ip defaulted in one", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 5000, }, { DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindAddress: "127.0.0.1", LocalBindPort: 5000, }, } }, "upstreams cannot contain duplicates", }, { "connect-proxy: Upstreams duplicated by name", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 5000, }, { DestinationType: UpstreamDestTypeService, DestinationName: "foo", LocalBindPort: 5001, }, } }, "upstreams cannot contain duplicates", }, { "connect-proxy: Upstreams duplicated by name and datacenter", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", Datacenter: "dc2", LocalBindPort: 5000, }, { DestinationType: UpstreamDestTypeService, DestinationName: "foo", Datacenter: "dc2", LocalBindPort: 5001, }, } }, "upstreams cannot contain duplicates", }, { "connect-proxy: Upstreams duplicated by name and namespace", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationNamespace: "alternate", LocalBindPort: 5000, }, { DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationNamespace: "alternate", LocalBindPort: 5001, }, } }, "upstreams cannot contain duplicates", }, { "connect-proxy: valid Upstream.PeerDestination", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationPeer: "peer1", LocalBindPort: 5000, }, } }, "", }, { "connect-proxy: invalid locality", func(x *NodeService) { x.Locality = &Locality{Zone: "bad"} }, "zone cannot be set without region", }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { ns := TestNodeServiceProxy(t) tc.Modify(ns) err := ns.Validate() assert.Equal(t, err != nil, tc.Err != "", err) if err == nil { return } assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) }) } } func TestStructs_NodeService_ValidateConnectProxyWithAgentAutoAssign(t *testing.T) { t.Run("connect-proxy: no port set", func(t *testing.T) { ns := TestNodeServiceProxy(t) ns.Port = 0 err := ns.ValidateForAgent() assert.NoError(t, err) }) } func TestStructs_NodeService_ValidateConnectProxy_In_Partition(t *testing.T) { cases := []struct { Name string Modify func(*NodeService) Err string }{ { "valid", func(x *NodeService) {}, "", }, { "connect-proxy: Upstreams non default partition another dc", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { // baseline DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationPartition: "foo", Datacenter: "dc1", LocalBindPort: 5000, }, } }, "upstreams cannot target another datacenter in non default partition", }, { "connect-proxy: Upstreams non default partition same dc", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { // baseline DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationPartition: "foo", LocalBindPort: 5000, }, } }, "", }, { "connect-proxy: Upstream with peer targets partition different from NodeService", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", DestinationPartition: "part1", DestinationPeer: "peer1", LocalBindPort: 5000, }, } }, "upstreams must target peers in the same partition as the service", }, { "connect-proxy: Upstream with peer defaults to NodeService's peer", func(x *NodeService) { x.Proxy.Upstreams = Upstreams{ { DestinationType: UpstreamDestTypeService, DestinationName: "foo", // No DestinationPartition here but we assert that it defaults to "bar" and not "default" DestinationPeer: "peer1", LocalBindPort: 5000, }, } }, "", }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { ns := TestNodeServiceProxyInPartition(t, "bar") tc.Modify(ns) err := ns.Validate() assert.Equal(t, err != nil, tc.Err != "", err) if err == nil { return } assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) }) } } func TestStructs_NodeService_ValidateSidecarService(t *testing.T) { cases := []struct { Name string Modify func(*NodeService) Err string }{ { "valid", func(x *NodeService) {}, "", }, { "ID can't be set", func(x *NodeService) { x.Connect.SidecarService.ID = "foo" }, "SidecarService cannot specify an ID", }, { "Nested sidecar can't be set", func(x *NodeService) { x.Connect.SidecarService.Connect = &ServiceConnect{ SidecarService: &ServiceDefinition{}, } }, "SidecarService cannot have a nested SidecarService", }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { ns := TestNodeServiceSidecar(t) tc.Modify(ns) err := ns.Validate() assert.Equal(t, err != nil, tc.Err != "", err) if err == nil { return } assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) }) } } func TestStructs_NodeService_ConnectNativeEmptyPortError(t *testing.T) { ns := TestNodeService() ns.Connect.Native = true ns.Port = 0 err := ns.Validate() assert.Error(t, err) assert.Contains(t, err.Error(), "Port or SocketPath must be set for a Connect native service.") } func TestStructs_NodeService_IsSame(t *testing.T) { ns := &NodeService{ ID: "node1", Service: "theservice", Tags: []string{"foo", "bar"}, Address: "127.0.0.1", TaggedAddresses: map[string]ServiceAddress{ "lan": { Address: "127.0.0.1", Port: 3456, }, "wan": { Address: "198.18.0.1", Port: 1234, }, }, Meta: map[string]string{ "meta1": "value1", "meta2": "value2", }, Port: 1234, EnableTagOverride: true, Proxy: ConnectProxyConfig{ DestinationServiceName: "db", Config: map[string]interface{}{ "foo": "bar", }, }, Weights: &Weights{Passing: 1, Warning: 1}, } if !ns.IsSame(ns) { t.Fatalf("should be equal to itself") } other := &NodeService{ ID: "node1", Service: "theservice", Tags: []string{"foo", "bar"}, Address: "127.0.0.1", Port: 1234, EnableTagOverride: true, TaggedAddresses: map[string]ServiceAddress{ "wan": { Address: "198.18.0.1", Port: 1234, }, "lan": { Address: "127.0.0.1", Port: 3456, }, }, Meta: map[string]string{ // We don't care about order "meta2": "value2", "meta1": "value1", }, Proxy: ConnectProxyConfig{ DestinationServiceName: "db", Config: map[string]interface{}{ "foo": "bar", }, }, Weights: &Weights{Passing: 1, Warning: 1}, RaftIndex: RaftIndex{ CreateIndex: 1, ModifyIndex: 2, }, } if !ns.IsSame(other) || !other.IsSame(ns) { t.Fatalf("should not care about Raft fields") } check := func(twiddle, restore func()) { t.Helper() if !ns.IsSame(other) || !other.IsSame(ns) { t.Fatalf("should be the same") } twiddle() if ns.IsSame(other) || other.IsSame(ns) { t.Fatalf("should not be the same") } restore() if !ns.IsSame(other) || !other.IsSame(ns) { t.Fatalf("should be the same again") } } check(func() { other.ID = "XXX" }, func() { other.ID = "node1" }) check(func() { other.Service = "XXX" }, func() { other.Service = "theservice" }) check(func() { other.Tags = nil }, func() { other.Tags = []string{"foo", "bar"} }) check(func() { other.Tags = []string{"foo"} }, func() { other.Tags = []string{"foo", "bar"} }) check(func() { other.Address = "XXX" }, func() { other.Address = "127.0.0.1" }) check(func() { other.Port = 9999 }, func() { other.Port = 1234 }) check(func() { other.Meta["meta2"] = "wrongValue" }, func() { other.Meta["meta2"] = "value2" }) check(func() { other.EnableTagOverride = false }, func() { other.EnableTagOverride = true }) check(func() { other.Kind = ServiceKindConnectProxy }, func() { other.Kind = "" }) check(func() { other.Proxy.DestinationServiceName = "" }, func() { other.Proxy.DestinationServiceName = "db" }) check(func() { other.Proxy.DestinationServiceID = "XXX" }, func() { other.Proxy.DestinationServiceID = "" }) check(func() { other.Proxy.LocalServiceAddress = "XXX" }, func() { other.Proxy.LocalServiceAddress = "" }) check(func() { other.Proxy.LocalServicePort = 9999 }, func() { other.Proxy.LocalServicePort = 0 }) check(func() { other.Proxy.Config["baz"] = "XXX" }, func() { delete(other.Proxy.Config, "baz") }) check(func() { other.Connect.Native = true }, func() { other.Connect.Native = false }) otherServiceNode := other.ToServiceNode("node1") copyNodeService := otherServiceNode.ToNodeService() if !copyNodeService.IsSame(other) { t.Fatalf("copy should be the same, but was\n %#v\nVS\n %#v", copyNodeService, other) } otherServiceNodeCopy2 := copyNodeService.ToServiceNode("node1") if !otherServiceNode.IsSameService(otherServiceNodeCopy2) { t.Fatalf("copy should be the same, but was\n %#v\nVS\n %#v", otherServiceNode, otherServiceNodeCopy2) } check(func() { other.TaggedAddresses["lan"] = ServiceAddress{Address: "127.0.0.1", Port: 9999} }, func() { other.TaggedAddresses["lan"] = ServiceAddress{Address: "127.0.0.1", Port: 3456} }) } func TestStructs_HealthCheck_IsSame(t *testing.T) { type testcase struct { name string setup func(*HealthCheck) expect bool } cases := []testcase{ { name: "Node", setup: func(hc *HealthCheck) { hc.Node = "XXX" }, }, { name: "Node casing", setup: func(hc *HealthCheck) { hc.Node = "NoDe1" }, expect: true, }, { name: "CheckID", setup: func(hc *HealthCheck) { hc.CheckID = "XXX" }, }, { name: "Name", setup: func(hc *HealthCheck) { hc.Name = "XXX" }, }, { name: "Status", setup: func(hc *HealthCheck) { hc.Status = "XXX" }, }, { name: "Notes", setup: func(hc *HealthCheck) { hc.Notes = "XXX" }, }, { name: "Output", setup: func(hc *HealthCheck) { hc.Output = "XXX" }, }, { name: "ServiceID", setup: func(hc *HealthCheck) { hc.ServiceID = "XXX" }, }, { name: "ServiceName", setup: func(hc *HealthCheck) { hc.ServiceName = "XXX" }, }, } run := func(t *testing.T, tc testcase) { hc := &HealthCheck{ Node: "node1", CheckID: "check1", Name: "thecheck", Status: api.HealthPassing, Notes: "it's all good", Output: "lgtm", ServiceID: "service1", ServiceName: "theservice", ServiceTags: []string{"foo"}, } if !hc.IsSame(hc) { t.Fatalf("should be equal to itself") } other := &HealthCheck{ Node: "node1", CheckID: "check1", Name: "thecheck", Status: api.HealthPassing, Notes: "it's all good", Output: "lgtm", ServiceID: "service1", ServiceName: "theservice", ServiceTags: []string{"foo"}, RaftIndex: RaftIndex{ CreateIndex: 1, ModifyIndex: 2, }, } if !hc.IsSame(other) || !other.IsSame(hc) { t.Fatalf("should not care about Raft fields") } tc.setup(hc) if tc.expect { if !hc.IsSame(other) || !other.IsSame(hc) { t.Fatalf("should be the same") } } else { if hc.IsSame(other) || other.IsSame(hc) { t.Fatalf("should not be the same") } } } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } func TestStructs_HealthCheck_Marshalling(t *testing.T) { d := &HealthCheckDefinition{} buf, err := d.MarshalJSON() require.NoError(t, err) require.NotContains(t, string(buf), `"Interval":""`) require.NotContains(t, string(buf), `"Timeout":""`) require.NotContains(t, string(buf), `"DeregisterCriticalServiceAfter":""`) } func TestStructs_HealthCheck_Clone(t *testing.T) { hc := &HealthCheck{ Node: "node1", CheckID: "check1", Name: "thecheck", Status: api.HealthPassing, Notes: "it's all good", Output: "lgtm", ServiceID: "service1", ServiceName: "theservice", } clone := hc.Clone() if !hc.IsSame(clone) { t.Fatalf("should be equal to its clone") } clone.Output = "different" if hc.IsSame(clone) { t.Fatalf("should not longer be equal to its clone") } } func TestCheckServiceNodes_Shuffle(t *testing.T) { // Make a huge list of nodes. var nodes CheckServiceNodes for i := 0; i < 100; i++ { nodes = append(nodes, CheckServiceNode{ Node: &Node{ Node: fmt.Sprintf("node%d", i), Address: fmt.Sprintf("127.0.0.%d", i+1), }, }) } // Keep track of how many unique shuffles we get. uniques := make(map[string]struct{}) for i := 0; i < 100; i++ { nodes.Shuffle() var names []string for _, node := range nodes { names = append(names, node.Node.Node) } key := strings.Join(names, "|") uniques[key] = struct{}{} } // We have to allow for the fact that there won't always be a unique // shuffle each pass, so we just look for smell here without the test // being flaky. if len(uniques) < 50 { t.Fatalf("unique shuffle ratio too low: %d/100", len(uniques)) } } func TestCheckServiceNodes_Filter(t *testing.T) { nodes := CheckServiceNodes{ CheckServiceNode{ Node: &Node{ Node: "node1", Address: "127.0.0.1", }, Checks: HealthChecks{ &HealthCheck{ Status: api.HealthWarning, }, }, }, CheckServiceNode{ Node: &Node{ Node: "node2", Address: "127.0.0.2", }, Checks: HealthChecks{ &HealthCheck{ Status: api.HealthPassing, }, }, }, CheckServiceNode{ Node: &Node{ Node: "node3", Address: "127.0.0.3", }, Checks: HealthChecks{ &HealthCheck{ Status: api.HealthCritical, }, }, }, CheckServiceNode{ Node: &Node{ Node: "node4", Address: "127.0.0.4", }, Checks: HealthChecks{ // This check has a different ID to the others to ensure it is not // ignored by accident &HealthCheck{ CheckID: "failing2", Status: api.HealthCritical, }, }, }, } // Test the case where warnings are allowed. { twiddle := make(CheckServiceNodes, len(nodes)) if n := copy(twiddle, nodes); n != len(nodes) { t.Fatalf("bad: %d", n) } filtered := twiddle.Filter(false) expected := CheckServiceNodes{ nodes[0], nodes[1], } if !reflect.DeepEqual(filtered, expected) { t.Fatalf("bad: %v", filtered) } } // Limit to only passing checks. { twiddle := make(CheckServiceNodes, len(nodes)) if n := copy(twiddle, nodes); n != len(nodes) { t.Fatalf("bad: %d", n) } filtered := twiddle.Filter(true) expected := CheckServiceNodes{ nodes[1], } if !reflect.DeepEqual(filtered, expected) { t.Fatalf("bad: %v", filtered) } } // Allow failing checks to be ignored (note that the test checks have empty // CheckID which is valid). { twiddle := make(CheckServiceNodes, len(nodes)) if n := copy(twiddle, nodes); n != len(nodes) { t.Fatalf("bad: %d", n) } filtered := twiddle.FilterIgnore(true, []types.CheckID{""}) expected := CheckServiceNodes{ nodes[0], nodes[1], nodes[2], // Node 3's critical check should be ignored. // Node 4 should still be failing since it's got a critical check with a // non-ignored ID. } if !reflect.DeepEqual(filtered, expected) { t.Fatalf("bad: %v", filtered) } } } func TestCheckServiceNode_CanRead(t *testing.T) { type testCase struct { name string csn CheckServiceNode authz acl.Authorizer expected acl.EnforcementDecision } fn := func(t *testing.T, tc testCase) { actual := tc.csn.CanRead(tc.authz) require.Equal(t, tc.expected, actual) } var testCases = []testCase{ { name: "empty", expected: acl.Deny, }, { name: "node read not authorized", csn: CheckServiceNode{ Node: &Node{Node: "name"}, Service: &NodeService{Service: "service-name"}, }, authz: aclAuthorizerCheckServiceNode{allowLocalService: true}, expected: acl.Deny, }, { name: "service read not authorized", csn: CheckServiceNode{ Node: &Node{Node: "name"}, Service: &NodeService{Service: "service-name"}, }, authz: aclAuthorizerCheckServiceNode{allowLocalNode: true}, expected: acl.Deny, }, { name: "read authorized", csn: CheckServiceNode{ Node: &Node{Node: "name"}, Service: &NodeService{Service: "service-name"}, }, authz: acl.AllowAll(), expected: acl.Allow, }, { name: "can read imported csn if can read imported data", csn: CheckServiceNode{ Node: &Node{Node: "name", PeerName: "cluster-2"}, Service: &NodeService{Service: "service-name", PeerName: "cluster-2"}, }, authz: aclAuthorizerCheckServiceNode{allowImported: true}, expected: acl.Allow, }, { name: "can't read imported csn with authz for local services and nodes", csn: CheckServiceNode{ Node: &Node{Node: "name", PeerName: "cluster-2"}, Service: &NodeService{Service: "service-name", PeerName: "cluster-2"}, }, authz: aclAuthorizerCheckServiceNode{allowLocalService: true, allowLocalNode: true}, expected: acl.Deny, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { fn(t, tc) }) } } type aclAuthorizerCheckServiceNode struct { acl.Authorizer allowLocalNode bool allowLocalService bool allowImported bool } func (a aclAuthorizerCheckServiceNode) ServiceRead(_ string, ctx *acl.AuthorizerContext) acl.EnforcementDecision { if ctx.Peer != "" { if a.allowImported { return acl.Allow } return acl.Deny } if a.allowLocalService { return acl.Allow } return acl.Deny } func (a aclAuthorizerCheckServiceNode) NodeRead(_ string, ctx *acl.AuthorizerContext) acl.EnforcementDecision { if ctx.Peer != "" { if a.allowImported { return acl.Allow } return acl.Deny } if a.allowLocalNode { return acl.Allow } return acl.Deny } func TestStructs_DirEntry_Clone(t *testing.T) { e := &DirEntry{ LockIndex: 5, Key: "hello", Flags: 23, Value: []byte("this is a test"), Session: "session1", RaftIndex: RaftIndex{ CreateIndex: 1, ModifyIndex: 2, }, } clone := e.Clone() if !reflect.DeepEqual(e, clone) { t.Fatalf("bad: %v", clone) } e.Value = []byte("a new value") if reflect.DeepEqual(e, clone) { t.Fatalf("clone wasn't independent of the original") } } func TestStructs_ValidateServiceAndNodeMetadata(t *testing.T) { tooMuchMeta := make(map[string]string) for i := 0; i < metaMaxKeyPairs+1; i++ { tooMuchMeta[fmt.Sprint(i)] = "value" } type testcase struct { Meta map[string]string AllowConsulPrefix bool NodeError string ServiceError string GatewayError string } cases := map[string]testcase{ "should succeed": { map[string]string{ "key1": "value1", "key2": "value2", }, false, "", "", "", }, "invalid key": { map[string]string{ "": "value1", }, false, "Couldn't load metadata pair", "Couldn't load metadata pair", "Couldn't load metadata pair", }, "too many keys": { tooMuchMeta, false, "cannot contain more than", "cannot contain more than", "cannot contain more than", }, "reserved key prefix denied": { map[string]string{ MetaKeyReservedPrefix + "key": "value1", }, false, "reserved for internal use", "reserved for internal use", "reserved for internal use", }, "reserved key prefix allowed": { map[string]string{ MetaKeyReservedPrefix + "key": "value1", }, true, "", "", "", }, "reserved key prefix allowed via an allowlist just for gateway - " + MetaWANFederationKey: { map[string]string{ MetaWANFederationKey: "value1", }, false, "reserved for internal use", "reserved for internal use", "", }, } for name, tc := range cases { tc := tc t.Run(name, func(t *testing.T) { t.Run("ValidateNodeMetadata", func(t *testing.T) { err := ValidateNodeMetadata(tc.Meta, tc.AllowConsulPrefix) if tc.NodeError == "" { require.NoError(t, err) } else { testutil.RequireErrorContains(t, err, tc.NodeError) } }) t.Run("ValidateServiceMetadata - typical", func(t *testing.T) { err := ValidateServiceMetadata(ServiceKindTypical, tc.Meta, tc.AllowConsulPrefix) if tc.ServiceError == "" { require.NoError(t, err) } else { testutil.RequireErrorContains(t, err, tc.ServiceError) } }) t.Run("ValidateServiceMetadata - mesh-gateway", func(t *testing.T) { err := ValidateServiceMetadata(ServiceKindMeshGateway, tc.Meta, tc.AllowConsulPrefix) if tc.GatewayError == "" { require.NoError(t, err) } else { testutil.RequireErrorContains(t, err, tc.GatewayError) } }) }) } } func TestStructs_validateMetaPair(t *testing.T) { longKey := strings.Repeat("a", metaKeyMaxLength+1) longValue := strings.Repeat("b", metaValueMaxLength+1) pairs := []struct { Key string Value string Error string AllowConsulPrefix bool AllowConsulKeys map[string]struct{} }{ // valid pair {"key", "value", "", false, nil}, // invalid, blank key {"", "value", "cannot be blank", false, nil}, // allowed special chars in key name {"k_e-y", "value", "", false, nil}, // disallowed special chars in key name {"(%key&)", "value", "invalid characters", false, nil}, // key too long {longKey, "value", "Key is too long", false, nil}, // reserved prefix {MetaKeyReservedPrefix + "key", "value", "reserved for internal use", false, nil}, // reserved prefix, allowed {MetaKeyReservedPrefix + "key", "value", "", true, nil}, // reserved prefix, not allowed via an allowlist {MetaKeyReservedPrefix + "bad", "value", "reserved for internal use", false, map[string]struct{}{MetaKeyReservedPrefix + "good": {}}}, // reserved prefix, allowed via an allowlist {MetaKeyReservedPrefix + "good", "value", "", true, map[string]struct{}{MetaKeyReservedPrefix + "good": {}}}, // value too long {"key", longValue, "Value is too long", false, nil}, } for _, pair := range pairs { err := validateMetaPair(pair.Key, pair.Value, pair.AllowConsulPrefix, pair.AllowConsulKeys) if pair.Error == "" && err != nil { t.Fatalf("should have succeeded: %v, %v", pair, err) } else if pair.Error != "" && !strings.Contains(err.Error(), pair.Error) { t.Fatalf("should have failed: %v, %v", pair, err) } } } func TestDCSpecificRequest_CacheInfoKey(t *testing.T) { assertCacheInfoKeyIsComplete(t, &DCSpecificRequest{}) } func TestNodeSpecificRequest_CacheInfoKey(t *testing.T) { assertCacheInfoKeyIsComplete(t, &NodeSpecificRequest{}) } func TestServiceSpecificRequest_CacheInfoKey(t *testing.T) { assertCacheInfoKeyIsComplete(t, &ServiceSpecificRequest{}) } func TestServiceDumpRequest_CacheInfoKey(t *testing.T) { // ServiceKind is only included when UseServiceKind=true assertCacheInfoKeyIsComplete(t, &ServiceDumpRequest{}, "ServiceKind") } // cacheInfoIgnoredFields are fields that can be ignored in all cache.Request types // because the cache itself includes these values in the cache key, or because // they are options used to specify the cache operation, and are not part of the // cache entry value. var cacheInfoIgnoredFields = map[string]bool{ // Datacenter is part of the cache key added by the cache itself. "Datacenter": true, // PeerName is part of the cache key added by the cache itself. "PeerName": true, // QuerySource is always the same for every request from a single agent, so it // is excluded from the key. "Source": true, // EnterpriseMeta is an empty struct, so can not be included. enterpriseMetaField: true, } // assertCacheInfoKeyIsComplete is an assertion to verify that all fields on a request // struct are considered as part of the cache key. It is used to prevent regressions // when new fields are added to the struct. If a field is not included in the cache // key it can lead to API requests or DNS requests returning the wrong value // because a request matches the wrong entry in the agent/cache.Cache. func assertCacheInfoKeyIsComplete(t *testing.T, request cache.Request, ignoredFields ...string) { t.Helper() ignored := make(map[string]bool, len(ignoredFields)) for _, f := range ignoredFields { ignored[f] = true } fuzzer := fuzz.NewWithSeed(time.Now().UnixNano()) fuzzer.Funcs(randQueryOptions) fuzzer.Fuzz(request) requestValue := reflect.ValueOf(request).Elem() for i := 0; i < requestValue.NumField(); i++ { originalKey := request.CacheInfo().Key field := requestValue.Field(i) fieldName := requestValue.Type().Field(i).Name originalValue := field.Interface() if cacheInfoIgnoredFields[fieldName] || ignored[fieldName] { continue } for i := 0; reflect.DeepEqual(originalValue, field.Interface()) && i < 20; i++ { fuzzer.Fuzz(field.Addr().Interface()) } key := request.CacheInfo().Key if originalKey == key { t.Fatalf("expected field %v to be represented in the CacheInfo.Key, %v change to %v (key: %v)", fieldName, originalValue, field.Interface(), key) } } } func randQueryOptions(o *QueryOptions, c fuzz.Continue) { c.Fuzz(&o.Filter) } func TestSpecificServiceRequest_CacheInfo(t *testing.T) { tests := []struct { name string req ServiceSpecificRequest mutate func(req *ServiceSpecificRequest) want *cache.RequestInfo wantSame bool }{ { name: "basic params", req: ServiceSpecificRequest{ QueryOptions: QueryOptions{Token: "foo"}, Datacenter: "dc1", }, want: &cache.RequestInfo{ Token: "foo", Datacenter: "dc1", }, wantSame: true, }, { name: "name should be considered", req: ServiceSpecificRequest{ ServiceName: "web", }, mutate: func(req *ServiceSpecificRequest) { req.ServiceName = "db" }, wantSame: false, }, { name: "node meta should be considered", req: ServiceSpecificRequest{ NodeMetaFilters: map[string]string{ "foo": "bar", }, }, mutate: func(req *ServiceSpecificRequest) { req.NodeMetaFilters = map[string]string{ "foo": "qux", } }, wantSame: false, }, { name: "address should be considered", req: ServiceSpecificRequest{ ServiceAddress: "1.2.3.4", }, mutate: func(req *ServiceSpecificRequest) { req.ServiceAddress = "4.3.2.1" }, wantSame: false, }, { name: "tag filter should be considered", req: ServiceSpecificRequest{ TagFilter: true, }, mutate: func(req *ServiceSpecificRequest) { req.TagFilter = false }, wantSame: false, }, { name: "connect should be considered", req: ServiceSpecificRequest{ Connect: true, }, mutate: func(req *ServiceSpecificRequest) { req.Connect = false }, wantSame: false, }, { name: "tags should be different", req: ServiceSpecificRequest{ ServiceName: "web", ServiceTags: []string{"foo"}, }, mutate: func(req *ServiceSpecificRequest) { req.ServiceTags = []string{"foo", "bar"} }, wantSame: false, }, { name: "tags should not depend on order", req: ServiceSpecificRequest{ ServiceName: "web", ServiceTags: []string{"bar", "foo"}, }, mutate: func(req *ServiceSpecificRequest) { req.ServiceTags = []string{"foo", "bar"} }, wantSame: true, }, // DEPRECATED (singular-service-tag) - remove this when upgrade RPC compat // with 1.2.x is not required. { name: "legacy requests with singular tag should be different", req: ServiceSpecificRequest{ ServiceName: "web", ServiceTag: "foo", }, mutate: func(req *ServiceSpecificRequest) { req.ServiceTag = "bar" }, wantSame: false, }, { name: "with integress=true", req: ServiceSpecificRequest{ Datacenter: "dc1", ServiceName: "my-service", }, mutate: func(req *ServiceSpecificRequest) { req.Ingress = true }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { info := tc.req.CacheInfo() if tc.mutate != nil { tc.mutate(&tc.req) } afterInfo := tc.req.CacheInfo() // Check key matches or not if tc.wantSame { require.Equal(t, info, afterInfo) } else { require.NotEqual(t, info, afterInfo) } if tc.want != nil { // Reset key since we don't care about the actual hash value as long as // it does/doesn't change appropriately (asserted with wantSame above). info.Key = "" require.Equal(t, *tc.want, info) } }) } } func TestNodeService_JSON_OmitTaggedAdddresses(t *testing.T) { cases := []struct { name string ns NodeService }{ { "nil", NodeService{ TaggedAddresses: nil, }, }, { "empty", NodeService{ TaggedAddresses: make(map[string]ServiceAddress), }, }, } for _, tc := range cases { name := tc.name ns := tc.ns t.Run(name, func(t *testing.T) { data, err := json.Marshal(ns) require.NoError(t, err) var raw map[string]interface{} err = json.Unmarshal(data, &raw) require.NoError(t, err) require.NotContains(t, raw, "TaggedAddresses") require.NotContains(t, raw, "tagged_addresses") }) } } func TestServiceNode_JSON_OmitServiceTaggedAdddresses(t *testing.T) { cases := []struct { name string sn ServiceNode }{ { "nil", ServiceNode{ ServiceTaggedAddresses: nil, }, }, { "empty", ServiceNode{ ServiceTaggedAddresses: make(map[string]ServiceAddress), }, }, } for _, tc := range cases { name := tc.name sn := tc.sn t.Run(name, func(t *testing.T) { data, err := json.Marshal(sn) require.NoError(t, err) var raw map[string]interface{} err = json.Unmarshal(data, &raw) require.NoError(t, err) require.NotContains(t, raw, "ServiceTaggedAddresses") require.NotContains(t, raw, "service_tagged_addresses") }) } } func TestNode_BestAddress(t *testing.T) { type testCase struct { input Node lanAddr string wanAddr string } nodeAddr := "10.1.2.3" nodeWANAddr := "198.18.19.20" cases := map[string]testCase{ "address": { input: Node{ Address: nodeAddr, }, lanAddr: nodeAddr, wanAddr: nodeAddr, }, "wan-address": { input: Node{ Address: nodeAddr, TaggedAddresses: map[string]string{ "wan": nodeWANAddr, }, }, lanAddr: nodeAddr, wanAddr: nodeWANAddr, }, } for name, tc := range cases { name := name tc := tc t.Run(name, func(t *testing.T) { require.Equal(t, tc.lanAddr, tc.input.BestAddress(false)) require.Equal(t, tc.wanAddr, tc.input.BestAddress(true)) }) } } func TestNodeService_BestAddress(t *testing.T) { type testCase struct { input NodeService lanAddr string lanPort int wanAddr string wanPort int } serviceAddr := "10.2.3.4" servicePort := 1234 serviceWANAddr := "198.19.20.21" serviceWANPort := 987 cases := map[string]testCase{ "no-address": { input: NodeService{ Port: servicePort, }, lanAddr: "", lanPort: servicePort, wanAddr: "", wanPort: servicePort, }, "service-address": { input: NodeService{ Address: serviceAddr, Port: servicePort, }, lanAddr: serviceAddr, lanPort: servicePort, wanAddr: serviceAddr, wanPort: servicePort, }, "service-wan-address": { input: NodeService{ Address: serviceAddr, Port: servicePort, TaggedAddresses: map[string]ServiceAddress{ "wan": { Address: serviceWANAddr, Port: serviceWANPort, }, }, }, lanAddr: serviceAddr, lanPort: servicePort, wanAddr: serviceWANAddr, wanPort: serviceWANPort, }, "service-wan-address-default-port": { input: NodeService{ Address: serviceAddr, Port: servicePort, TaggedAddresses: map[string]ServiceAddress{ "wan": { Address: serviceWANAddr, Port: 0, }, }, }, lanAddr: serviceAddr, lanPort: servicePort, wanAddr: serviceWANAddr, wanPort: servicePort, }, "service-wan-address-node-lan": { input: NodeService{ Port: servicePort, TaggedAddresses: map[string]ServiceAddress{ "wan": { Address: serviceWANAddr, Port: serviceWANPort, }, }, }, lanAddr: "", lanPort: servicePort, wanAddr: serviceWANAddr, wanPort: serviceWANPort, }, } for name, tc := range cases { name := name tc := tc t.Run(name, func(t *testing.T) { addr, port := tc.input.BestAddress(false) require.Equal(t, tc.lanAddr, addr) require.Equal(t, tc.lanPort, port) addr, port = tc.input.BestAddress(true) require.Equal(t, tc.wanAddr, addr) require.Equal(t, tc.wanPort, port) }) } } func TestCheckServiceNode_BestAddress(t *testing.T) { type testCase struct { input CheckServiceNode lanAddr string lanPort int lanIdx uint64 wanAddr string wanPort int wanIdx uint64 } nodeAddr := "10.1.2.3" nodeWANAddr := "198.18.19.20" nodeIdx := uint64(11) serviceAddr := "10.2.3.4" servicePort := 1234 serviceIdx := uint64(22) serviceWANAddr := "198.19.20.21" serviceWANPort := 987 cases := map[string]testCase{ "node-address": { input: CheckServiceNode{ Node: &Node{ Address: nodeAddr, RaftIndex: RaftIndex{ ModifyIndex: nodeIdx, }, }, Service: &NodeService{ Port: servicePort, RaftIndex: RaftIndex{ ModifyIndex: serviceIdx, }, }, }, lanAddr: nodeAddr, lanIdx: nodeIdx, lanPort: servicePort, wanAddr: nodeAddr, wanIdx: nodeIdx, wanPort: servicePort, }, "node-wan-address": { input: CheckServiceNode{ Node: &Node{ Address: nodeAddr, TaggedAddresses: map[string]string{ "wan": nodeWANAddr, }, RaftIndex: RaftIndex{ ModifyIndex: nodeIdx, }, }, Service: &NodeService{ Port: servicePort, RaftIndex: RaftIndex{ ModifyIndex: serviceIdx, }, }, }, lanAddr: nodeAddr, lanIdx: nodeIdx, lanPort: servicePort, wanAddr: nodeWANAddr, wanIdx: nodeIdx, wanPort: servicePort, }, "service-address": { input: CheckServiceNode{ Node: &Node{ Address: nodeAddr, // this will be ignored TaggedAddresses: map[string]string{ "wan": nodeWANAddr, }, RaftIndex: RaftIndex{ ModifyIndex: nodeIdx, }, }, Service: &NodeService{ Address: serviceAddr, Port: servicePort, RaftIndex: RaftIndex{ ModifyIndex: serviceIdx, }, }, }, lanAddr: serviceAddr, lanIdx: serviceIdx, lanPort: servicePort, wanAddr: serviceAddr, wanIdx: serviceIdx, wanPort: servicePort, }, "service-wan-address": { input: CheckServiceNode{ Node: &Node{ Address: nodeAddr, // this will be ignored TaggedAddresses: map[string]string{ "wan": nodeWANAddr, }, RaftIndex: RaftIndex{ ModifyIndex: nodeIdx, }, }, Service: &NodeService{ Address: serviceAddr, Port: servicePort, TaggedAddresses: map[string]ServiceAddress{ "wan": { Address: serviceWANAddr, Port: serviceWANPort, }, }, RaftIndex: RaftIndex{ ModifyIndex: serviceIdx, }, }, }, lanAddr: serviceAddr, lanIdx: serviceIdx, lanPort: servicePort, wanAddr: serviceWANAddr, wanIdx: serviceIdx, wanPort: serviceWANPort, }, "service-wan-address-default-port": { input: CheckServiceNode{ Node: &Node{ Address: nodeAddr, // this will be ignored TaggedAddresses: map[string]string{ "wan": nodeWANAddr, }, RaftIndex: RaftIndex{ ModifyIndex: nodeIdx, }, }, Service: &NodeService{ Address: serviceAddr, Port: servicePort, TaggedAddresses: map[string]ServiceAddress{ "wan": { Address: serviceWANAddr, Port: 0, }, }, RaftIndex: RaftIndex{ ModifyIndex: serviceIdx, }, }, }, lanAddr: serviceAddr, lanIdx: serviceIdx, lanPort: servicePort, wanAddr: serviceWANAddr, wanIdx: serviceIdx, wanPort: servicePort, }, "service-wan-address-node-lan": { input: CheckServiceNode{ Node: &Node{ Address: nodeAddr, // this will be ignored TaggedAddresses: map[string]string{ "wan": nodeWANAddr, }, RaftIndex: RaftIndex{ ModifyIndex: nodeIdx, }, }, Service: &NodeService{ Port: servicePort, TaggedAddresses: map[string]ServiceAddress{ "wan": { Address: serviceWANAddr, Port: serviceWANPort, }, }, RaftIndex: RaftIndex{ ModifyIndex: serviceIdx, }, }, }, lanAddr: nodeAddr, lanIdx: nodeIdx, lanPort: servicePort, wanAddr: serviceWANAddr, wanIdx: serviceIdx, wanPort: serviceWANPort, }, } for name, tc := range cases { name := name tc := tc t.Run(name, func(t *testing.T) { idx, addr, port := tc.input.BestAddress(false) require.Equal(t, tc.lanAddr, addr) require.Equal(t, tc.lanPort, port) require.Equal(t, tc.lanIdx, idx) idx, addr, port = tc.input.BestAddress(true) require.Equal(t, tc.wanAddr, addr) require.Equal(t, tc.wanPort, port) require.Equal(t, tc.wanIdx, idx) }) } } func TestNodeService_JSON_Marshal(t *testing.T) { ns := &NodeService{ Service: "foo", Proxy: ConnectProxyConfig{ Config: map[string]interface{}{ "bind_addresses": map[string]interface{}{ "default": map[string]interface{}{ "Address": "0.0.0.0", "Port": "443", }, }, }, }, } buf, err := json.Marshal(ns) require.NoError(t, err) var out NodeService require.NoError(t, json.Unmarshal(buf, &out)) require.Equal(t, *ns, out) } func TestServiceNode_JSON_Marshal(t *testing.T) { sn := &ServiceNode{ Node: "foo", ServiceName: "foo", ServiceProxy: ConnectProxyConfig{ Config: map[string]interface{}{ "bind_addresses": map[string]interface{}{ "default": map[string]interface{}{ "Address": "0.0.0.0", "Port": "443", }, }, }, }, } buf, err := json.Marshal(sn) require.NoError(t, err) var out ServiceNode require.NoError(t, json.Unmarshal(buf, &out)) require.Equal(t, *sn, out) } // frankensteinStruct is an amalgamation of all of the different kinds of // fields you could have on struct defined in the agent/structs package that we // send through msgpack type frankensteinStruct struct { Child *monsterStruct ChildSlice []*monsterStruct ChildMap map[string]*monsterStruct } type monsterStruct struct { Bool bool Int int Uint8 uint8 Uint64 uint64 Float32 float32 Float64 float64 String string Hash []byte Uint32Slice []uint32 Float64Slice []float64 StringSlice []string MapInt map[string]int MapString map[string]string MapStringSlice map[string][]string // We explicitly DO NOT try to test the following types that involve // interface{} as the TestMsgpackEncodeDecode test WILL fail. // // These are tested elsewhere for the very specific scenario in question, // which usually takes a secondary trip through mapstructure during decode // which papers over some of the additional conversions necessary to finish // decoding. // MapIface map[string]interface{} // MapMapIface map[string]map[string]interface{} Dur time.Duration DurPtr *time.Duration Time time.Time TimePtr *time.Time RaftIndex } func makeFrank() *frankensteinStruct { return &frankensteinStruct{ Child: makeMonster(), ChildSlice: []*monsterStruct{ makeMonster(), makeMonster(), }, ChildMap: map[string]*monsterStruct{ "one": makeMonster(), // only put one key in here so the map order is fixed }, } } func makeMonster() *monsterStruct { var d time.Duration = 9 * time.Hour var t time.Time = time.Date(2008, 1, 2, 3, 4, 5, 0, time.UTC) return &monsterStruct{ Bool: true, Int: -8, Uint8: 5, Uint64: 9, Float32: 5.25, Float64: 99.5, String: "strval", Hash: []byte("hello"), Uint32Slice: []uint32{1, 2, 3, 4}, Float64Slice: []float64{9.2, 6.25}, StringSlice: []string{"foo", "bar"}, // // MapIface will hold an amalgam of what AuthMethods and // // CAConfigurations use in 'Config' // MapIface: map[string]interface{}{ // "Name": "inner", // "Dur": "5s", // "Bool": true, // "Float": 15.25, // "Int": int64(94), // "Nested": map[string]string{ // this doesn't survive // "foo": "bar", // }, // }, // // MapMapIface map[string]map[string]interface{} MapInt: map[string]int{ "int": 5, }, MapString: map[string]string{ "aaa": "bbb", }, MapStringSlice: map[string][]string{ "aaa": {"bbb"}, }, Dur: 5 * time.Second, DurPtr: &d, Time: t.Add(-5 * time.Hour), TimePtr: &t, RaftIndex: RaftIndex{ CreateIndex: 1, ModifyIndex: 3, }, } } func TestStructs_MsgpackEncodeDecode_Monolith(t *testing.T) { t.Run("monster", func(t *testing.T) { in := makeMonster() TestMsgpackEncodeDecode(t, in, false) }) t.Run("frankenstein", func(t *testing.T) { in := makeFrank() TestMsgpackEncodeDecode(t, in, false) }) } func TestSnapshotRequestResponse_MsgpackEncodeDecode(t *testing.T) { t.Run("request", func(t *testing.T) { in := &SnapshotRequest{ Datacenter: "foo", Token: "blah", AllowStale: true, Op: SnapshotRestore, } TestMsgpackEncodeDecode(t, in, true) }) t.Run("response", func(t *testing.T) { in := &SnapshotResponse{ Error: "blah", QueryMeta: QueryMeta{ Index: 3, LastContact: 5 * time.Second, KnownLeader: true, ConsistencyLevel: "default", ResultsFilteredByACLs: true, }, } TestMsgpackEncodeDecode(t, in, true) }) } func TestGatewayService_IsSame(t *testing.T) { gateway := NewServiceName("gateway", nil) svc := NewServiceName("web", nil) kind := ServiceKindTerminatingGateway ca := "ca.pem" cert := "client.pem" key := "tls.key" sni := "mydomain" wildcard := false g := &GatewayService{ Gateway: gateway, Service: svc, GatewayKind: kind, CAFile: ca, CertFile: cert, KeyFile: key, SNI: sni, FromWildcard: wildcard, } other := &GatewayService{ Gateway: gateway, Service: svc, GatewayKind: kind, CAFile: ca, CertFile: cert, KeyFile: key, SNI: sni, FromWildcard: wildcard, } check := func(twiddle, restore func()) { t.Helper() if !g.IsSame(other) || !other.IsSame(g) { t.Fatalf("should be the same") } twiddle() if g.IsSame(other) || other.IsSame(g) { t.Fatalf("should be different, was %#v VS %#v", g, other) } restore() if !g.IsSame(other) || !other.IsSame(g) { t.Fatalf("should be the same") } } check(func() { other.Gateway = NewServiceName("other", nil) }, func() { other.Gateway = gateway }) check(func() { other.Service = NewServiceName("other", nil) }, func() { other.Service = svc }) check(func() { other.GatewayKind = ServiceKindIngressGateway }, func() { other.GatewayKind = kind }) check(func() { other.CAFile = "/certs/cert.pem" }, func() { other.CAFile = ca }) check(func() { other.CertFile = "/certs/cert.pem" }, func() { other.CertFile = cert }) check(func() { other.KeyFile = "/certs/cert.pem" }, func() { other.KeyFile = key }) check(func() { other.SNI = "alt-domain" }, func() { other.SNI = sni }) check(func() { other.FromWildcard = true }, func() { other.FromWildcard = wildcard }) if !g.IsSame(other) { t.Fatalf("should be equal, was %#v VS %#v", g, other) } } func TestServiceList_Sort(t *testing.T) { type testcase struct { name string list []ServiceName expect []ServiceName } run := func(t *testing.T, tc testcase) { t.Run("written order", func(t *testing.T) { ServiceList(tc.list).Sort() require.Equal(t, tc.expect, tc.list) }) t.Run("random order", func(t *testing.T) { rand.Shuffle(len(tc.list), func(i, j int) { tc.list[i], tc.list[j] = tc.list[j], tc.list[i] }) ServiceList(tc.list).Sort() require.Equal(t, tc.expect, tc.list) }) } sn := func(name string) ServiceName { return NewServiceName(name, nil) } cases := []testcase{ { name: "nil", list: nil, expect: nil, }, { name: "empty", list: []ServiceName{}, expect: []ServiceName{}, }, { name: "one", list: []ServiceName{sn("foo")}, expect: []ServiceName{sn("foo")}, }, { name: "multiple", list: []ServiceName{ sn("food"), sn("zip"), sn("Bar"), sn("ba"), sn("foo"), sn("bar"), sn("Foo"), sn("Zip"), sn("foo"), sn("bar"), sn("barrier"), }, expect: []ServiceName{ sn("Bar"), sn("Foo"), sn("Zip"), sn("ba"), sn("bar"), sn("bar"), sn("barrier"), sn("foo"), sn("foo"), sn("food"), sn("zip"), }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } }