From c09693e545c5138ee7da5e102e5afa9e73c25c87 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Fri, 24 Jan 2020 10:04:58 -0500 Subject: [PATCH] Updates to Config Entries and Connect for Namespaces (#7116) --- agent/agent.go | 8 + agent/agent_endpoint.go | 2 +- agent/cache-types/catalog_service_list.go | 55 ++++ .../cache-types/catalog_service_list_test.go | 66 +++++ agent/cache-types/connect_ca_leaf.go | 8 +- agent/cache-types/connect_ca_leaf_oss.go | 7 + agent/cache-types/service_checks.go | 24 +- agent/config_endpoint.go | 20 ++ agent/connect/ca/provider_consul_test.go | 4 +- agent/connect/common_names.go | 61 +++- agent/connect/common_names_test.go | 115 ++++++++ agent/connect/testing_ca.go | 2 +- agent/consul/acl.go | 21 ++ agent/consul/acl_endpoint.go | 15 +- agent/consul/catalog_endpoint.go | 28 ++ agent/consul/config.go | 3 + agent/consul/config_endpoint.go | 127 +++++--- agent/consul/config_endpoint_test.go | 90 +++--- agent/consul/config_replication.go | 3 +- agent/consul/config_replication_test.go | 4 +- agent/consul/discovery_chain_endpoint.go | 17 +- agent/consul/discoverychain/compile.go | 92 +++--- agent/consul/discoverychain/compile_oss.go | 9 + agent/consul/discoverychain/compile_test.go | 132 ++++----- agent/consul/fsm/commands_oss.go | 6 +- agent/consul/fsm/commands_oss_test.go | 3 +- agent/consul/fsm/snapshot_oss_test.go | 8 +- agent/consul/intention_endpoint_test.go | 73 ++--- agent/consul/leader.go | 2 +- agent/consul/leader_test.go | 2 +- agent/consul/server_test.go | 2 +- agent/consul/state/catalog.go | 26 ++ agent/consul/state/config_entry.go | 233 +++++++-------- agent/consul/state/config_entry_oss.go | 73 +++++ agent/consul/state/config_entry_test.go | 273 +++++++++++------- agent/consul/state/state_store_oss_test.go | 7 + agent/discovery_chain_endpoint.go | 6 +- agent/proxycfg/manager.go | 33 +-- agent/proxycfg/manager_test.go | 41 +-- agent/proxycfg/snapshot.go | 17 +- agent/proxycfg/state.go | 114 ++++---- agent/proxycfg/state_test.go | 20 +- agent/proxycfg/testing.go | 22 +- agent/service_manager.go | 17 +- agent/service_manager_test.go | 27 +- agent/structs/config_entry.go | 86 +++++- agent/structs/config_entry_discoverychain.go | 148 +++++++--- .../config_entry_discoverychain_oss.go | 39 +++ .../config_entry_discoverychain_test.go | 12 +- agent/structs/config_entry_oss.go | 7 + agent/structs/connect_proxy_config_oss.go | 13 + agent/structs/discovery_chain.go | 4 + agent/structs/discovery_chain_oss.go | 7 + agent/structs/structs.go | 54 +++- agent/structs/structs_oss.go | 17 ++ agent/structs/testing_intention.go | 4 +- agent/xds/clusters.go | 8 +- agent/xds/clusters_test.go | 4 +- agent/xds/endpoints.go | 8 +- agent/xds/endpoints_test.go | 6 +- agent/xds/listeners.go | 4 +- agent/xds/server.go | 15 +- agent/xds/server_oss.go | 12 + agent/xds/server_test.go | 47 +-- api/config_entry.go | 2 + api/config_entry_discoverychain.go | 15 +- api/config_entry_discoverychain_test.go | 27 +- command/config/delete/config_delete.go | 1 + command/config/list/config_list.go | 1 + command/config/read/config_read.go | 1 + command/config/write/config_write.go | 1 + command/connect/envoy/bootstrap_tpl.go | 9 +- command/connect/envoy/envoy.go | 2 + .../envoy/testdata/access-log-path.golden | 5 +- .../connect/envoy/testdata/defaults.golden | 5 +- .../envoy/testdata/existing-ca-file.golden | 5 +- .../envoy/testdata/extra_-multiple.golden | 5 +- .../envoy/testdata/extra_-single.golden | 5 +- .../envoy/testdata/grpc-addr-config.golden | 5 +- .../envoy/testdata/grpc-addr-env.golden | 5 +- .../envoy/testdata/grpc-addr-flag.golden | 5 +- .../envoy/testdata/grpc-addr-unix.golden | 5 +- .../testdata/stats-config-override.golden | 5 +- .../connect/envoy/testdata/token-arg.golden | 5 +- .../connect/envoy/testdata/token-env.golden | 5 +- .../envoy/testdata/token-file-arg.golden | 5 +- .../envoy/testdata/token-file-env.golden | 5 +- .../testdata/zipkin-tracing-config.golden | 5 +- .../connect/envoy/Dockerfile-tcpdump | 7 + .../connect/envoy/docker-compose.yml | 71 ++++- test/integration/connect/envoy/helpers.bash | 67 ++++- test/integration/connect/envoy/run-tests.sh | 26 +- 92 files changed, 1893 insertions(+), 825 deletions(-) create mode 100644 agent/cache-types/catalog_service_list.go create mode 100644 agent/cache-types/catalog_service_list_test.go create mode 100644 agent/cache-types/connect_ca_leaf_oss.go create mode 100644 agent/connect/common_names_test.go create mode 100644 agent/consul/discoverychain/compile_oss.go create mode 100644 agent/consul/state/config_entry_oss.go create mode 100644 agent/consul/state/state_store_oss_test.go create mode 100644 agent/structs/config_entry_discoverychain_oss.go create mode 100644 agent/structs/config_entry_oss.go create mode 100644 agent/structs/connect_proxy_config_oss.go create mode 100644 agent/structs/discovery_chain_oss.go create mode 100644 agent/xds/server_oss.go create mode 100644 test/integration/connect/envoy/Dockerfile-tcpdump diff --git a/agent/agent.go b/agent/agent.go index 69787b2c34..37f3108eab 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -4024,6 +4024,14 @@ func (a *Agent) registerCache() { RefreshTimeout: 10 * time.Minute, }) + a.cache.RegisterType(cachetype.CatalogServiceListName, &cachetype.CatalogServiceList{ + RPC: a, + }, &cache.RegisterOptions{ + Refresh: true, + RefreshTimer: 0 * time.Second, + RefreshTimeout: 10 * time.Minute, + }) + a.cache.RegisterType(cachetype.CatalogDatacentersName, &cachetype.CatalogDatacenters{ RPC: a, }, &cache.RegisterOptions{ diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index d7405d4da4..aec1582007 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -292,7 +292,7 @@ func (s *HTTPServer) AgentService(resp http.ResponseWriter, req *http.Request) ( svcState := s.agent.State.ServiceState(sid) if svcState == nil { resp.WriteHeader(http.StatusNotFound) - fmt.Fprintf(resp, "unknown service ID: %s", id) + fmt.Fprintf(resp, "unknown service ID: %s", sid.String()) return "", nil, nil } diff --git a/agent/cache-types/catalog_service_list.go b/agent/cache-types/catalog_service_list.go new file mode 100644 index 0000000000..3bcfcd3714 --- /dev/null +++ b/agent/cache-types/catalog_service_list.go @@ -0,0 +1,55 @@ +package cachetype + +import ( + "fmt" + + "github.com/hashicorp/consul/agent/cache" + "github.com/hashicorp/consul/agent/structs" +) + +// Recommended name for registration. +const CatalogServiceListName = "catalog-services-list" + +// CatalogServiceList supports fetching service names via the catalog. +type CatalogServiceList struct { + RPC RPC +} + +func (c *CatalogServiceList) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) { + var result cache.FetchResult + + // The request should be a DCSpecificRequest. + reqReal, ok := req.(*structs.DCSpecificRequest) + if !ok { + return result, fmt.Errorf( + "Internal cache failure: request wrong type: %T", req) + } + + // Lightweight copy this object so that manipulating QueryOptions doesn't race. + dup := *reqReal + reqReal = &dup + + // Set the minimum query index to our current index so we block + reqReal.QueryOptions.MinQueryIndex = opts.MinIndex + reqReal.QueryOptions.MaxQueryTime = opts.Timeout + + // Always allow stale - there's no point in hitting leader if the request is + // going to be served from cache and end up arbitrarily stale anyway. This + // allows cached service-discover to automatically read scale across all + // servers too. + reqReal.AllowStale = true + + // Fetch + var reply structs.IndexedServiceList + if err := c.RPC.RPC("Catalog.ServiceList", reqReal, &reply); err != nil { + return result, err + } + + result.Value = &reply + result.Index = reply.QueryMeta.Index + return result, nil +} + +func (c *CatalogServiceList) SupportsBlocking() bool { + return true +} diff --git a/agent/cache-types/catalog_service_list_test.go b/agent/cache-types/catalog_service_list_test.go new file mode 100644 index 0000000000..f7ac28e73b --- /dev/null +++ b/agent/cache-types/catalog_service_list_test.go @@ -0,0 +1,66 @@ +package cachetype + +import ( + "testing" + "time" + + "github.com/hashicorp/consul/agent/cache" + "github.com/hashicorp/consul/agent/structs" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestCatalogServiceList(t *testing.T) { + rpc := TestRPC(t) + typ := &CatalogServiceList{RPC: rpc} + + // Expect the proper RPC call. This also sets the expected value + // since that is return-by-pointer in the arguments. + var resp *structs.IndexedServiceList + rpc.On("RPC", "Catalog.ServiceList", mock.Anything, mock.Anything).Return(nil). + Run(func(args mock.Arguments) { + req := args.Get(1).(*structs.DCSpecificRequest) + require.Equal(t, uint64(24), req.QueryOptions.MinQueryIndex) + require.Equal(t, 1*time.Second, req.QueryOptions.MaxQueryTime) + require.True(t, req.AllowStale) + + reply := args.Get(2).(*structs.IndexedServiceList) + reply.Services = structs.ServiceList{ + structs.ServiceInfo{ + Name: "foo", + }, + structs.ServiceInfo{ + Name: "bar", + }, + } + reply.QueryMeta.Index = 48 + resp = reply + }) + + // Fetch + resultA, err := typ.Fetch(cache.FetchOptions{ + MinIndex: 24, + Timeout: 1 * time.Second, + }, &structs.DCSpecificRequest{ + Datacenter: "dc1", + }) + require.NoError(t, err) + require.Equal(t, cache.FetchResult{ + Value: resp, + Index: 48, + }, resultA) + + rpc.AssertExpectations(t) +} + +func TestCatalogServiceList_badReqType(t *testing.T) { + rpc := TestRPC(t) + typ := &CatalogServiceList{RPC: rpc} + + // Fetch + _, err := typ.Fetch(cache.FetchOptions{}, cache.TestRequest( + t, cache.RequestInfo{Key: "foo", MinIndex: 64})) + require.Error(t, err) + require.Contains(t, err.Error(), "wrong type") + rpc.AssertExpectations(t) +} diff --git a/agent/cache-types/connect_ca_leaf.go b/agent/cache-types/connect_ca_leaf.go index e5fb8c891e..fee5c3221e 100644 --- a/agent/cache-types/connect_ca_leaf.go +++ b/agent/cache-types/connect_ca_leaf.go @@ -515,17 +515,17 @@ func (c *ConnectCALeaf) generateNewLeaf(req *ConnectCALeafRequest, id = &connect.SpiffeIDService{ Host: roots.TrustDomain, Datacenter: req.Datacenter, - Namespace: "default", + Namespace: req.TargetNamespace(), Service: req.Service, } - commonName = connect.ServiceCN(req.Service, roots.TrustDomain) + commonName = connect.ServiceCN(req.Service, req.TargetNamespace(), roots.TrustDomain) } else if req.Agent != "" { id = &connect.SpiffeIDAgent{ Host: roots.TrustDomain, Datacenter: req.Datacenter, Agent: req.Agent, } - commonName = connect.ServiceCN(req.Agent, roots.TrustDomain) + commonName = connect.AgentCN(req.Agent, roots.TrustDomain) dnsNames = append([]string{"localhost"}, req.DNSSAN...) ipAddresses = append([]net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::")}, req.IPSAN...) } else { @@ -645,6 +645,8 @@ type ConnectCALeafRequest struct { IPSAN []net.IP MinQueryIndex uint64 MaxQueryTime time.Duration + + structs.EnterpriseMeta } func (r *ConnectCALeafRequest) Key() string { diff --git a/agent/cache-types/connect_ca_leaf_oss.go b/agent/cache-types/connect_ca_leaf_oss.go new file mode 100644 index 0000000000..2045e85eeb --- /dev/null +++ b/agent/cache-types/connect_ca_leaf_oss.go @@ -0,0 +1,7 @@ +// +build !consulent + +package cachetype + +func (req *ConnectCALeafRequest) TargetNamespace() string { + return "default" +} diff --git a/agent/cache-types/service_checks.go b/agent/cache-types/service_checks.go index d54b597a31..6f7af528ff 100644 --- a/agent/cache-types/service_checks.go +++ b/agent/cache-types/service_checks.go @@ -2,6 +2,7 @@ package cachetype import ( "fmt" + "strconv" "time" "github.com/hashicorp/consul/agent/cache" @@ -55,8 +56,8 @@ func (c *ServiceHTTPChecks) Fetch(opts cache.FetchOptions, req cache.Request) (c hash, resp, err := c.Agent.LocalBlockingQuery(true, lastHash, reqReal.MaxQueryTime, func(ws memdb.WatchSet) (string, interface{}, error) { - // TODO (namespaces) update with the real ent meta once thats plumbed through - svcState := c.Agent.LocalState().ServiceState(structs.NewServiceID(reqReal.ServiceID, nil)) + sid := structs.NewServiceID(reqReal.ServiceID, &reqReal.EnterpriseMeta) + svcState := c.Agent.LocalState().ServiceState(sid) if svcState == nil { return "", result, fmt.Errorf("Internal cache failure: service '%s' not in agent state", reqReal.ServiceID) } @@ -64,8 +65,7 @@ func (c *ServiceHTTPChecks) Fetch(opts cache.FetchOptions, req cache.Request) (c // WatchCh will receive updates on service (de)registrations and check (de)registrations ws.Add(svcState.WatchCh) - // TODO (namespaces) update with a real entMeta - reply := c.Agent.ServiceHTTPBasedChecks(structs.NewServiceID(reqReal.ServiceID, nil)) + reply := c.Agent.ServiceHTTPBasedChecks(sid) hash, err := hashChecks(reply) if err != nil { @@ -103,16 +103,28 @@ type ServiceHTTPChecksRequest struct { ServiceID string MinQueryIndex uint64 MaxQueryTime time.Duration + structs.EnterpriseMeta } func (s *ServiceHTTPChecksRequest) CacheInfo() cache.RequestInfo { - return cache.RequestInfo{ + info := cache.RequestInfo{ Token: "", - Key: ServiceHTTPChecksName + ":" + s.ServiceID, Datacenter: "", MinIndex: s.MinQueryIndex, Timeout: s.MaxQueryTime, } + + v, err := hashstructure.Hash([]interface{}{ + s.ServiceID, + s.EnterpriseMeta, + }, nil) + if err == nil { + // If there is an error, we don't set the key. A blank key forces + // no cache for this request. + info.Key = strconv.FormatUint(v, 10) + } + + return info } func hashChecks(checks []structs.CheckType) (string, error) { diff --git a/agent/config_endpoint.go b/agent/config_endpoint.go index 925d5bedfe..9ba68e6978 100644 --- a/agent/config_endpoint.go +++ b/agent/config_endpoint.go @@ -32,6 +32,14 @@ func (s *HTTPServer) configGet(resp http.ResponseWriter, req *http.Request) (int } pathArgs := strings.SplitN(strings.TrimPrefix(req.URL.Path, "/v1/config/"), "/", 2) + if len(pathArgs) == 2 { + if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil { + return nil, err + } + } else if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil { + return nil, err + } + switch len(pathArgs) { case 2: // Both kind/name provided. @@ -85,6 +93,11 @@ func (s *HTTPServer) configDelete(resp http.ResponseWriter, req *http.Request) ( return nil, nil } args.Entry = entry + // Parse enterprise meta. + meta := args.Entry.GetEnterpriseMeta() + if err := s.parseEntMetaNoWildcard(req, meta); err != nil { + return nil, err + } var reply struct{} if err := s.agent.RPC("ConfigEntry.Delete", &args, &reply); err != nil { @@ -113,6 +126,13 @@ func (s *HTTPServer) ConfigApply(resp http.ResponseWriter, req *http.Request) (i return nil, BadRequestError{Reason: fmt.Sprintf("Request decoding failed: %v", err)} } + // Parse enterprise meta. + var meta structs.EnterpriseMeta + if err := s.parseEntMetaNoWildcard(req, &meta); err != nil { + return nil, err + } + args.Entry.GetEnterpriseMeta().Merge(&meta) + // Check for cas value if casStr := req.URL.Query().Get("cas"); casStr != "" { casVal, err := strconv.ParseUint(casStr, 10, 64) diff --git a/agent/connect/ca/provider_consul_test.go b/agent/connect/ca/provider_consul_test.go index dbb9fc22e4..854e6f90be 100644 --- a/agent/connect/ca/provider_consul_test.go +++ b/agent/connect/ca/provider_consul_test.go @@ -174,7 +174,7 @@ func TestConsulCAProvider_SignLeaf(t *testing.T) { parsed, err := connect.ParseCert(cert) require.NoError(err) require.Equal(spiffeService.URI(), parsed.URIs[0]) - require.Equal(connect.ServiceCN("foo", connect.TestClusterID), parsed.Subject.CommonName) + require.Equal(connect.ServiceCN("foo", "default", connect.TestClusterID), parsed.Subject.CommonName) require.Equal(uint64(2), parsed.SerialNumber.Uint64()) subjectKeyID, err := connect.KeyId(csr.PublicKey) require.NoError(err) @@ -203,7 +203,7 @@ func TestConsulCAProvider_SignLeaf(t *testing.T) { parsed, err := connect.ParseCert(cert) require.NoError(err) require.Equal(spiffeService.URI(), parsed.URIs[0]) - require.Equal(connect.ServiceCN("bar", connect.TestClusterID), parsed.Subject.CommonName) + require.Equal(connect.ServiceCN("bar", "default", connect.TestClusterID), parsed.Subject.CommonName) require.Equal(parsed.SerialNumber.Uint64(), uint64(2)) requireNotEncoded(t, parsed.SubjectKeyId) requireNotEncoded(t, parsed.AuthorityKeyId) diff --git a/agent/connect/common_names.go b/agent/connect/common_names.go index 5c942dbe7b..a4821a7aee 100644 --- a/agent/connect/common_names.go +++ b/agent/connect/common_names.go @@ -11,6 +11,56 @@ import ( var invalidDNSNameChars = regexp.MustCompile(`[^a-z0-9]`) +const ( + // 64 = max length of a certificate common name + // 21 = 7 bytes for ".consul", 9 bytes for . and 5 bytes for ".svc." + // This ends up being 43 bytes + maxServiceAndNamespaceLen = 64 - 21 + minServiceNameLen = 15 + minNamespaceNameLen = 15 +) + +// trucateServiceAndNamespace will take a service name and namespace name and truncate +// them appropriately so that they would fit within the space alloted for them in the +// Common Name field of the x509 certificate. That field is capped at 64 characters +// in length and there is other data that must be a part of the name too. This function +// takes all of that into account. +func truncateServiceAndNamespace(serviceName, namespace string) (string, string) { + svcLen := len(serviceName) + nsLen := len(namespace) + totalLen := svcLen + nsLen + + // quick exit when the entirety of both can fit + if totalLen <= maxServiceAndNamespaceLen { + return serviceName, namespace + } + + toRemove := totalLen - maxServiceAndNamespaceLen + // now we must figure out how to truncate each one, we need to ensure we don't remove all of either one. + if svcLen <= minServiceNameLen { + // only remove bytes from the namespace + return serviceName, truncateTo(namespace, nsLen-toRemove) + } else if nsLen <= minNamespaceNameLen { + // only remove bytes from the service name + return truncateTo(serviceName, svcLen-toRemove), namespace + } else { + // we can remove an "equal" amount from each. If the number of bytes to remove is odd we give it to the namespace + svcTruncate := svcLen - (toRemove / 2) - (toRemove % 2) + nsTruncate := nsLen - (toRemove / 2) + + // checks to ensure we don't reduce one side too much when they are not roughly balanced in length. + if svcTruncate <= minServiceNameLen { + svcTruncate = minServiceNameLen + nsTruncate = maxServiceAndNamespaceLen - minServiceNameLen + } else if nsTruncate <= minNamespaceNameLen { + svcTruncate = maxServiceAndNamespaceLen - minNamespaceNameLen + nsTruncate = minNamespaceNameLen + } + + return truncateTo(serviceName, svcTruncate), truncateTo(namespace, nsTruncate) + } +} + // ServiceCN returns the common name for a service's certificate. We can't use // SPIFFE URIs because some CAs require valid FQDN format. We can't use SNI // values because they are often too long than the 64 bytes allowed by @@ -31,11 +81,12 @@ var invalidDNSNameChars = regexp.MustCompile(`[^a-z0-9]`) // total at 64. // // trust domain is truncated to keep the whole name short -func ServiceCN(serviceName, trustDomain string) string { +func ServiceCN(serviceName, namespace, trustDomain string) string { svc := invalidDNSNameChars.ReplaceAllString(strings.ToLower(serviceName), "") - // 20 = 7 bytes for ".consul", 8 bytes for trust domain, 5 bytes for ".svc." - return fmt.Sprintf("%s.svc.%s.consul", - truncateTo(svc, 64-20), truncateTo(trustDomain, 8)) + + svc, namespace = truncateServiceAndNamespace(svc, namespace) + return fmt.Sprintf("%s.svc.%s.%s.consul", + svc, namespace, truncateTo(trustDomain, 8)) } // AgentCN returns the common name for an agent certificate. See ServiceCN for @@ -122,7 +173,7 @@ func CNForCertURI(uri CertURI) (string, error) { // didn't include the Common Name in the CSR. switch id := uri.(type) { case *SpiffeIDService: - return ServiceCN(id.Service, id.Host), nil + return ServiceCN(id.Service, id.Namespace, id.Host), nil case *SpiffeIDAgent: return AgentCN(id.Agent, id.Host), nil case *SpiffeIDSigning: diff --git a/agent/connect/common_names_test.go b/agent/connect/common_names_test.go new file mode 100644 index 0000000000..15b612a5b4 --- /dev/null +++ b/agent/connect/common_names_test.go @@ -0,0 +1,115 @@ +package connect + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestServiceAndNamespaceTruncation(t *testing.T) { + type tcase struct { + service string + namespace string + // if left as "" then its expected to not be truncated + expectedService string + // if left as "" then its expected to not be truncated + expectedNamespace string + } + + cases := map[string]tcase{ + "short-no-truncation": tcase{ + service: "foo", + namespace: "bar", + }, + "long-service-no-truncation": tcase{ + // -3 because thats the length of the namespace + service: strings.Repeat("a", maxServiceAndNamespaceLen-3), + namespace: "bar", + }, + "long-namespace-no-truncation": tcase{ + service: "foo", + // -3 because thats the length of the service name + namespace: strings.Repeat("b", maxServiceAndNamespaceLen-3), + }, + "truncate-service-only": tcase{ + // this should force the service name to be truncated + service: strings.Repeat("a", maxServiceAndNamespaceLen-minNamespaceNameLen+5), + expectedService: strings.Repeat("a", maxServiceAndNamespaceLen-minNamespaceNameLen), + // this is the maximum length that will never be truncated for a namespace + namespace: strings.Repeat("b", minNamespaceNameLen), + }, + "truncate-namespace-only": tcase{ + // this is the maximum length that will never be truncated for a service name + service: strings.Repeat("a", minServiceNameLen), + // this should force the namespace name to be truncated + namespace: strings.Repeat("b", maxServiceAndNamespaceLen-minServiceNameLen+5), + expectedNamespace: strings.Repeat("b", maxServiceAndNamespaceLen-minServiceNameLen), + }, + "truncate-both-even": tcase{ + // this test would need to be update if the maxServiceAndNamespaceLen variable is updated + // I could put some more complex logic into here to prevent that but it would be mostly + // duplicating the logic in the function itself and thus not really be testing anything + // + // The original lengths of 50 / 51 were chose when maxServiceAndNamespaceLen would be 43 + // Therefore a total of 58 characters must be removed. This was chose so that the value + // could be evenly split between the two strings. + service: strings.Repeat("a", 50), + expectedService: strings.Repeat("a", 21), + namespace: strings.Repeat("b", 51), + expectedNamespace: strings.Repeat("b", 22), + }, + "truncate-both-odd": tcase{ + // this test would need to be update if the maxServiceAndNamespaceLen variable is updated + // I could put some more complex logic into here to prevent that but it would be mostly + // duplicating the logic in the function itself and thus not really be testing anything + // + // The original lengths of 50 / 57 were chose when maxServiceAndNamespaceLen would be 43 + // Therefore a total of 57 characters must be removed. This was chose so that the value + // could not be evenly removed from both so the namespace should be truncated to a length + // 1 character more than the service. + service: strings.Repeat("a", 50), + expectedService: strings.Repeat("a", 21), + namespace: strings.Repeat("b", 50), + expectedNamespace: strings.Repeat("b", 22), + }, + "truncate-both-min-svc": tcase{ + service: strings.Repeat("a", minServiceNameLen+1), + expectedService: strings.Repeat("a", minServiceNameLen), + namespace: strings.Repeat("b", maxServiceAndNamespaceLen), + expectedNamespace: strings.Repeat("b", maxServiceAndNamespaceLen-minServiceNameLen), + }, + "truncate-both-min-ns": tcase{ + service: strings.Repeat("a", maxServiceAndNamespaceLen), + expectedService: strings.Repeat("a", maxServiceAndNamespaceLen-minNamespaceNameLen), + namespace: strings.Repeat("b", minNamespaceNameLen+1), + expectedNamespace: strings.Repeat("b", minNamespaceNameLen), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + actualSvc, actualNamespace := truncateServiceAndNamespace(tc.service, tc.namespace) + + expectedLen := len(tc.service) + len(tc.namespace) + if tc.expectedService != "" || tc.expectedNamespace != "" { + expectedLen = maxServiceAndNamespaceLen + } + + actualLen := len(actualSvc) + len(actualNamespace) + + require.Equal(t, expectedLen, actualLen, "Combined length of %d (svc: %d, ns: %d) does not match expected value of %d", actualLen, len(actualSvc), len(actualNamespace), expectedLen) + + if tc.expectedService != "" { + require.Equal(t, tc.expectedService, actualSvc) + } else { + require.Equal(t, tc.service, actualSvc) + } + if tc.expectedNamespace != "" { + require.Equal(t, tc.expectedNamespace, actualNamespace) + } else { + require.Equal(t, tc.namespace, actualNamespace) + } + }) + } +} diff --git a/agent/connect/testing_ca.go b/agent/connect/testing_ca.go index 36d9909e7d..c9ec35c9c2 100644 --- a/agent/connect/testing_ca.go +++ b/agent/connect/testing_ca.go @@ -211,7 +211,7 @@ func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string, // Cert template for generation template := x509.Certificate{ SerialNumber: sn, - Subject: pkix.Name{CommonName: ServiceCN(service, TestClusterID)}, + Subject: pkix.Name{CommonName: ServiceCN(service, "default", TestClusterID)}, URIs: []*url.URL{spiffeId.URI()}, SignatureAlgorithm: SigAlgoForKeyType(rootKeyType), BasicConstraintsValid: true, diff --git a/agent/consul/acl.go b/agent/consul/acl.go index 4fb550b910..1ce52dfa66 100644 --- a/agent/consul/acl.go +++ b/agent/consul/acl.go @@ -1647,6 +1647,25 @@ func (f *aclFilter) filterAuthMethods(methods *structs.ACLAuthMethods) { *methods = ret } +func (f *aclFilter) filterServiceList(services *structs.ServiceList) { + ret := make(structs.ServiceList, 0, len(*services)) + for _, svc := range *services { + var authzContext acl.AuthorizerContext + + svc.FillAuthzContext(&authzContext) + + if f.authorizer.ServiceRead(svc.Name, &authzContext) != acl.Allow { + sid := structs.NewServiceID(svc.Name, &svc.EnterpriseMeta) + f.logger.Printf("[DEBUG] consul: dropping service %q from result due to ACLs", sid.String()) + continue + } + + ret = append(ret, svc) + } + + *services = ret +} + func (r *ACLResolver) filterACLWithAuthorizer(authorizer acl.Authorizer, subj interface{}) error { if authorizer == nil { return nil @@ -1726,6 +1745,8 @@ func (r *ACLResolver) filterACLWithAuthorizer(authorizer acl.Authorizer, subj in case **structs.ACLAuthMethod: filt.filterAuthMethod(v) + case *structs.IndexedServiceList: + filt.filterServiceList(&v.Services) default: panic(fmt.Errorf("Unhandled type passed to ACL filter: %T %#v", subj, subj)) } diff --git a/agent/consul/acl_endpoint.go b/agent/consul/acl_endpoint.go index 051253603d..aed0262d15 100644 --- a/agent/consul/acl_endpoint.go +++ b/agent/consul/acl_endpoint.go @@ -821,17 +821,26 @@ func (a *ACL) TokenList(args *structs.ACLTokenListRequest, reply *structs.ACLTok } var authzContext acl.AuthorizerContext - authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext) + var requestMeta structs.EnterpriseMeta + authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &requestMeta, &authzContext) if err != nil { return err - } else if authz == nil || authz.ACLRead(&authzContext) != acl.Allow { + } + // merge the token default meta into the requests meta + args.EnterpriseMeta.Merge(&requestMeta) + args.EnterpriseMeta.FillAuthzContext(&authzContext) + if authz == nil || authz.ACLRead(&authzContext) != acl.Allow { return acl.ErrPermissionDenied } var methodMeta *structs.EnterpriseMeta if args.AuthMethod != "" { methodMeta = args.ACLAuthMethodEnterpriseMeta.ToEnterpriseMeta() - methodMeta.Merge(&args.EnterpriseMeta) + // attempt to merge in the overall meta, wildcards will not be merged + methodMeta.MergeNoWildcard(&args.EnterpriseMeta) + // in the event that the meta above didn't merge due to being a wildcard + // we ensure that proper token based meta inference occurs + methodMeta.Merge(&requestMeta) } return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta, diff --git a/agent/consul/catalog_endpoint.go b/agent/consul/catalog_endpoint.go index 1442def6cd..6c069d29a4 100644 --- a/agent/consul/catalog_endpoint.go +++ b/agent/consul/catalog_endpoint.go @@ -320,6 +320,34 @@ func (c *Catalog) ListServices(args *structs.DCSpecificRequest, reply *structs.I }) } +func (c *Catalog) ServiceList(args *structs.DCSpecificRequest, reply *structs.IndexedServiceList) error { + if done, err := c.srv.forward("Catalog.ServiceList", args, args, reply); done { + return err + } + + authz, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil) + if err != nil { + return err + } + + if err := c.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil { + return err + } + + return c.srv.blockingQuery( + &args.QueryOptions, + &reply.QueryMeta, + func(ws memdb.WatchSet, state *state.Store) error { + index, services, err := state.ServiceList(ws, &args.EnterpriseMeta) + if err != nil { + return err + } + + reply.Index, reply.Services = index, services + return c.srv.filterACLWithAuthorizer(authz, reply) + }) +} + // ServiceNodes returns all the nodes registered as part of a service func (c *Catalog) ServiceNodes(args *structs.ServiceSpecificRequest, reply *structs.IndexedServiceNodes) error { if done, err := c.srv.forward("Catalog.ServiceNodes", args, args, reply); done { diff --git a/agent/consul/config.go b/agent/consul/config.go index 89c4bc5b85..0ac3e40965 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -349,6 +349,9 @@ type Config struct { // a Consul server is now up and known about. ServerUp func() + // Shutdown callback is used to trigger a full Consul shutdown + Shutdown func() + // UserEventHandler callback can be used to handle incoming // user events. This function should not block. UserEventHandler func(serf.UserEvent) diff --git a/agent/consul/config_endpoint.go b/agent/consul/config_endpoint.go index 5ec9332504..e8faca01e5 100644 --- a/agent/consul/config_endpoint.go +++ b/agent/consul/config_endpoint.go @@ -19,6 +19,10 @@ type ConfigEntry struct { // Apply does an upsert of the given config entry. func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error { + if err := c.srv.validateEnterpriseRequest(args.Entry.GetEnterpriseMeta(), true); err != nil { + return err + } + // Ensure that all config entry writes go to the primary datacenter. These will then // be replicated to all the other datacenters. args.Datacenter = c.srv.config.PrimaryDatacenter @@ -28,6 +32,12 @@ func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error } defer metrics.MeasureSince([]string{"config_entry", "apply"}, time.Now()) + entMeta := args.Entry.GetEnterpriseMeta() + authz, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, entMeta, nil) + if err != nil { + return err + } + // Normalize and validate the incoming config entry. if err := args.Entry.Normalize(); err != nil { return err @@ -36,12 +46,7 @@ func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error return err } - // Fetch the ACL token, if any. - rule, err := c.srv.ResolveToken(args.Token) - if err != nil { - return err - } - if rule != nil && !args.Entry.CanWrite(rule) { + if authz != nil && !args.Entry.CanWrite(authz) { return acl.ErrPermissionDenied } @@ -64,13 +69,16 @@ func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error // Get returns a single config entry by Kind/Name. func (c *ConfigEntry) Get(args *structs.ConfigEntryQuery, reply *structs.ConfigEntryResponse) error { + if err := c.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil { + return err + } + if done, err := c.srv.forward("ConfigEntry.Get", args, args, reply); done { return err } defer metrics.MeasureSince([]string{"config_entry", "get"}, time.Now()) - // Fetch the ACL token, if any. - rule, err := c.srv.ResolveToken(args.Token) + authz, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil) if err != nil { return err } @@ -80,7 +88,7 @@ func (c *ConfigEntry) Get(args *structs.ConfigEntryQuery, reply *structs.ConfigE if err != nil { return err } - if rule != nil && !lookupEntry.CanRead(rule) { + if authz != nil && !lookupEntry.CanRead(authz) { return acl.ErrPermissionDenied } @@ -88,7 +96,7 @@ func (c *ConfigEntry) Get(args *structs.ConfigEntryQuery, reply *structs.ConfigE &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { - index, entry, err := state.ConfigEntry(ws, args.Kind, args.Name) + index, entry, err := state.ConfigEntry(ws, args.Kind, args.Name, &args.EnterpriseMeta) if err != nil { return err } @@ -106,13 +114,16 @@ func (c *ConfigEntry) Get(args *structs.ConfigEntryQuery, reply *structs.ConfigE // List returns all the config entries of the given kind. If Kind is blank, // all existing config entries will be returned. func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.IndexedConfigEntries) error { + if err := c.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil { + return err + } + if done, err := c.srv.forward("ConfigEntry.List", args, args, reply); done { return err } defer metrics.MeasureSince([]string{"config_entry", "list"}, time.Now()) - // Fetch the ACL token, if any. - rule, err := c.srv.ResolveToken(args.Token) + authz, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil) if err != nil { return err } @@ -125,7 +136,7 @@ func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.Indexe &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { - index, entries, err := state.ConfigEntriesByKind(ws, args.Kind) + index, entries, err := state.ConfigEntriesByKind(ws, args.Kind, &args.EnterpriseMeta) if err != nil { return err } @@ -133,7 +144,7 @@ func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.Indexe // Filter the entries returned by ACL permissions. filteredEntries := make([]structs.ConfigEntry, 0, len(entries)) for _, entry := range entries { - if rule != nil && !entry.CanRead(rule) { + if authz != nil && !entry.CanRead(authz) { continue } filteredEntries = append(filteredEntries, entry) @@ -148,13 +159,16 @@ func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.Indexe // ListAll returns all the known configuration entries func (c *ConfigEntry) ListAll(args *structs.DCSpecificRequest, reply *structs.IndexedGenericConfigEntries) error { + if err := c.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil { + return err + } + if done, err := c.srv.forward("ConfigEntry.ListAll", args, args, reply); done { return err } defer metrics.MeasureSince([]string{"config_entry", "listAll"}, time.Now()) - // Fetch the ACL token, if any. - rule, err := c.srv.ResolveToken(args.Token) + authz, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil) if err != nil { return err } @@ -163,7 +177,7 @@ func (c *ConfigEntry) ListAll(args *structs.DCSpecificRequest, reply *structs.In &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { - index, entries, err := state.ConfigEntries(ws) + index, entries, err := state.ConfigEntries(ws, &args.EnterpriseMeta) if err != nil { return err } @@ -171,7 +185,7 @@ func (c *ConfigEntry) ListAll(args *structs.DCSpecificRequest, reply *structs.In // Filter the entries returned by ACL permissions. filteredEntries := make([]structs.ConfigEntry, 0, len(entries)) for _, entry := range entries { - if rule != nil && !entry.CanRead(rule) { + if authz != nil && !entry.CanRead(authz) { continue } filteredEntries = append(filteredEntries, entry) @@ -185,6 +199,10 @@ func (c *ConfigEntry) ListAll(args *structs.DCSpecificRequest, reply *structs.In // Delete deletes a config entry. func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *struct{}) error { + if err := c.srv.validateEnterpriseRequest(args.Entry.GetEnterpriseMeta(), true); err != nil { + return err + } + // Ensure that all config entry writes go to the primary datacenter. These will then // be replicated to all the other datacenters. args.Datacenter = c.srv.config.PrimaryDatacenter @@ -194,17 +212,17 @@ func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *struct{}) } defer metrics.MeasureSince([]string{"config_entry", "delete"}, time.Now()) + authz, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, args.Entry.GetEnterpriseMeta(), nil) + if err != nil { + return err + } + // Normalize the incoming entry. if err := args.Entry.Normalize(); err != nil { return err } - // Fetch the ACL token, if any. - rule, err := c.srv.ResolveToken(args.Token) - if err != nil { - return err - } - if rule != nil && !args.Entry.CanWrite(rule) { + if authz != nil && !args.Entry.CanWrite(authz) { return acl.ErrPermissionDenied } @@ -221,18 +239,21 @@ func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *struct{}) // ResolveServiceConfig func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, reply *structs.ServiceConfigResponse) error { + if err := c.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil { + return err + } + if done, err := c.srv.forward("ConfigEntry.ResolveServiceConfig", args, args, reply); done { return err } defer metrics.MeasureSince([]string{"config_entry", "resolve_service_config"}, time.Now()) - // Fetch the ACL token, if any. - rule, err := c.srv.ResolveToken(args.Token) + var authzContext acl.AuthorizerContext + authz, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext) if err != nil { return err } - // TODO (namespaces) use actual ent authz context - if rule != nil && rule.ServiceRead(args.Name, nil) != acl.Allow { + if authz != nil && authz.ServiceRead(args.Name, &authzContext) != acl.Allow { return acl.ErrPermissionDenied } @@ -246,7 +267,7 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r // Pass the WatchSet to both the service and proxy config lookups. If either is updated // during the blocking query, this function will be rerun and these state store lookups // will both be current. - index, serviceEntry, err := state.ConfigEntry(ws, structs.ServiceDefaults, args.Name) + index, serviceEntry, err := state.ConfigEntry(ws, structs.ServiceDefaults, args.Name, &args.EnterpriseMeta) if err != nil { return err } @@ -259,7 +280,9 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r } } - _, proxyEntry, err := state.ConfigEntry(ws, structs.ProxyDefaults, structs.ProxyConfigGlobal) + // Use the default enterprise meta to look up the global proxy defaults. In the future we may allow per-namespace proxy-defaults + // but not yet. + _, proxyEntry, err := state.ConfigEntry(ws, structs.ProxyDefaults, structs.ProxyConfigGlobal, structs.DefaultEnterpriseMeta()) if err != nil { return err } @@ -305,9 +328,24 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r proxyConfGlobalProtocol = proxyConf.Config["protocol"] } - // Apply the upstream protocols to the upstream configs - for _, upstream := range args.Upstreams { - _, upstreamEntry, err := state.ConfigEntry(ws, structs.ServiceDefaults, upstream) + // map the legacy request structure using only service names + // to the new ServiceID type. + upstreamIDs := args.UpstreamIDs + legacyUpstreams := false + + if len(upstreamIDs) == 0 { + legacyUpstreams = true + + upstreamIDs = make([]structs.ServiceID, 0) + for _, upstream := range args.Upstreams { + upstreamIDs = append(upstreamIDs, structs.NewServiceID(upstream, &args.EnterpriseMeta)) + } + } + + usConfigs := make(map[structs.ServiceID]map[string]interface{}) + + for _, upstream := range upstreamIDs { + _, upstreamEntry, err := state.ConfigEntry(ws, structs.ServiceDefaults, upstream.ID, &upstream.EnterpriseMeta) if err != nil { return err } @@ -336,11 +374,30 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r continue } + usConfigs[upstream] = map[string]interface{}{ + "protocol": protocol, + } + } + + // don't allocate the slices just to not fill them + if len(usConfigs) == 0 { + return nil + } + + if legacyUpstreams { if reply.UpstreamConfigs == nil { reply.UpstreamConfigs = make(map[string]map[string]interface{}) } - reply.UpstreamConfigs[upstream] = map[string]interface{}{ - "protocol": protocol, + for us, conf := range usConfigs { + reply.UpstreamConfigs[us.ID] = conf + } + } else { + if reply.UpstreamIDConfigs == nil { + reply.UpstreamIDConfigs = make(structs.UpstreamConfigs, 0, len(usConfigs)) + } + + for us, conf := range usConfigs { + reply.UpstreamIDConfigs = append(reply.UpstreamIDConfigs, structs.UpstreamConfig{Upstream: us, Config: conf}) } } diff --git a/agent/consul/config_endpoint_test.go b/agent/consul/config_endpoint_test.go index 32ff43a701..94722d10a4 100644 --- a/agent/consul/config_endpoint_test.go +++ b/agent/consul/config_endpoint_test.go @@ -53,7 +53,7 @@ func TestConfigEntry_Apply(t *testing.T) { // the previous RPC should not return until the primary has been updated but will return // before the secondary has the data. state := s1.fsm.State() - _, entry, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, entry, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(t, err) serviceConf, ok := entry.(*structs.ServiceConfigEntry) @@ -64,7 +64,7 @@ func TestConfigEntry_Apply(t *testing.T) { retry.Run(t, func(r *retry.R) { // wait for replication to happen state := s2.fsm.State() - _, entry, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, entry, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(r, err) require.NotNil(r, entry) // this test is not testing that the config entries that are replicated are correct as thats done elsewhere. @@ -91,7 +91,7 @@ func TestConfigEntry_Apply(t *testing.T) { require.True(t, out) state = s1.fsm.State() - _, entry, err = state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, entry, err = state.ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(t, err) serviceConf, ok = entry.(*structs.ServiceConfigEntry) @@ -124,7 +124,7 @@ func TestConfigEntry_ProxyDefaultsMeshGateway(t *testing.T) { require.True(t, out) state := s1.fsm.State() - _, entry, err := state.ConfigEntry(nil, structs.ProxyDefaults, "global") + _, entry, err := state.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) proxyConf, ok := entry.(*structs.ProxyConfigEntry) @@ -192,7 +192,7 @@ operator = "write" require.NoError(err) state := s1.fsm.State() - _, entry, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, entry, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(err) serviceConf, ok := entry.(*structs.ServiceConfigEntry) @@ -237,7 +237,7 @@ func TestConfigEntry_Get(t *testing.T) { Name: "foo", } state := s1.fsm.State() - require.NoError(state.EnsureConfigEntry(1, entry)) + require.NoError(state.EnsureConfigEntry(1, entry, nil)) args := structs.ConfigEntryQuery{ Kind: structs.ServiceDefaults, @@ -296,11 +296,11 @@ operator = "read" require.NoError(state.EnsureConfigEntry(1, &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", - })) + }, nil)) // This should fail since we don't have write perms for the "db" service. args := structs.ConfigEntryQuery{ @@ -350,8 +350,8 @@ func TestConfigEntry_List(t *testing.T) { }, }, } - require.NoError(state.EnsureConfigEntry(1, expected.Entries[0])) - require.NoError(state.EnsureConfigEntry(2, expected.Entries[1])) + require.NoError(state.EnsureConfigEntry(1, expected.Entries[0], nil)) + require.NoError(state.EnsureConfigEntry(2, expected.Entries[1], nil)) args := structs.ConfigEntryQuery{ Kind: structs.ServiceDefaults, @@ -394,9 +394,9 @@ func TestConfigEntry_ListAll(t *testing.T) { }, }, } - require.NoError(state.EnsureConfigEntry(1, expected.Entries[0])) - require.NoError(state.EnsureConfigEntry(2, expected.Entries[1])) - require.NoError(state.EnsureConfigEntry(3, expected.Entries[2])) + require.NoError(state.EnsureConfigEntry(1, expected.Entries[0], nil)) + require.NoError(state.EnsureConfigEntry(2, expected.Entries[1], nil)) + require.NoError(state.EnsureConfigEntry(3, expected.Entries[2], nil)) args := structs.DCSpecificRequest{ Datacenter: "dc1", @@ -451,15 +451,15 @@ operator = "read" require.NoError(state.EnsureConfigEntry(1, &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(3, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", - })) + }, nil)) // This should filter out the "db" service since we don't have permissions for it. args := structs.ConfigEntryQuery{ @@ -532,15 +532,15 @@ operator = "read" require.NoError(state.EnsureConfigEntry(1, &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(3, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", - })) + }, nil)) // This should filter out the "db" service since we don't have permissions for it. args := structs.ConfigEntryQuery{ @@ -600,10 +600,10 @@ func TestConfigEntry_Delete(t *testing.T) { Name: "foo", } state := s1.fsm.State() - require.NoError(t, state.EnsureConfigEntry(1, entry)) + require.NoError(t, state.EnsureConfigEntry(1, entry, nil)) // Verify it's there. - _, existing, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, existing, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(t, err) serviceConf, ok := existing.(*structs.ServiceConfigEntry) @@ -613,7 +613,7 @@ func TestConfigEntry_Delete(t *testing.T) { retry.Run(t, func(r *retry.R) { // wait for it to be replicated into the secondary dc - _, existing, err := s2.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, existing, err := s2.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(r, err) require.NotNil(r, existing) }) @@ -627,13 +627,13 @@ func TestConfigEntry_Delete(t *testing.T) { require.NoError(t, msgpackrpc.CallWithCodec(codec2, "ConfigEntry.Delete", &args, &out)) // Verify the entry was deleted. - _, existing, err = s1.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, existing, err = s1.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(t, err) require.Nil(t, existing) // verify it gets deleted from the secondary too retry.Run(t, func(r *retry.R) { - _, existing, err := s2.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, existing, err := s2.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(r, err) require.Nil(r, existing) }) @@ -682,11 +682,11 @@ operator = "write" require.NoError(state.EnsureConfigEntry(1, &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", - })) + }, nil)) // This should fail since we don't have write perms for the "db" service. args := structs.ConfigEntryRequest{ @@ -709,7 +709,7 @@ operator = "write" require.NoError(msgpackrpc.CallWithCodec(codec, "ConfigEntry.Delete", &args, &out)) // Verify the entry was deleted. - _, existing, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, existing, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(err) require.Nil(existing) @@ -729,7 +729,7 @@ operator = "write" args.WriteRequest.Token = id require.NoError(msgpackrpc.CallWithCodec(codec, "ConfigEntry.Delete", &args, &out)) - _, existing, err = state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, existing, err = state.ConfigEntry(nil, structs.ServiceDefaults, "foo", nil) require.NoError(err) require.Nil(existing) } @@ -753,17 +753,17 @@ func TestConfigEntry_ResolveServiceConfig(t *testing.T) { Config: map[string]interface{}{ "foo": 1, }, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", Protocol: "http", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "bar", Protocol: "grpc", - })) + }, nil)) args := structs.ServiceConfigRequest{ Name: "foo", @@ -788,7 +788,7 @@ func TestConfigEntry_ResolveServiceConfig(t *testing.T) { } require.Equal(expected, out) - _, entry, err := s1.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal) + _, entry, err := s1.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal, nil) require.NoError(err) require.NotNil(entry) proxyConf, ok := entry.(*structs.ProxyConfigEntry) @@ -819,17 +819,17 @@ func TestConfigEntry_ResolveServiceConfig_Blocking(t *testing.T) { Config: map[string]interface{}{ "global": 1, }, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", Protocol: "grpc", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(3, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "bar", Protocol: "http", - })) + }, nil)) var index uint64 @@ -863,6 +863,7 @@ func TestConfigEntry_ResolveServiceConfig_Blocking(t *testing.T) { require.NoError(state.DeleteConfigEntry(index+1, structs.ServiceDefaults, "foo", + nil, )) }() @@ -927,6 +928,7 @@ func TestConfigEntry_ResolveServiceConfig_Blocking(t *testing.T) { require.NoError(state.DeleteConfigEntry(index+1, structs.ProxyDefaults, structs.ProxyConfigGlobal, + nil, )) }() @@ -979,24 +981,24 @@ func TestConfigEntry_ResolveServiceConfig_UpstreamProxyDefaultsProtocol(t *testi Config: map[string]interface{}{ "protocol": "http", }, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "bar", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "other", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "alreadyprotocol", Protocol: "grpc", - })) + }, nil)) args := structs.ServiceConfigRequest{ Name: "foo", @@ -1100,15 +1102,15 @@ operator = "write" require.NoError(state.EnsureConfigEntry(1, &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, - })) + }, nil)) require.NoError(state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", - })) + }, nil)) require.NoError(state.EnsureConfigEntry(3, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", - })) + }, nil)) // This should fail since we don't have write perms for the "db" service. args := structs.ServiceConfigRequest{ @@ -1163,7 +1165,7 @@ func TestConfigEntry_ProxyDefaultsExposeConfig(t *testing.T) { require.True(t, out) state := s1.fsm.State() - _, entry, err := state.ConfigEntry(nil, structs.ProxyDefaults, "global") + _, entry, err := state.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) proxyConf, ok := entry.(*structs.ProxyConfigEntry) diff --git a/agent/consul/config_replication.go b/agent/consul/config_replication.go index 64ebd1e66e..4ae12689fe 100644 --- a/agent/consul/config_replication.go +++ b/agent/consul/config_replication.go @@ -105,6 +105,7 @@ func (s *Server) fetchConfigEntries(lastRemoteIndex uint64) (*structs.IndexedGen MinQueryIndex: lastRemoteIndex, Token: s.tokens.ReplicationToken(), }, + EnterpriseMeta: *s.replicationEnterpriseMeta(), } var response structs.IndexedGenericConfigEntries @@ -138,7 +139,7 @@ func (s *Server) replicateConfig(ctx context.Context, lastRemoteIndex uint64) (u // replication process is. defer metrics.MeasureSince([]string{"leader", "replication", "config", "apply"}, time.Now()) - _, local, err := s.fsm.State().ConfigEntries(nil) + _, local, err := s.fsm.State().ConfigEntries(nil, s.replicationEnterpriseMeta()) if err != nil { return 0, false, fmt.Errorf("failed to retrieve local config entries: %v", err) } diff --git a/agent/consul/config_replication_test.go b/agent/consul/config_replication_test.go index 06fcf30b59..3780b1ea27 100644 --- a/agent/consul/config_replication_test.go +++ b/agent/consul/config_replication_test.go @@ -74,9 +74,9 @@ func TestReplication_ConfigEntries(t *testing.T) { entries = append(entries, arg.Entry) checkSame := func(t *retry.R) error { - _, remote, err := s1.fsm.State().ConfigEntries(nil) + _, remote, err := s1.fsm.State().ConfigEntries(nil, structs.ReplicationEnterpriseMeta()) require.NoError(t, err) - _, local, err := s2.fsm.State().ConfigEntries(nil) + _, local, err := s2.fsm.State().ConfigEntries(nil, structs.ReplicationEnterpriseMeta()) require.NoError(t, err) require.Len(t, local, len(remote)) diff --git a/agent/consul/discovery_chain_endpoint.go b/agent/consul/discovery_chain_endpoint.go index 97877b6c3e..a4a21ec053 100644 --- a/agent/consul/discovery_chain_endpoint.go +++ b/agent/consul/discovery_chain_endpoint.go @@ -30,12 +30,13 @@ func (c *DiscoveryChain) Get(args *structs.DiscoveryChainRequest, reply *structs defer metrics.MeasureSince([]string{"discovery_chain", "get"}, time.Now()) // Fetch the ACL token, if any. - rule, err := c.srv.ResolveToken(args.Token) + entMeta := args.GetEnterpriseMeta() + var authzContext acl.AuthorizerContext + rule, err := c.srv.ResolveTokenAndDefaultMeta(args.Token, entMeta, &authzContext) if err != nil { return err } - // TODO (namespaces) use actual ent authz context - if rule != nil && rule.ServiceRead(args.Name, nil) != acl.Allow { + if rule != nil && rule.ServiceRead(args.Name, &authzContext) != acl.Allow { return acl.ErrPermissionDenied } @@ -48,17 +49,11 @@ func (c *DiscoveryChain) Get(args *structs.DiscoveryChainRequest, reply *structs evalDC = c.srv.config.Datacenter } - evalNS := args.EvaluateInNamespace - if evalNS == "" { - // TODO(namespaces) pull from something else? - evalNS = "default" - } - return c.srv.blockingQuery( &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { - index, entries, err := state.ReadDiscoveryChainConfigEntries(ws, args.Name) + index, entries, err := state.ReadDiscoveryChainConfigEntries(ws, args.Name, entMeta) if err != nil { return err } @@ -82,7 +77,7 @@ func (c *DiscoveryChain) Get(args *structs.DiscoveryChainRequest, reply *structs // Then we compile it into something useful. chain, err := discoverychain.Compile(discoverychain.CompileRequest{ ServiceName: args.Name, - EvaluateInNamespace: evalNS, + EvaluateInNamespace: entMeta.NamespaceOrDefault(), EvaluateInDatacenter: evalDC, EvaluateInTrustDomain: currentTrustDomain, UseInDatacenter: c.srv.config.Datacenter, diff --git a/agent/consul/discoverychain/compile.go b/agent/consul/discoverychain/compile.go index 730684e1e5..9bb5d2905d 100644 --- a/agent/consul/discoverychain/compile.go +++ b/agent/consul/discoverychain/compile.go @@ -92,7 +92,7 @@ func Compile(req CompileRequest) (*structs.CompiledDiscoveryChain, error) { overrideConnectTimeout: req.OverrideConnectTimeout, entries: entries, - resolvers: make(map[string]*structs.ServiceResolverConfigEntry), + resolvers: make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry), splitterNodes: make(map[string]*structs.DiscoveryGraphNode), resolveNodes: make(map[string]*structs.DiscoveryGraphNode), @@ -134,7 +134,7 @@ type compiler struct { // resolvers is initially seeded by copying the provided entries.Resolvers // map and default resolvers are added as they are needed. - resolvers map[string]*structs.ServiceResolverConfigEntry + resolvers map[structs.ServiceID]*structs.ServiceResolverConfigEntry // cached nodes splitterNodes map[string]*structs.DiscoveryGraphNode @@ -180,6 +180,13 @@ type customizationMarkers struct { ConnectTimeout bool } +// serviceIDString deviates from the standard formatting you would get with +// the String() method on the type itself. It is this way to be more +// consistent with other string ids within the discovery chain. +func serviceIDString(sid structs.ServiceID) string { + return fmt.Sprintf("%s.%s", sid.ID, sid.NamespaceOrDefault()) +} + func (m *customizationMarkers) IsZero() bool { return !m.MeshGateway && !m.Protocol && !m.ConnectTimeout } @@ -201,19 +208,19 @@ func (c *compiler) recordNode(node *structs.DiscoveryGraphNode) { c.nodes[node.MapKey()] = node } -func (c *compiler) recordServiceProtocol(serviceName string) error { - if serviceDefault := c.entries.GetService(serviceName); serviceDefault != nil { - return c.recordProtocol(serviceName, serviceDefault.Protocol) +func (c *compiler) recordServiceProtocol(sid structs.ServiceID) error { + if serviceDefault := c.entries.GetService(sid); serviceDefault != nil { + return c.recordProtocol(sid, serviceDefault.Protocol) } if c.entries.GlobalProxy != nil { var cfg proxyConfig // Ignore errors and fallback on defaults if it does happen. _ = mapstructure.WeakDecode(c.entries.GlobalProxy.Config, &cfg) if cfg.Protocol != "" { - return c.recordProtocol(serviceName, cfg.Protocol) + return c.recordProtocol(sid, cfg.Protocol) } } - return c.recordProtocol(serviceName, "") + return c.recordProtocol(sid, "") } // proxyConfig is a snippet from agent/xds/config.go:ProxyConfig @@ -221,7 +228,7 @@ type proxyConfig struct { Protocol string `mapstructure:"protocol"` } -func (c *compiler) recordProtocol(fromService, protocol string) error { +func (c *compiler) recordProtocol(fromService structs.ServiceID, protocol string) error { if protocol == "" { protocol = "tcp" } else { @@ -234,7 +241,7 @@ func (c *compiler) recordProtocol(fromService, protocol string) error { return &structs.ConfigEntryGraphError{ Message: fmt.Sprintf( "discovery chain %q uses inconsistent protocols; service %q has %q which is not %q", - c.serviceName, fromService, protocol, c.protocol, + c.serviceName, fromService.String(), protocol, c.protocol, ), } } @@ -494,15 +501,17 @@ func (c *compiler) assembleChain() error { return fmt.Errorf("assembleChain should only be called once") } + sid := structs.NewServiceID(c.serviceName, c.GetEnterpriseMeta()) + // Check for short circuit path. if len(c.resolvers) == 0 && c.entries.IsChainEmpty() { // Materialize defaults and cache. - c.resolvers[c.serviceName] = newDefaultServiceResolver(c.serviceName) + c.resolvers[sid] = newDefaultServiceResolver(sid) } // The only router we consult is the one for the service name at the top of // the chain. - router := c.entries.GetRouter(c.serviceName) + router := c.entries.GetRouter(sid) if router != nil && c.disableAdvancedRoutingFeatures { router = nil c.customizedBy.Protocol = true @@ -520,13 +529,15 @@ func (c *compiler) assembleChain() error { return nil } + routerID := structs.NewServiceID(router.Name, router.GetEnterpriseMeta()) + routeNode := &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: router.Name, + Name: serviceIDString(routerID), Routes: make([]*structs.DiscoveryRoute, 0, len(router.Routes)+1), } c.usesAdvancedRoutingFeatures = true - if err := c.recordServiceProtocol(router.Name); err != nil { + if err := c.recordServiceProtocol(routerID); err != nil { return err } @@ -543,19 +554,20 @@ func (c *compiler) assembleChain() error { dest := route.Destination svc := defaultIfEmpty(dest.Service, c.serviceName) + destNamespace := defaultIfEmpty(dest.Namespace, c.evaluateInNamespace) // Check to see if the destination is eligible for splitting. var ( node *structs.DiscoveryGraphNode err error ) - if dest.ServiceSubset == "" && dest.Namespace == "" { + if dest.ServiceSubset == "" { node, err = c.getSplitterOrResolverNode( - c.newTarget(svc, dest.ServiceSubset, dest.Namespace, ""), + c.newTarget(svc, "", destNamespace, ""), ) } else { node, err = c.getResolverNode( - c.newTarget(svc, dest.ServiceSubset, dest.Namespace, ""), + c.newTarget(svc, dest.ServiceSubset, destNamespace, ""), false, ) } @@ -573,7 +585,7 @@ func (c *compiler) assembleChain() error { } defaultRoute := &structs.DiscoveryRoute{ - Definition: newDefaultServiceRoute(c.serviceName), + Definition: newDefaultServiceRoute(c.serviceName, c.evaluateInNamespace), NextNode: defaultDestinationNode.MapKey(), } routeNode.Routes = append(routeNode.Routes, defaultRoute) @@ -584,7 +596,7 @@ func (c *compiler) assembleChain() error { return nil } -func newDefaultServiceRoute(serviceName string) *structs.ServiceRoute { +func newDefaultServiceRoute(serviceName string, namespace string) *structs.ServiceRoute { return &structs.ServiceRoute{ Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ @@ -592,7 +604,8 @@ func newDefaultServiceRoute(serviceName string) *structs.ServiceRoute { }, }, Destination: &structs.ServiceRouteDestination{ - Service: serviceName, + Service: serviceName, + Namespace: namespace, }, } } @@ -652,7 +665,7 @@ func (c *compiler) rewriteTarget(t *structs.DiscoveryTarget, service, serviceSub } func (c *compiler) getSplitterOrResolverNode(target *structs.DiscoveryTarget) (*structs.DiscoveryGraphNode, error) { - nextNode, err := c.getSplitterNode(target.Service) + nextNode, err := c.getSplitterNode(target.ServiceID()) if err != nil { return nil, err } else if nextNode != nil { @@ -661,14 +674,15 @@ func (c *compiler) getSplitterOrResolverNode(target *structs.DiscoveryTarget) (* return c.getResolverNode(target, false) } -func (c *compiler) getSplitterNode(name string) (*structs.DiscoveryGraphNode, error) { +func (c *compiler) getSplitterNode(sid structs.ServiceID) (*structs.DiscoveryGraphNode, error) { + name := serviceIDString(sid) // Do we already have the node? if prev, ok := c.splitterNodes[name]; ok { return prev, nil } // Fetch the config entry. - splitter := c.entries.GetSplitter(name) + splitter := c.entries.GetSplitter(sid) if splitter != nil && c.disableAdvancedRoutingFeatures { splitter = nil c.customizedBy.Protocol = true @@ -694,10 +708,15 @@ func (c *compiler) getSplitterNode(name string) (*structs.DiscoveryGraphNode, er } splitNode.Splits = append(splitNode.Splits, compiledSplit) - svc := defaultIfEmpty(split.Service, name) + svc := defaultIfEmpty(split.Service, sid.ID) + splitID := structs.ServiceID{ + ID: svc, + EnterpriseMeta: *split.GetEnterpriseMeta(&sid.EnterpriseMeta), + } + // Check to see if the split is eligible for additional splitting. - if svc != name && split.ServiceSubset == "" && split.Namespace == "" { - nextNode, err := c.getSplitterNode(svc) + if !splitID.Matches(&sid) && split.ServiceSubset == "" { + nextNode, err := c.getSplitterNode(splitID) if err != nil { return nil, err } else if nextNode != nil { @@ -708,7 +727,7 @@ func (c *compiler) getSplitterNode(name string) (*structs.DiscoveryGraphNode, er } node, err := c.getResolverNode( - c.newTarget(svc, split.ServiceSubset, split.Namespace, ""), + c.newTarget(splitID.ID, split.ServiceSubset, splitID.NamespaceOrDefault(), ""), false, ) if err != nil { @@ -738,16 +757,18 @@ RESOLVE_AGAIN: return prev, nil } - if err := c.recordServiceProtocol(target.Service); err != nil { + targetID := target.ServiceID() + + if err := c.recordServiceProtocol(targetID); err != nil { return nil, err } // Fetch the config entry. - resolver, ok := c.resolvers[target.Service] + resolver, ok := c.resolvers[targetID] if !ok { // Materialize defaults and cache. - resolver = newDefaultServiceResolver(target.Service) - c.resolvers[target.Service] = resolver + resolver = newDefaultServiceResolver(targetID) + c.resolvers[targetID] = resolver } if _, ok := redirectHistory[target.ID]; ok { @@ -829,7 +850,7 @@ RESOLVE_AGAIN: target.Subset = resolver.Subsets[target.ServiceSubset] - if serviceDefault := c.entries.GetService(target.Service); serviceDefault != nil && serviceDefault.ExternalSNI != "" { + if serviceDefault := c.entries.GetService(targetID); serviceDefault != nil && serviceDefault.ExternalSNI != "" { // Override the default SNI value. target.SNI = serviceDefault.ExternalSNI target.External = true @@ -873,7 +894,7 @@ RESOLVE_AGAIN: } else { // Default mesh gateway settings - if serviceDefault := c.entries.GetService(target.Service); serviceDefault != nil { + if serviceDefault := c.entries.GetService(targetID); serviceDefault != nil { target.MeshGateway = serviceDefault.MeshGateway } @@ -967,10 +988,11 @@ RESOLVE_AGAIN: return node, nil } -func newDefaultServiceResolver(serviceName string) *structs.ServiceResolverConfigEntry { +func newDefaultServiceResolver(sid structs.ServiceID) *structs.ServiceResolverConfigEntry { return &structs.ServiceResolverConfigEntry{ - Kind: structs.ServiceResolver, - Name: serviceName, + Kind: structs.ServiceResolver, + Name: sid.ID, + EnterpriseMeta: sid.EnterpriseMeta, } } diff --git a/agent/consul/discoverychain/compile_oss.go b/agent/consul/discoverychain/compile_oss.go new file mode 100644 index 0000000000..b0ae3460e1 --- /dev/null +++ b/agent/consul/discoverychain/compile_oss.go @@ -0,0 +1,9 @@ +// +build !consulent + +package discoverychain + +import "github.com/hashicorp/consul/agent/structs" + +func (c *compiler) GetEnterpriseMeta() *structs.EnterpriseMeta { + return structs.DefaultEnterpriseMeta() +} diff --git a/agent/consul/discoverychain/compile_test.go b/agent/consul/discoverychain/compile_test.go index df38b1ec4a..00fec07531 100644 --- a/agent/consul/discoverychain/compile_test.go +++ b/agent/consul/discoverychain/compile_test.go @@ -154,14 +154,14 @@ func testcase_JustRouterWithDefaults() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main", + StartNode: "router:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main": &structs.DiscoveryGraphNode{ + "router:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main", + Name: "main.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main"), + Definition: newDefaultServiceRoute("main", "default"), NextNode: "resolver:main.default.dc1", }, }, @@ -204,14 +204,14 @@ func testcase_RouterWithDefaults_NoSplit_WithResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main", + StartNode: "router:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main": &structs.DiscoveryGraphNode{ + "router:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main", + Name: "main.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main"), + Definition: newDefaultServiceRoute("main", "default"), NextNode: "resolver:main.default.dc1", }, }, @@ -255,21 +255,21 @@ func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main", + StartNode: "router:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main": &structs.DiscoveryGraphNode{ + "router:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main", + Name: "main.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main"), - NextNode: "splitter:main", + Definition: newDefaultServiceRoute("main", "default"), + NextNode: "splitter:main.default", }, }, }, - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 100, @@ -317,21 +317,21 @@ func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestC expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main", + StartNode: "router:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main": &structs.DiscoveryGraphNode{ + "router:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main", + Name: "main.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main"), - NextNode: "splitter:main", + Definition: newDefaultServiceRoute("main", "default"), + NextNode: "splitter:main.default", }, }, }, - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 100, @@ -386,21 +386,21 @@ func testcase_RouterWithDefaults_WithNoopSplit_WithResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main", + StartNode: "router:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main": &structs.DiscoveryGraphNode{ + "router:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main", + Name: "main.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main"), - NextNode: "splitter:main", + Definition: newDefaultServiceRoute("main", "default"), + NextNode: "splitter:main.default", }, }, }, - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 100, @@ -463,22 +463,22 @@ func testcase_RouteBypassesSplit() compileTestCase { }, ) - router := entries.GetRouter("main") + router := entries.GetRouter(structs.NewServiceID("main", nil)) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main", + StartNode: "router:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main": &structs.DiscoveryGraphNode{ + "router:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main", + Name: "main.default", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], NextNode: "resolver:bypass.other.default.dc1", }, { - Definition: newDefaultServiceRoute("main"), + Definition: newDefaultServiceRoute("main", "default"), NextNode: "resolver:main.default.dc1", }, }, @@ -530,11 +530,11 @@ func testcase_NoopSplit_DefaultResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main", + StartNode: "splitter:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 100, @@ -583,11 +583,11 @@ func testcase_NoopSplit_WithResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main", + StartNode: "splitter:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 100, @@ -643,11 +643,11 @@ func testcase_SubsetSplit() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main", + StartNode: "splitter:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 60, @@ -712,11 +712,11 @@ func testcase_ServiceSplit() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main", + StartNode: "splitter:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 60, @@ -801,11 +801,11 @@ func testcase_SplitBypassesSplit() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main", + StartNode: "splitter:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 100, @@ -1278,11 +1278,11 @@ func testcase_NoopSplit_WithDefaultSubset() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main", + StartNode: "splitter:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 100, @@ -1586,11 +1586,11 @@ func testcase_MultiDatacenterCanary() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main", + StartNode: "splitter:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main": &structs.DiscoveryGraphNode{ + "splitter:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main", + Name: "main.default", Splits: []*structs.DiscoverySplit{ { Weight: 60, @@ -1712,16 +1712,16 @@ func testcase_AllBellsAndWhistles() compileTestCase { ) var ( - router = entries.GetRouter("main") + router = entries.GetRouter(structs.NewServiceID("main", nil)) ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main", + StartNode: "router:main.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main": &structs.DiscoveryGraphNode{ + "router:main.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main", + Name: "main.default", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], @@ -1729,17 +1729,17 @@ func testcase_AllBellsAndWhistles() compileTestCase { }, { Definition: &router.Routes[1], - NextNode: "splitter:svc-split", + NextNode: "splitter:svc-split.default", }, { - Definition: newDefaultServiceRoute("main"), + Definition: newDefaultServiceRoute("main", "default"), NextNode: "resolver:default-subset.main.default.dc1", }, }, }, - "splitter:svc-split": &structs.DiscoveryGraphNode{ + "splitter:svc-split.default": &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "svc-split", + Name: "svc-split.default", Splits: []*structs.DiscoverySplit{ { Weight: 60, @@ -2191,9 +2191,9 @@ func setServiceProtocol(entries *structs.DiscoveryChainConfigEntries, name, prot func newEntries() *structs.DiscoveryChainConfigEntries { return &structs.DiscoveryChainConfigEntries{ - Routers: make(map[string]*structs.ServiceRouterConfigEntry), - Splitters: make(map[string]*structs.ServiceSplitterConfigEntry), - Resolvers: make(map[string]*structs.ServiceResolverConfigEntry), + Routers: make(map[structs.ServiceID]*structs.ServiceRouterConfigEntry), + Splitters: make(map[structs.ServiceID]*structs.ServiceSplitterConfigEntry), + Resolvers: make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry), } } diff --git a/agent/consul/fsm/commands_oss.go b/agent/consul/fsm/commands_oss.go index 4ccfc50839..7a6e3fea85 100644 --- a/agent/consul/fsm/commands_oss.go +++ b/agent/consul/fsm/commands_oss.go @@ -456,7 +456,7 @@ func (c *FSM) applyConfigEntryOperation(buf []byte, index uint64) interface{} { case structs.ConfigEntryUpsertCAS: defer metrics.MeasureSinceWithLabels([]string{"fsm", "config_entry", req.Entry.GetKind()}, time.Now(), []metrics.Label{{Name: "op", Value: "upsert"}}) - updated, err := c.state.EnsureConfigEntryCAS(index, req.Entry.GetRaftIndex().ModifyIndex, req.Entry) + updated, err := c.state.EnsureConfigEntryCAS(index, req.Entry.GetRaftIndex().ModifyIndex, req.Entry, req.Entry.GetEnterpriseMeta()) if err != nil { return err } @@ -464,14 +464,14 @@ func (c *FSM) applyConfigEntryOperation(buf []byte, index uint64) interface{} { case structs.ConfigEntryUpsert: defer metrics.MeasureSinceWithLabels([]string{"fsm", "config_entry", req.Entry.GetKind()}, time.Now(), []metrics.Label{{Name: "op", Value: "upsert"}}) - if err := c.state.EnsureConfigEntry(index, req.Entry); err != nil { + if err := c.state.EnsureConfigEntry(index, req.Entry, req.Entry.GetEnterpriseMeta()); err != nil { return err } return true case structs.ConfigEntryDelete: defer metrics.MeasureSinceWithLabels([]string{"fsm", "config_entry", req.Entry.GetKind()}, time.Now(), []metrics.Label{{Name: "op", Value: "delete"}}) - return c.state.DeleteConfigEntry(index, req.Entry.GetKind(), req.Entry.GetName()) + return c.state.DeleteConfigEntry(index, req.Entry.GetKind(), req.Entry.GetName(), req.Entry.GetEnterpriseMeta()) default: return fmt.Errorf("invalid config entry operation type: %v", req.Op) } diff --git a/agent/consul/fsm/commands_oss_test.go b/agent/consul/fsm/commands_oss_test.go index b48e7f71f4..c9511c7320 100644 --- a/agent/consul/fsm/commands_oss_test.go +++ b/agent/consul/fsm/commands_oss_test.go @@ -1422,6 +1422,7 @@ func TestFSM_ConfigEntry(t *testing.T) { Config: map[string]interface{}{ "foo": "bar", }, + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), } // Create a new request. @@ -1441,7 +1442,7 @@ func TestFSM_ConfigEntry(t *testing.T) { // Verify it's in the state store. { - _, config, err := fsm.state.ConfigEntry(nil, structs.ProxyDefaults, "global") + _, config, err := fsm.state.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) entry.RaftIndex.CreateIndex = 1 entry.RaftIndex.ModifyIndex = 1 diff --git a/agent/consul/fsm/snapshot_oss_test.go b/agent/consul/fsm/snapshot_oss_test.go index 0cb16ad843..6593204c0b 100644 --- a/agent/consul/fsm/snapshot_oss_test.go +++ b/agent/consul/fsm/snapshot_oss_test.go @@ -240,8 +240,8 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) { Kind: structs.ProxyDefaults, Name: "global", } - require.NoError(fsm.state.EnsureConfigEntry(18, serviceConfig)) - require.NoError(fsm.state.EnsureConfigEntry(19, proxyConfig)) + require.NoError(fsm.state.EnsureConfigEntry(18, serviceConfig, structs.DefaultEnterpriseMeta())) + require.NoError(fsm.state.EnsureConfigEntry(19, proxyConfig, structs.DefaultEnterpriseMeta())) chunkState := &raftchunking.State{ ChunkMap: make(raftchunking.ChunkMap), @@ -479,11 +479,11 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) { assert.Equal(caConfig, caConf) // Verify config entries are restored - _, serviceConfEntry, err := fsm2.state.ConfigEntry(nil, structs.ServiceDefaults, "foo") + _, serviceConfEntry, err := fsm2.state.ConfigEntry(nil, structs.ServiceDefaults, "foo", structs.DefaultEnterpriseMeta()) require.NoError(err) assert.Equal(serviceConfig, serviceConfEntry) - _, proxyConfEntry, err := fsm2.state.ConfigEntry(nil, structs.ProxyDefaults, "global") + _, proxyConfEntry, err := fsm2.state.ConfigEntry(nil, structs.ProxyDefaults, "global", structs.DefaultEnterpriseMeta()) require.NoError(err) assert.Equal(proxyConfig, proxyConfEntry) diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index 03caca012e..2a61135456 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -1105,6 +1105,8 @@ service "foo" { Op: structs.IntentionOpCreate, Intention: structs.TestIntention(t), } + ixn.Intention.SourceNS = "default" + ixn.Intention.DestinationNS = "default" ixn.Intention.DestinationName = name ixn.WriteRequest.Token = "root" @@ -1230,13 +1232,7 @@ func TestIntentionMatch_good(t *testing.T) { func TestIntentionMatch_acl(t *testing.T) { t.Parallel() - assert := assert.New(t) - dir1, s1 := testServerWithConfig(t, func(c *Config) { - c.ACLDatacenter = "dc1" - c.ACLsEnabled = true - c.ACLMasterToken = "root" - c.ACLDefaultPolicy = "deny" - }) + dir1, s1 := testACLServerWithConfig(t, nil, false) defer os.RemoveAll(dir1) defer s1.Shutdown() codec := rpcClient(t, s1) @@ -1244,37 +1240,15 @@ func TestIntentionMatch_acl(t *testing.T) { testrpc.WaitForLeader(t, s1.RPC, "dc1") - // Create an ACL with service write permissions. This will grant - // intentions read. - var token string - { - var rules = ` -service "bar" { - policy = "write" -}` - - req := structs.ACLRequest{ - Datacenter: "dc1", - Op: structs.ACLSet, - ACL: structs.ACL{ - Name: "User token", - Type: structs.ACLTokenTypeClient, - Rules: rules, - }, - WriteRequest: structs.WriteRequest{Token: "root"}, - } - assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token)) - } + token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service "bar" { policy = "write" }`) + require.NoError(t, err) // Create some records { - insert := [][]string{ - {"foo", "*"}, - {"foo", "bar"}, - {"foo", "baz"}, // shouldn't match - {"bar", "bar"}, // shouldn't match - {"bar", "*"}, // shouldn't match - {"*", "*"}, + insert := []string{ + "*", + "bar", + "baz", } for _, v := range insert { @@ -1283,13 +1257,12 @@ service "bar" { Op: structs.IntentionOpCreate, Intention: structs.TestIntention(t), } - ixn.Intention.DestinationNS = v[0] - ixn.Intention.DestinationName = v[1] - ixn.WriteRequest.Token = "root" + ixn.Intention.DestinationName = v + ixn.WriteRequest.Token = TestDefaultMasterToken // Create var reply string - assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) + require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) } } @@ -1301,7 +1274,7 @@ service "bar" { Type: structs.IntentionMatchDestination, Entries: []structs.IntentionMatchEntry{ { - Namespace: "foo", + Namespace: "default", Name: "bar", }, }, @@ -1309,8 +1282,8 @@ service "bar" { } var resp structs.IndexedIntentionMatches err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp) - assert.True(acl.IsErrPermissionDenied(err)) - assert.Len(resp.Matches, 0) + require.True(t, acl.IsErrPermissionDenied(err)) + require.Len(t, resp.Matches, 0) } // Test with proper token @@ -1321,24 +1294,24 @@ service "bar" { Type: structs.IntentionMatchDestination, Entries: []structs.IntentionMatchEntry{ { - Namespace: "foo", + Namespace: "default", Name: "bar", }, }, }, - QueryOptions: structs.QueryOptions{Token: token}, + QueryOptions: structs.QueryOptions{Token: token.SecretID}, } var resp structs.IndexedIntentionMatches - assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)) - assert.Len(resp.Matches, 1) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)) + require.Len(t, resp.Matches, 1) - expected := [][]string{{"foo", "bar"}, {"foo", "*"}, {"*", "*"}} - var actual [][]string + expected := []string{"bar", "*"} + var actual []string for _, ixn := range resp.Matches[0] { - actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName}) + actual = append(actual, ixn.DestinationName) } - assert.Equal(expected, actual) + require.ElementsMatch(t, expected, actual) } } diff --git a/agent/consul/leader.go b/agent/consul/leader.go index 61a7c2d73f..b9eca607c8 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -957,7 +957,7 @@ func (s *Server) bootstrapConfigEntries(entries []structs.ConfigEntry) error { state := s.fsm.State() for _, entry := range entries { // avoid a round trip through Raft if we know the CAS is going to fail - _, existing, err := state.ConfigEntry(nil, entry.GetKind(), entry.GetName()) + _, existing, err := state.ConfigEntry(nil, entry.GetKind(), entry.GetName(), entry.GetEnterpriseMeta()) if err != nil { return fmt.Errorf("Failed to determine whether the configuration for %q / %q already exists: %v", entry.GetKind(), entry.GetName(), err) } diff --git a/agent/consul/leader_test.go b/agent/consul/leader_test.go index 4f5bcc3248..45b02a2406 100644 --- a/agent/consul/leader_test.go +++ b/agent/consul/leader_test.go @@ -1153,7 +1153,7 @@ func TestLeader_ConfigEntryBootstrap(t *testing.T) { testrpc.WaitForTestAgent(t, s1.RPC, "dc1") retry.Run(t, func(t *retry.R) { - _, entry, err := s1.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal) + _, entry, err := s1.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal, structs.DefaultEnterpriseMeta()) require.NoError(t, err) require.NotNil(t, entry) global, ok := entry.(*structs.ProxyConfigEntry) diff --git a/agent/consul/server_test.go b/agent/consul/server_test.go index ea3607eef1..0373864de2 100644 --- a/agent/consul/server_test.go +++ b/agent/consul/server_test.go @@ -1135,7 +1135,7 @@ func TestServer_Reload(t *testing.T) { s.ReloadConfig(s.config) - _, entry, err := s.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal) + _, entry, err := s.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal, structs.DefaultEnterpriseMeta()) require.NoError(t, err) require.NotNil(t, entry) global, ok := entry.(*structs.ProxyConfigEntry) diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 5d066221a7..bd19f9c6e2 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -748,6 +748,32 @@ func (s *Store) Services(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (ui return idx, results, nil } +func (s *Store) ServiceList(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.ServiceList, error) { + tx := s.db.Txn(false) + defer tx.Abort() + + idx := s.catalogServicesMaxIndex(tx, entMeta) + + services, err := s.catalogServiceList(tx, entMeta, true) + if err != nil { + return 0, nil, fmt.Errorf("failed querying services: %s", err) + } + ws.Add(services.WatchCh()) + + unique := make(map[structs.ServiceID]struct{}) + for service := services.Next(); service != nil; service = services.Next() { + svc := service.(*structs.ServiceNode) + unique[svc.CompoundServiceName()] = struct{}{} + } + + results := make(structs.ServiceList, 0, len(unique)) + for sid, _ := range unique { + results = append(results, structs.ServiceInfo{Name: sid.ID, EnterpriseMeta: sid.EnterpriseMeta}) + } + + return idx, results, nil +} + // ServicesByNodeMeta returns all services, filtered by the given node metadata. func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, entMeta *structs.EnterpriseMeta) (uint64, structs.Services, error) { tx := s.db.Txn(false) diff --git a/agent/consul/state/config_entry.go b/agent/consul/state/config_entry.go index 83d99660b6..e2cdb8e33c 100644 --- a/agent/consul/state/config_entry.go +++ b/agent/consul/state/config_entry.go @@ -12,48 +12,6 @@ const ( configTableName = "config-entries" ) -// configTableSchema returns a new table schema used to store global -// config entries. -func configTableSchema() *memdb.TableSchema { - return &memdb.TableSchema{ - Name: configTableName, - Indexes: map[string]*memdb.IndexSchema{ - "id": &memdb.IndexSchema{ - Name: "id", - AllowMissing: false, - Unique: true, - Indexer: &memdb.CompoundIndex{ - Indexes: []memdb.Indexer{ - &memdb.StringFieldIndex{ - Field: "Kind", - Lowercase: true, - }, - &memdb.StringFieldIndex{ - Field: "Name", - Lowercase: true, - }, - }, - }, - }, - "kind": &memdb.IndexSchema{ - Name: "kind", - AllowMissing: false, - Unique: false, - Indexer: &memdb.StringFieldIndex{ - Field: "Kind", - Lowercase: true, - }, - }, - "link": &memdb.IndexSchema{ - Name: "link", - AllowMissing: true, - Unique: false, - Indexer: &ConfigEntryLinkIndex{}, - }, - }, - } -} - type ConfigEntryLinkIndex struct { } @@ -61,7 +19,7 @@ type discoveryChainConfigEntry interface { structs.ConfigEntry // ListRelatedServices returns a list of other names of services referenced // in this config entry. - ListRelatedServices() []string + ListRelatedServices() []structs.ServiceID } func (s *ConfigEntryLinkIndex) FromObject(obj interface{}) (bool, [][]byte, error) { @@ -84,7 +42,7 @@ func (s *ConfigEntryLinkIndex) FromObject(obj interface{}) (bool, [][]byte, erro vals := make([][]byte, 0, numLinks) for _, linkedService := range linkedServices { - vals = append(vals, []byte(linkedService+"\x00")) + vals = append(vals, []byte(linkedService.String()+"\x00")) } return true, vals, nil @@ -94,13 +52,12 @@ func (s *ConfigEntryLinkIndex) FromArgs(args ...interface{}) ([]byte, error) { if len(args) != 1 { return nil, fmt.Errorf("must provide only a single argument") } - arg, ok := args[0].(string) + arg, ok := args[0].(structs.ServiceID) if !ok { - return nil, fmt.Errorf("argument must be a string: %#v", args[0]) + return nil, fmt.Errorf("argument must be a structs.ServiceID: %#v", args[0]) } // Add the null character as a terminator - arg += "\x00" - return []byte(arg), nil + return []byte(arg.String() + "\x00"), nil } func (s *ConfigEntryLinkIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) { @@ -150,18 +107,18 @@ func (s *Restore) ConfigEntry(c structs.ConfigEntry) error { } // ConfigEntry is called to get a given config entry. -func (s *Store) ConfigEntry(ws memdb.WatchSet, kind, name string) (uint64, structs.ConfigEntry, error) { +func (s *Store) ConfigEntry(ws memdb.WatchSet, kind, name string, entMeta *structs.EnterpriseMeta) (uint64, structs.ConfigEntry, error) { tx := s.db.Txn(false) defer tx.Abort() - return s.configEntryTxn(tx, ws, kind, name) + return s.configEntryTxn(tx, ws, kind, name, entMeta) } -func (s *Store) configEntryTxn(tx *memdb.Txn, ws memdb.WatchSet, kind, name string) (uint64, structs.ConfigEntry, error) { +func (s *Store) configEntryTxn(tx *memdb.Txn, ws memdb.WatchSet, kind, name string, entMeta *structs.EnterpriseMeta) (uint64, structs.ConfigEntry, error) { // Get the index idx := maxIndexTxn(tx, configTableName) // Get the existing config entry. - watchCh, existing, err := tx.FirstWatch(configTableName, "id", kind, name) + watchCh, existing, err := s.firstWatchConfigEntryWithTxn(tx, kind, name, entMeta) if err != nil { return 0, nil, fmt.Errorf("failed config entry lookup: %s", err) } @@ -179,19 +136,19 @@ func (s *Store) configEntryTxn(tx *memdb.Txn, ws memdb.WatchSet, kind, name stri } // ConfigEntries is called to get all config entry objects. -func (s *Store) ConfigEntries(ws memdb.WatchSet) (uint64, []structs.ConfigEntry, error) { - return s.ConfigEntriesByKind(ws, "") +func (s *Store) ConfigEntries(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, []structs.ConfigEntry, error) { + return s.ConfigEntriesByKind(ws, "", entMeta) } // ConfigEntriesByKind is called to get all config entry objects with the given kind. // If kind is empty, all config entries will be returned. -func (s *Store) ConfigEntriesByKind(ws memdb.WatchSet, kind string) (uint64, []structs.ConfigEntry, error) { +func (s *Store) ConfigEntriesByKind(ws memdb.WatchSet, kind string, entMeta *structs.EnterpriseMeta) (uint64, []structs.ConfigEntry, error) { tx := s.db.Txn(false) defer tx.Abort() - return s.configEntriesByKindTxn(tx, ws, kind) + return s.configEntriesByKindTxn(tx, ws, kind, entMeta) } -func (s *Store) configEntriesByKindTxn(tx *memdb.Txn, ws memdb.WatchSet, kind string) (uint64, []structs.ConfigEntry, error) { +func (s *Store) configEntriesByKindTxn(tx *memdb.Txn, ws memdb.WatchSet, kind string, entMeta *structs.EnterpriseMeta) (uint64, []structs.ConfigEntry, error) { // Get the index idx := maxIndexTxn(tx, configTableName) @@ -199,9 +156,9 @@ func (s *Store) configEntriesByKindTxn(tx *memdb.Txn, ws memdb.WatchSet, kind st var iter memdb.ResultIterator var err error if kind != "" { - iter, err = tx.Get(configTableName, "kind", kind) + iter, err = getConfigEntryKindsWithTxn(tx, kind, entMeta) } else { - iter, err = tx.Get(configTableName, "id") + iter, err = getAllConfigEntriesWithTxn(tx, entMeta) } if err != nil { return 0, nil, fmt.Errorf("failed config entry lookup: %s", err) @@ -216,11 +173,11 @@ func (s *Store) configEntriesByKindTxn(tx *memdb.Txn, ws memdb.WatchSet, kind st } // EnsureConfigEntry is called to do an upsert of a given config entry. -func (s *Store) EnsureConfigEntry(idx uint64, conf structs.ConfigEntry) error { +func (s *Store) EnsureConfigEntry(idx uint64, conf structs.ConfigEntry, entMeta *structs.EnterpriseMeta) error { tx := s.db.Txn(true) defer tx.Abort() - if err := s.ensureConfigEntryTxn(tx, idx, conf); err != nil { + if err := s.ensureConfigEntryTxn(tx, idx, conf, entMeta); err != nil { return err } @@ -229,9 +186,9 @@ func (s *Store) EnsureConfigEntry(idx uint64, conf structs.ConfigEntry) error { } // ensureConfigEntryTxn upserts a config entry inside of a transaction. -func (s *Store) ensureConfigEntryTxn(tx *memdb.Txn, idx uint64, conf structs.ConfigEntry) error { +func (s *Store) ensureConfigEntryTxn(tx *memdb.Txn, idx uint64, conf structs.ConfigEntry, entMeta *structs.EnterpriseMeta) error { // Check for existing configuration. - existing, err := tx.First(configTableName, "id", conf.GetKind(), conf.GetName()) + existing, err := s.firstConfigEntryWithTxn(tx, conf.GetKind(), conf.GetName(), entMeta) if err != nil { return fmt.Errorf("failed configuration lookup: %s", err) } @@ -252,13 +209,14 @@ func (s *Store) ensureConfigEntryTxn(tx *memdb.Txn, idx uint64, conf structs.Con conf.GetKind(), conf.GetName(), conf, + entMeta, ) if err != nil { return err // Err is already sufficiently decorated. } // Insert the config entry and update the index - if err := tx.Insert(configTableName, conf); err != nil { + if err := s.insertConfigEntryWithTxn(tx, conf); err != nil { return fmt.Errorf("failed inserting config entry: %s", err) } if err := indexUpdateMaxTxn(tx, idx, configTableName); err != nil { @@ -269,12 +227,12 @@ func (s *Store) ensureConfigEntryTxn(tx *memdb.Txn, idx uint64, conf structs.Con } // EnsureConfigEntryCAS is called to do a check-and-set upsert of a given config entry. -func (s *Store) EnsureConfigEntryCAS(idx, cidx uint64, conf structs.ConfigEntry) (bool, error) { +func (s *Store) EnsureConfigEntryCAS(idx, cidx uint64, conf structs.ConfigEntry, entMeta *structs.EnterpriseMeta) (bool, error) { tx := s.db.Txn(true) defer tx.Abort() // Check for existing configuration. - existing, err := tx.First(configTableName, "id", conf.GetKind(), conf.GetName()) + existing, err := s.firstConfigEntryWithTxn(tx, conf.GetKind(), conf.GetName(), entMeta) if err != nil { return false, fmt.Errorf("failed configuration lookup: %s", err) } @@ -295,7 +253,7 @@ func (s *Store) EnsureConfigEntryCAS(idx, cidx uint64, conf structs.ConfigEntry) return false, nil } - if err := s.ensureConfigEntryTxn(tx, idx, conf); err != nil { + if err := s.ensureConfigEntryTxn(tx, idx, conf, entMeta); err != nil { return false, err } @@ -303,12 +261,12 @@ func (s *Store) EnsureConfigEntryCAS(idx, cidx uint64, conf structs.ConfigEntry) return true, nil } -func (s *Store) DeleteConfigEntry(idx uint64, kind, name string) error { +func (s *Store) DeleteConfigEntry(idx uint64, kind, name string, entMeta *structs.EnterpriseMeta) error { tx := s.db.Txn(true) defer tx.Abort() // Try to retrieve the existing config entry. - existing, err := tx.First(configTableName, "id", kind, name) + existing, err := s.firstConfigEntryWithTxn(tx, kind, name, entMeta) if err != nil { return fmt.Errorf("failed config entry lookup: %s", err) } @@ -322,6 +280,7 @@ func (s *Store) DeleteConfigEntry(idx uint64, kind, name string) error { kind, name, nil, + entMeta, ) if err != nil { return err // Err is already sufficiently decorated. @@ -351,7 +310,9 @@ func (s *Store) validateProposedConfigEntryInGraph( idx uint64, kind, name string, next structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) error { + validateAllChains := false switch kind { @@ -368,7 +329,7 @@ func (s *Store) validateProposedConfigEntryInGraph( return fmt.Errorf("unhandled kind %q during validation of %q", kind, name) } - return s.validateProposedConfigEntryInServiceGraph(tx, idx, kind, name, next, validateAllChains) + return s.validateProposedConfigEntryInServiceGraph(tx, idx, kind, name, next, validateAllChains, entMeta) } var serviceGraphKinds = []string{ @@ -383,10 +344,11 @@ func (s *Store) validateProposedConfigEntryInServiceGraph( kind, name string, next structs.ConfigEntry, validateAllChains bool, + entMeta *structs.EnterpriseMeta, ) error { // Collect all of the chains that could be affected by this change // including our own. - checkChains := make(map[string]struct{}) + checkChains := make(map[structs.ServiceID]struct{}) if validateAllChains { // Must be proxy-defaults/global. @@ -395,23 +357,24 @@ func (s *Store) validateProposedConfigEntryInServiceGraph( // somehow omit the ones that have a default protocol configured. for _, kind := range serviceGraphKinds { - _, entries, err := s.configEntriesByKindTxn(tx, nil, kind) + _, entries, err := s.configEntriesByKindTxn(tx, nil, kind, entMeta) if err != nil { return err } for _, entry := range entries { - checkChains[entry.GetName()] = struct{}{} + checkChains[structs.NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())] = struct{}{} } } } else { // Must be a single chain. - checkChains[name] = struct{}{} + sid := structs.NewServiceID(name, entMeta) + checkChains[sid] = struct{}{} - iter, err := tx.Get(configTableName, "link", name) + iter, err := tx.Get(configTableName, "link", sid) for raw := iter.Next(); raw != nil; raw = iter.Next() { entry := raw.(structs.ConfigEntry) - checkChains[entry.GetName()] = struct{}{} + checkChains[structs.NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())] = struct{}{} } if err != nil { return err @@ -422,8 +385,8 @@ func (s *Store) validateProposedConfigEntryInServiceGraph( {Kind: kind, Name: name}: next, } - for chainName, _ := range checkChains { - if err := s.testCompileDiscoveryChain(tx, nil, chainName, overrides); err != nil { + for chain, _ := range checkChains { + if err := s.testCompileDiscoveryChain(tx, nil, chain.ID, overrides, &chain.EnterpriseMeta); err != nil { return err } } @@ -436,8 +399,9 @@ func (s *Store) testCompileDiscoveryChain( ws memdb.WatchSet, chainName string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) error { - _, speculativeEntries, err := s.readDiscoveryChainConfigEntriesTxn(tx, nil, chainName, overrides) + _, speculativeEntries, err := s.readDiscoveryChainConfigEntriesTxn(tx, nil, chainName, overrides, entMeta) if err != nil { return err } @@ -448,7 +412,7 @@ func (s *Store) testCompileDiscoveryChain( // TODO(rb): we should thread a better value than "dc1" and the throwaway trust domain down here as that is going to sometimes show up in user facing errors req := discoverychain.CompileRequest{ ServiceName: chainName, - EvaluateInNamespace: "default", + EvaluateInNamespace: entMeta.NamespaceOrDefault(), EvaluateInDatacenter: "dc1", EvaluateInTrustDomain: "b6fc9da3-03d4-4b5a-9134-c045e9b20152.consul", UseInDatacenter: "dc1", @@ -467,8 +431,9 @@ func (s *Store) testCompileDiscoveryChain( func (s *Store) ReadDiscoveryChainConfigEntries( ws memdb.WatchSet, serviceName string, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.DiscoveryChainConfigEntries, error) { - return s.readDiscoveryChainConfigEntries(ws, serviceName, nil) + return s.readDiscoveryChainConfigEntries(ws, serviceName, nil, entMeta) } // readDiscoveryChainConfigEntries will query for the full discovery chain for @@ -485,10 +450,11 @@ func (s *Store) readDiscoveryChainConfigEntries( ws memdb.WatchSet, serviceName string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.DiscoveryChainConfigEntries, error) { tx := s.db.Txn(false) defer tx.Abort() - return s.readDiscoveryChainConfigEntriesTxn(tx, ws, serviceName, overrides) + return s.readDiscoveryChainConfigEntriesTxn(tx, ws, serviceName, overrides, entMeta) } func (s *Store) readDiscoveryChainConfigEntriesTxn( @@ -496,6 +462,7 @@ func (s *Store) readDiscoveryChainConfigEntriesTxn( ws memdb.WatchSet, serviceName string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.DiscoveryChainConfigEntries, error) { res := structs.NewDiscoveryChainConfigEntries() @@ -511,13 +478,15 @@ func (s *Store) readDiscoveryChainConfigEntriesTxn( // the end of this function to indicate "no such entry". var ( - todoSplitters = make(map[string]struct{}) - todoResolvers = make(map[string]struct{}) - todoDefaults = make(map[string]struct{}) + todoSplitters = make(map[structs.ServiceID]struct{}) + todoResolvers = make(map[structs.ServiceID]struct{}) + todoDefaults = make(map[structs.ServiceID]struct{}) ) + sid := structs.NewServiceID(serviceName, entMeta) + // Grab the proxy defaults if they exist. - idx, proxy, err := s.getProxyConfigEntryTxn(tx, ws, structs.ProxyConfigGlobal, overrides) + idx, proxy, err := s.getProxyConfigEntryTxn(tx, ws, structs.ProxyConfigGlobal, overrides, structs.DefaultEnterpriseMeta()) if err != nil { return 0, nil, err } else if proxy != nil { @@ -525,14 +494,14 @@ func (s *Store) readDiscoveryChainConfigEntriesTxn( } // At every step we'll need service defaults. - todoDefaults[serviceName] = struct{}{} + todoDefaults[sid] = struct{}{} // first fetch the router, of which we only collect 1 per chain eval - _, router, err := s.getRouterConfigEntryTxn(tx, ws, serviceName, overrides) + _, router, err := s.getRouterConfigEntryTxn(tx, ws, serviceName, overrides, entMeta) if err != nil { return 0, nil, err } else if router != nil { - res.Routers[serviceName] = router + res.Routers[sid] = router } if router != nil { @@ -541,39 +510,39 @@ func (s *Store) readDiscoveryChainConfigEntriesTxn( } } else { // Next hop in the chain is the splitter. - todoSplitters[serviceName] = struct{}{} + todoSplitters[sid] = struct{}{} } for { - name, ok := anyKey(todoSplitters) + splitID, ok := anyKey(todoSplitters) if !ok { break } - delete(todoSplitters, name) + delete(todoSplitters, splitID) - if _, ok := res.Splitters[name]; ok { + if _, ok := res.Splitters[splitID]; ok { continue // already fetched } // Yes, even for splitters. - todoDefaults[name] = struct{}{} + todoDefaults[splitID] = struct{}{} - _, splitter, err := s.getSplitterConfigEntryTxn(tx, ws, name, overrides) + _, splitter, err := s.getSplitterConfigEntryTxn(tx, ws, splitID.ID, overrides, &splitID.EnterpriseMeta) if err != nil { return 0, nil, err } if splitter == nil { - res.Splitters[name] = nil + res.Splitters[splitID] = nil // Next hop in the chain is the resolver. - todoResolvers[name] = struct{}{} + todoResolvers[splitID] = struct{}{} continue } - res.Splitters[name] = splitter + res.Splitters[splitID] = splitter - todoResolvers[name] = struct{}{} + todoResolvers[splitID] = struct{}{} for _, svc := range splitter.ListRelatedServices() { // If there is no splitter, this will end up adding a resolver // after another iteration. @@ -582,30 +551,30 @@ func (s *Store) readDiscoveryChainConfigEntriesTxn( } for { - name, ok := anyKey(todoResolvers) + resolverID, ok := anyKey(todoResolvers) if !ok { break } - delete(todoResolvers, name) + delete(todoResolvers, resolverID) - if _, ok := res.Resolvers[name]; ok { + if _, ok := res.Resolvers[resolverID]; ok { continue // already fetched } // And resolvers, too. - todoDefaults[name] = struct{}{} + todoDefaults[resolverID] = struct{}{} - _, resolver, err := s.getResolverConfigEntryTxn(tx, ws, name, overrides) + _, resolver, err := s.getResolverConfigEntryTxn(tx, ws, resolverID.ID, overrides, &resolverID.EnterpriseMeta) if err != nil { return 0, nil, err } if resolver == nil { - res.Resolvers[name] = nil + res.Resolvers[resolverID] = nil continue } - res.Resolvers[name] = resolver + res.Resolvers[resolverID] = resolver for _, svc := range resolver.ListRelatedServices() { todoResolvers[svc] = struct{}{} @@ -613,48 +582,48 @@ func (s *Store) readDiscoveryChainConfigEntriesTxn( } for { - name, ok := anyKey(todoDefaults) + svcID, ok := anyKey(todoDefaults) if !ok { break } - delete(todoDefaults, name) + delete(todoDefaults, svcID) - if _, ok := res.Services[name]; ok { + if _, ok := res.Services[svcID]; ok { continue // already fetched } - _, entry, err := s.getServiceConfigEntryTxn(tx, ws, name, overrides) + _, entry, err := s.getServiceConfigEntryTxn(tx, ws, svcID.ID, overrides, &svcID.EnterpriseMeta) if err != nil { return 0, nil, err } if entry == nil { - res.Services[name] = nil + res.Services[svcID] = nil continue } - res.Services[name] = entry + res.Services[svcID] = entry } // Strip nils now that they are no longer necessary. - for name, entry := range res.Routers { + for sid, entry := range res.Routers { if entry == nil { - delete(res.Routers, name) + delete(res.Routers, sid) } } - for name, entry := range res.Splitters { + for sid, entry := range res.Splitters { if entry == nil { - delete(res.Splitters, name) + delete(res.Splitters, sid) } } - for name, entry := range res.Resolvers { + for sid, entry := range res.Resolvers { if entry == nil { - delete(res.Resolvers, name) + delete(res.Resolvers, sid) } } - for name, entry := range res.Services { + for sid, entry := range res.Services { if entry == nil { - delete(res.Services, name) + delete(res.Services, sid) } } @@ -663,14 +632,14 @@ func (s *Store) readDiscoveryChainConfigEntriesTxn( // anyKey returns any key from the provided map if any exist. Useful for using // a map as a simple work queue of sorts. -func anyKey(m map[string]struct{}) (string, bool) { +func anyKey(m map[structs.ServiceID]struct{}) (structs.ServiceID, bool) { if len(m) == 0 { - return "", false + return structs.ServiceID{}, false } for k, _ := range m { return k, true } - return "", false + return structs.ServiceID{}, false } // getProxyConfigEntryTxn is a convenience method for fetching a @@ -682,8 +651,9 @@ func (s *Store) getProxyConfigEntryTxn( ws memdb.WatchSet, name string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.ProxyConfigEntry, error) { - idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ProxyDefaults, name, overrides) + idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ProxyDefaults, name, overrides, entMeta) if err != nil { return 0, nil, err } else if entry == nil { @@ -706,8 +676,9 @@ func (s *Store) getServiceConfigEntryTxn( ws memdb.WatchSet, serviceName string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.ServiceConfigEntry, error) { - idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceDefaults, serviceName, overrides) + idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceDefaults, serviceName, overrides, entMeta) if err != nil { return 0, nil, err } else if entry == nil { @@ -730,8 +701,9 @@ func (s *Store) getRouterConfigEntryTxn( ws memdb.WatchSet, serviceName string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.ServiceRouterConfigEntry, error) { - idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceRouter, serviceName, overrides) + idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceRouter, serviceName, overrides, entMeta) if err != nil { return 0, nil, err } else if entry == nil { @@ -754,8 +726,9 @@ func (s *Store) getSplitterConfigEntryTxn( ws memdb.WatchSet, serviceName string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.ServiceSplitterConfigEntry, error) { - idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceSplitter, serviceName, overrides) + idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceSplitter, serviceName, overrides, entMeta) if err != nil { return 0, nil, err } else if entry == nil { @@ -778,8 +751,9 @@ func (s *Store) getResolverConfigEntryTxn( ws memdb.WatchSet, serviceName string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, *structs.ServiceResolverConfigEntry, error) { - idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceResolver, serviceName, overrides) + idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceResolver, serviceName, overrides, entMeta) if err != nil { return 0, nil, err } else if entry == nil { @@ -799,6 +773,7 @@ func (s *Store) configEntryWithOverridesTxn( kind string, name string, overrides map[structs.ConfigEntryKindName]structs.ConfigEntry, + entMeta *structs.EnterpriseMeta, ) (uint64, structs.ConfigEntry, error) { if len(overrides) > 0 { entry, ok := overrides[structs.ConfigEntryKindName{ @@ -809,5 +784,5 @@ func (s *Store) configEntryWithOverridesTxn( } } - return s.configEntryTxn(tx, ws, kind, name) + return s.configEntryTxn(tx, ws, kind, name, entMeta) } diff --git a/agent/consul/state/config_entry_oss.go b/agent/consul/state/config_entry_oss.go new file mode 100644 index 0000000000..6168e5cd57 --- /dev/null +++ b/agent/consul/state/config_entry_oss.go @@ -0,0 +1,73 @@ +// +build !consulent + +package state + +import ( + "github.com/hashicorp/consul/agent/structs" + memdb "github.com/hashicorp/go-memdb" +) + +// configTableSchema returns a new table schema used to store global +// config entries. +func configTableSchema() *memdb.TableSchema { + return &memdb.TableSchema{ + Name: configTableName, + Indexes: map[string]*memdb.IndexSchema{ + "id": &memdb.IndexSchema{ + Name: "id", + AllowMissing: false, + Unique: true, + Indexer: &memdb.CompoundIndex{ + Indexes: []memdb.Indexer{ + &memdb.StringFieldIndex{ + Field: "Kind", + Lowercase: true, + }, + &memdb.StringFieldIndex{ + Field: "Name", + Lowercase: true, + }, + }, + }, + }, + "kind": &memdb.IndexSchema{ + Name: "kind", + AllowMissing: false, + Unique: false, + Indexer: &memdb.StringFieldIndex{ + Field: "Kind", + Lowercase: true, + }, + }, + "link": &memdb.IndexSchema{ + Name: "link", + AllowMissing: true, + Unique: false, + Indexer: &ConfigEntryLinkIndex{}, + }, + }, + } +} + +func (s *Store) firstConfigEntryWithTxn(tx *memdb.Txn, + kind, name string, entMeta *structs.EnterpriseMeta) (interface{}, error) { + return tx.First(configTableName, "id", kind, name) +} + +func (s *Store) firstWatchConfigEntryWithTxn(tx *memdb.Txn, + kind, name string, entMeta *structs.EnterpriseMeta) (<-chan struct{}, interface{}, error) { + return tx.FirstWatch(configTableName, "id", kind, name) +} + +func (s *Store) insertConfigEntryWithTxn(tx *memdb.Txn, conf structs.ConfigEntry) error { + return tx.Insert(configTableName, conf) +} + +func getAllConfigEntriesWithTxn(tx *memdb.Txn, entMeta *structs.EnterpriseMeta) (memdb.ResultIterator, error) { + return tx.Get(configTableName, "id") +} + +func getConfigEntryKindsWithTxn(tx *memdb.Txn, + kind string, entMeta *structs.EnterpriseMeta) (memdb.ResultIterator, error) { + return tx.Get(configTableName, "kind", kind) +} diff --git a/agent/consul/state/config_entry_test.go b/agent/consul/state/config_entry_test.go index 599e4e8e2f..f3596283fa 100644 --- a/agent/consul/state/config_entry_test.go +++ b/agent/consul/state/config_entry_test.go @@ -22,9 +22,9 @@ func TestStore_ConfigEntry(t *testing.T) { } // Create - require.NoError(s.EnsureConfigEntry(0, expected)) + require.NoError(s.EnsureConfigEntry(0, expected, nil)) - idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global") + idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(0), idx) require.Equal(expected, config) @@ -37,17 +37,17 @@ func TestStore_ConfigEntry(t *testing.T) { "DestinationServiceName": "bar", }, } - require.NoError(s.EnsureConfigEntry(1, updated)) + require.NoError(s.EnsureConfigEntry(1, updated, nil)) - idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global") + idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(1), idx) require.Equal(updated, config) // Delete - require.NoError(s.DeleteConfigEntry(2, structs.ProxyDefaults, "global")) + require.NoError(s.DeleteConfigEntry(2, structs.ProxyDefaults, "global", nil)) - idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global") + idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(2), idx) require.Nil(config) @@ -57,19 +57,19 @@ func TestStore_ConfigEntry(t *testing.T) { Kind: structs.ServiceDefaults, Name: "foo", } - require.NoError(s.EnsureConfigEntry(3, serviceConf)) + require.NoError(s.EnsureConfigEntry(3, serviceConf, nil)) ws := memdb.NewWatchSet() - _, _, err = s.ConfigEntry(ws, structs.ServiceDefaults, "foo") + _, _, err = s.ConfigEntry(ws, structs.ServiceDefaults, "foo", nil) require.NoError(err) // Make an unrelated modification and make sure the watch doesn't fire. - require.NoError(s.EnsureConfigEntry(4, updated)) + require.NoError(s.EnsureConfigEntry(4, updated, nil)) require.False(watchFired(ws)) // Update the watched config and make sure it fires. serviceConf.Protocol = "http" - require.NoError(s.EnsureConfigEntry(5, serviceConf)) + require.NoError(s.EnsureConfigEntry(5, serviceConf, nil)) require.True(watchFired(ws)) } @@ -86,9 +86,9 @@ func TestStore_ConfigEntryCAS(t *testing.T) { } // Create - require.NoError(s.EnsureConfigEntry(1, expected)) + require.NoError(s.EnsureConfigEntry(1, expected, nil)) - idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global") + idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(1), idx) require.Equal(expected, config) @@ -101,23 +101,23 @@ func TestStore_ConfigEntryCAS(t *testing.T) { "DestinationServiceName": "bar", }, } - ok, err := s.EnsureConfigEntryCAS(2, 99, updated) + ok, err := s.EnsureConfigEntryCAS(2, 99, updated, nil) require.False(ok) require.NoError(err) // Entry should not be changed - idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global") + idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(1), idx) require.Equal(expected, config) // Update with a valid index - ok, err = s.EnsureConfigEntryCAS(2, 1, updated) + ok, err = s.EnsureConfigEntryCAS(2, 1, updated, nil) require.True(ok) require.NoError(err) // Entry should be updated - idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global") + idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal(updated, config) @@ -141,25 +141,25 @@ func TestStore_ConfigEntries(t *testing.T) { Name: "test3", } - require.NoError(s.EnsureConfigEntry(0, entry1)) - require.NoError(s.EnsureConfigEntry(1, entry2)) - require.NoError(s.EnsureConfigEntry(2, entry3)) + require.NoError(s.EnsureConfigEntry(0, entry1, nil)) + require.NoError(s.EnsureConfigEntry(1, entry2, nil)) + require.NoError(s.EnsureConfigEntry(2, entry3, nil)) // Get all entries - idx, entries, err := s.ConfigEntries(nil) + idx, entries, err := s.ConfigEntries(nil, nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal([]structs.ConfigEntry{entry1, entry2, entry3}, entries) // Get all proxy entries - idx, entries, err = s.ConfigEntriesByKind(nil, structs.ProxyDefaults) + idx, entries, err = s.ConfigEntriesByKind(nil, structs.ProxyDefaults, nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal([]structs.ConfigEntry{entry1}, entries) // Get all service entries ws := memdb.NewWatchSet() - idx, entries, err = s.ConfigEntriesByKind(ws, structs.ServiceDefaults) + idx, entries, err = s.ConfigEntriesByKind(ws, structs.ServiceDefaults, nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal([]structs.ConfigEntry{entry2, entry3}, entries) @@ -172,20 +172,19 @@ func TestStore_ConfigEntries(t *testing.T) { Kind: structs.ServiceDefaults, Name: "test2", Protocol: "tcp", - })) + }, nil)) require.True(watchFired(ws)) } func TestStore_ConfigEntry_GraphValidation(t *testing.T) { - for _, tc := range []struct { - name string + type tcase struct { entries []structs.ConfigEntry op func(t *testing.T, s *Store) error expectErr string expectGraphErr bool - }{ - { - name: "splitter fails without default protocol", + } + cases := map[string]tcase{ + "splitter fails without default protocol": tcase{ entries: []structs.ConfigEntry{}, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceSplitterConfigEntry{ @@ -196,13 +195,12 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { {Weight: 10, Namespace: "v2"}, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, - { - name: "splitter fails with tcp protocol", + "splitter fails with tcp protocol": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -219,13 +217,12 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { {Weight: 10, Namespace: "v2"}, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, - { - name: "splitter works with http protocol", + "splitter works with http protocol": tcase{ entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, @@ -235,9 +232,22 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, &structs.ServiceConfigEntry{ - Kind: structs.ServiceDefaults, - Name: "main", - Protocol: "http", + Kind: structs.ServiceDefaults, + Name: "main", + Protocol: "http", + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), + }, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "main", + Subsets: map[string]structs.ServiceResolverSubset{ + "v1": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v1", + }, + "v2": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v2", + }, + }, }, }, op: func(t *testing.T, s *Store) error { @@ -245,15 +255,15 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ - {Weight: 90, Namespace: "v1"}, - {Weight: 10, Namespace: "v2"}, + {Weight: 90, ServiceSubset: "v1"}, + {Weight: 10, ServiceSubset: "v2"}, }, + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, }, - { - name: "splitter works with http protocol (from proxy-defaults)", + "splitter works with http protocol (from proxy-defaults)": tcase{ entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, @@ -272,11 +282,10 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { {Weight: 10, Namespace: "v2"}, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, }, - { - name: "router fails with tcp protocol", + "router fails with tcp protocol": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -301,13 +310,12 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, - { - name: "router fails without default protocol", + "router fails without default protocol": tcase{ entries: []structs.ConfigEntry{}, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceRouterConfigEntry{ @@ -326,37 +334,47 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, ///////////////////////////////////////////////// - { - name: "cannot remove default protocol after splitter created", + "cannot remove default protocol after splitter created": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "main", + Subsets: map[string]structs.ServiceResolverSubset{ + "v1": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v1", + }, + "v2": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v2", + }, + }, + }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ - {Weight: 90, Namespace: "v1"}, - {Weight: 10, Namespace: "v2"}, + {Weight: 90, ServiceSubset: "v1"}, + {Weight: 10, ServiceSubset: "v2"}, }, }, }, op: func(t *testing.T, s *Store) error { - return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main") + return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main", nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, - { - name: "cannot remove global default protocol after splitter created", + "cannot remove global default protocol after splitter created": tcase{ entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, @@ -375,13 +393,12 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, op: func(t *testing.T, s *Store) error { - return s.DeleteConfigEntry(0, structs.ProxyDefaults, structs.ProxyConfigGlobal) + return s.DeleteConfigEntry(0, structs.ProxyDefaults, structs.ProxyConfigGlobal, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, - { - name: "can remove global default protocol after splitter created if service default overrides it", + "can remove global default protocol after splitter created if service default overrides it": tcase{ entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, @@ -395,33 +412,56 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { Name: "main", Protocol: "http", }, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "main", + Subsets: map[string]structs.ServiceResolverSubset{ + "v1": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v1", + }, + "v2": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v2", + }, + }, + }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ - {Weight: 90, Namespace: "v1"}, - {Weight: 10, Namespace: "v2"}, + {Weight: 90, ServiceSubset: "v1"}, + {Weight: 10, ServiceSubset: "v2"}, }, }, }, op: func(t *testing.T, s *Store) error { - return s.DeleteConfigEntry(0, structs.ProxyDefaults, structs.ProxyConfigGlobal) + return s.DeleteConfigEntry(0, structs.ProxyDefaults, structs.ProxyConfigGlobal, nil) }, }, - { - name: "cannot change to tcp protocol after splitter created", + "cannot change to tcp protocol after splitter created": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "main", + Subsets: map[string]structs.ServiceResolverSubset{ + "v1": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v1", + }, + "v2": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == v2", + }, + }, + }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ - {Weight: 90, Namespace: "v1"}, - {Weight: 10, Namespace: "v2"}, + {Weight: 90, ServiceSubset: "v1"}, + {Weight: 10, ServiceSubset: "v2"}, }, }, }, @@ -431,19 +471,27 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { Name: "main", Protocol: "tcp", } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, - { - name: "cannot remove default protocol after router created", + "cannot remove default protocol after router created": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "main", + Subsets: map[string]structs.ServiceResolverSubset{ + "other": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == other", + }, + }, + }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", @@ -455,26 +503,34 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, Destination: &structs.ServiceRouteDestination{ - Namespace: "other", + ServiceSubset: "other", }, }, }, }, }, op: func(t *testing.T, s *Store) error { - return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main") + return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main", nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, - { - name: "cannot change to tcp protocol after router created", + "cannot change to tcp protocol after router created": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "main", + Subsets: map[string]structs.ServiceResolverSubset{ + "other": structs.ServiceResolverSubset{ + Filter: "Service.Meta.version == other", + }, + }, + }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", @@ -486,7 +542,7 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, Destination: &structs.ServiceRouteDestination{ - Namespace: "other", + ServiceSubset: "other", }, }, }, @@ -498,14 +554,13 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { Name: "main", Protocol: "tcp", } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, ///////////////////////////////////////////////// - { - name: "cannot split to a service using tcp", + "cannot split to a service using tcp": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -527,13 +582,12 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { {Weight: 10, Service: "other"}, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, - { - name: "cannot route to a service using tcp", + "cannot route to a service using tcp": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -563,14 +617,13 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, ///////////////////////////////////////////////// - { - name: "cannot failover to a service using a different protocol", + "cannot failover to a service using a different protocol": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -598,13 +651,12 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { }, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, - { - name: "cannot redirect to a service using a different protocol", + "cannot redirect to a service using a different protocol": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -630,14 +682,13 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { Service: "other", }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, ///////////////////////////////////////////////// - { - name: "redirect to a subset that does exist is fine", + "redirect to a subset that does exist is fine": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, @@ -659,11 +710,10 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { ServiceSubset: "v1", }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, }, - { - name: "cannot redirect to a subset that does not exist", + "cannot redirect to a subset that does not exist": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, @@ -680,14 +730,13 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { ServiceSubset: "v1", }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: `does not have a subset named "v1"`, expectGraphErr: true, }, ///////////////////////////////////////////////// - { - name: "cannot introduce circular resolver redirect", + "cannot introduce circular resolver redirect": tcase{ entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, @@ -705,13 +754,12 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { Service: "other", }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: `detected circular resolver redirect`, expectGraphErr: true, }, - { - name: "cannot introduce circular split", + "cannot introduce circular split": tcase{ entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, @@ -736,18 +784,21 @@ func TestStore_ConfigEntry_GraphValidation(t *testing.T) { {Weight: 100, Service: "other"}, }, } - return s.EnsureConfigEntry(0, entry) + return s.EnsureConfigEntry(0, entry, nil) }, expectErr: `detected circular reference`, expectGraphErr: true, }, - } { + } + + for name, tc := range cases { + name := name tc := tc - t.Run(tc.name, func(t *testing.T) { + t.Run(name, func(t *testing.T) { s := testStateStore(t) for _, entry := range tc.entries { - require.NoError(t, s.EnsureConfigEntry(0, entry)) + require.NoError(t, s.EnsureConfigEntry(0, entry, nil)) } err := tc.op(t, s) @@ -819,7 +870,7 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) { {Kind: structs.ServiceDefaults, Name: "main"}, }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { - defaults := entrySet.GetService("main") + defaults := entrySet.GetService(structs.NewServiceID("main", nil)) require.NotNil(t, defaults) require.Equal(t, "grpc", defaults.Protocol) }, @@ -912,7 +963,7 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) { {Kind: structs.ServiceRouter, Name: "main"}, }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { - router := entrySet.GetRouter("main") + router := entrySet.GetRouter(structs.NewServiceID("main", nil)) require.NotNil(t, router) require.Len(t, router.Routes, 1) @@ -992,7 +1043,7 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) { {Kind: structs.ServiceSplitter, Name: "main"}, }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { - splitter := entrySet.GetSplitter("main") + splitter := entrySet.GetSplitter(structs.NewServiceID("main", nil)) require.NotNil(t, splitter) require.Len(t, splitter.Splits, 2) @@ -1044,7 +1095,7 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) { {Kind: structs.ServiceResolver, Name: "main"}, }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { - resolver := entrySet.GetResolver("main") + resolver := entrySet.GetResolver(structs.NewServiceID("main", nil)) require.NotNil(t, resolver) require.Equal(t, 33*time.Second, resolver.ConnectTimeout) }, @@ -1055,18 +1106,18 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) { t.Run(tc.name, func(t *testing.T) { s := testStateStore(t) for _, entry := range tc.entries { - require.NoError(t, s.EnsureConfigEntry(0, entry)) + require.NoError(t, s.EnsureConfigEntry(0, entry, nil)) } t.Run("without override", func(t *testing.T) { - _, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", nil) + _, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", nil, nil) require.NoError(t, err) got := entrySetToKindNames(entrySet) require.ElementsMatch(t, tc.expectBefore, got) }) t.Run("with override", func(t *testing.T) { - _, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", tc.overrides) + _, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", tc.overrides, nil) if tc.expectAfterErr != "" { require.Error(t, err) @@ -1146,10 +1197,10 @@ func TestStore_ReadDiscoveryChainConfigEntries_SubsetSplit(t *testing.T) { } for _, entry := range entries { - require.NoError(t, s.EnsureConfigEntry(0, entry)) + require.NoError(t, s.EnsureConfigEntry(0, entry, nil)) } - _, entrySet, err := s.ReadDiscoveryChainConfigEntries(nil, "main") + _, entrySet, err := s.ReadDiscoveryChainConfigEntries(nil, "main", nil) require.NoError(t, err) require.Len(t, entrySet.Routers, 0) diff --git a/agent/consul/state/state_store_oss_test.go b/agent/consul/state/state_store_oss_test.go new file mode 100644 index 0000000000..794dfac431 --- /dev/null +++ b/agent/consul/state/state_store_oss_test.go @@ -0,0 +1,7 @@ +// +build !consulent + +package state + +func (s *Store) setupDefaultTestEntMeta() error { + return nil +} diff --git a/agent/discovery_chain_endpoint.go b/agent/discovery_chain_endpoint.go index 6ed895a9e7..1df2e39f40 100644 --- a/agent/discovery_chain_endpoint.go +++ b/agent/discovery_chain_endpoint.go @@ -24,7 +24,11 @@ func (s *HTTPServer) DiscoveryChainRead(resp http.ResponseWriter, req *http.Requ } args.EvaluateInDatacenter = req.URL.Query().Get("compile-dc") - // TODO(namespaces): args.EvaluateInNamespace = req.URL.Query().Get("compile-namespace") + var entMeta structs.EnterpriseMeta + if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil { + return nil, err + } + args.WithEnterpriseMeta(&entMeta) if req.Method == "POST" { var raw map[string]interface{} diff --git a/agent/proxycfg/manager.go b/agent/proxycfg/manager.go index 0511b5a364..536431e69d 100644 --- a/agent/proxycfg/manager.go +++ b/agent/proxycfg/manager.go @@ -45,8 +45,8 @@ type Manager struct { mu sync.Mutex started bool - proxies map[string]*state - watchers map[string]map[uint64]chan *ConfigSnapshot + proxies map[structs.ServiceID]*state + watchers map[structs.ServiceID]map[uint64]chan *ConfigSnapshot } // ManagerConfig holds the required external dependencies for a Manager @@ -79,8 +79,8 @@ func NewManager(cfg ManagerConfig) (*Manager, error) { // Single item buffer is enough since there is no data transferred so this // is "level triggering" and we can't miss actual data. stateCh: make(chan struct{}, 1), - proxies: make(map[string]*state), - watchers: make(map[string]map[uint64]chan *ConfigSnapshot), + proxies: make(map[structs.ServiceID]*state), + watchers: make(map[structs.ServiceID]map[uint64]chan *ConfigSnapshot), } return m, nil } @@ -130,7 +130,7 @@ func (m *Manager) syncState() { // Traverse the local state and ensure all proxy services are registered services := m.State.Services(structs.WildcardEnterpriseMeta()) - for _, svc := range services { + for sid, svc := range services { if svc.Kind != structs.ServiceKindConnectProxy && svc.Kind != structs.ServiceKindMeshGateway { continue } @@ -141,20 +141,16 @@ func (m *Manager) syncState() { // know that so we'd need to set it here if not during registration of the // proxy service. Sidecar Service in the interim can do that, but we should // validate more generally that that is always true. - err := m.ensureProxyServiceLocked(svc, m.State.ServiceToken(svc.CompoundServiceID())) + err := m.ensureProxyServiceLocked(svc, m.State.ServiceToken(sid)) if err != nil { - m.Logger.Printf("[ERR] failed to watch proxy service %s: %s", svc.ID, + m.Logger.Printf("[ERR] failed to watch proxy service %s: %s", sid.String(), err) } } // Now see if any proxies were removed for proxyID := range m.proxies { - var key structs.ServiceID - // TODO (namespaces) pass through some real enterprise meta that probably needs to come from the proxy tracking - key.Init(proxyID, nil) - - if _, ok := services[key]; !ok { + if _, ok := services[proxyID]; !ok { // Remove them m.removeProxyServiceLocked(proxyID) } @@ -163,7 +159,8 @@ func (m *Manager) syncState() { // ensureProxyServiceLocked adds or changes the proxy to our state. func (m *Manager) ensureProxyServiceLocked(ns *structs.NodeService, token string) error { - state, ok := m.proxies[ns.ID] + sid := ns.CompoundServiceID() + state, ok := m.proxies[sid] if ok { if !state.Changed(ns, token) { @@ -190,7 +187,7 @@ func (m *Manager) ensureProxyServiceLocked(ns *structs.NodeService, token string if err != nil { return err } - m.proxies[ns.ID] = state + m.proxies[sid] = state // Start a goroutine that will wait for changes and broadcast them to watchers. go func(ch <-chan ConfigSnapshot) { @@ -205,7 +202,7 @@ func (m *Manager) ensureProxyServiceLocked(ns *structs.NodeService, token string // removeProxyService is called when a service deregisters and frees all // resources for that service. -func (m *Manager) removeProxyServiceLocked(proxyID string) { +func (m *Manager) removeProxyServiceLocked(proxyID structs.ServiceID) { state, ok := m.proxies[proxyID] if !ok { return @@ -269,7 +266,7 @@ OUTER: // This should not be possible since we should be the only sender, enforced // by m.mu but error and drop the update rather than panic. m.Logger.Printf("[ERR] proxycfg: failed to deliver ConfigSnapshot to %q", - snap.ProxyID) + snap.ProxyID.String()) } } @@ -277,7 +274,7 @@ OUTER: // will not fail, but no updates will be delivered until the proxy is // registered. If there is already a valid snapshot in memory, it will be // delivered immediately. -func (m *Manager) Watch(proxyID string) (<-chan *ConfigSnapshot, CancelFunc) { +func (m *Manager) Watch(proxyID structs.ServiceID) (<-chan *ConfigSnapshot, CancelFunc) { m.mu.Lock() defer m.mu.Unlock() @@ -311,7 +308,7 @@ func (m *Manager) Watch(proxyID string) (<-chan *ConfigSnapshot, CancelFunc) { // closeWatchLocked cleans up state related to a single watcher. It assumes the // lock is held. -func (m *Manager) closeWatchLocked(proxyID string, watchIdx uint64) { +func (m *Manager) closeWatchLocked(proxyID structs.ServiceID, watchIdx uint64) { if watchers, ok := m.watchers[proxyID]; ok { if ch, ok := watchers[watchIdx]; ok { delete(watchers, watchIdx) diff --git a/agent/proxycfg/manager_test.go b/agent/proxycfg/manager_test.go index 2cff32146a..11e907ce47 100644 --- a/agent/proxycfg/manager_test.go +++ b/agent/proxycfg/manager_test.go @@ -138,7 +138,7 @@ func TestManager_BasicLifecycle(t *testing.T) { dbChainCacheKey := testGenCacheKey(&structs.DiscoveryChainRequest{ Name: "db", EvaluateInDatacenter: "dc1", - EvaluateInNamespace: "default", + EvaluateInNamespace: "", // This is because structs.TestUpstreams uses an opaque config // to override connect timeouts. OverrideConnectTimeout: 1 * time.Second, @@ -147,26 +147,29 @@ func TestManager_BasicLifecycle(t *testing.T) { }) dbHealthCacheKey := testGenCacheKey(&structs.ServiceSpecificRequest{ - Datacenter: "dc1", - QueryOptions: structs.QueryOptions{Token: "my-token", Filter: ""}, - ServiceName: "db", - Connect: true, + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: "my-token", Filter: ""}, + ServiceName: "db", + Connect: true, + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), }) db_v1_HealthCacheKey := testGenCacheKey(&structs.ServiceSpecificRequest{ Datacenter: "dc1", QueryOptions: structs.QueryOptions{Token: "my-token", Filter: "Service.Meta.version == v1", }, - ServiceName: "db", - Connect: true, + ServiceName: "db", + Connect: true, + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), }) db_v2_HealthCacheKey := testGenCacheKey(&structs.ServiceSpecificRequest{ Datacenter: "dc1", QueryOptions: structs.QueryOptions{Token: "my-token", Filter: "Service.Meta.version == v2", }, - ServiceName: "db", - Connect: true, + ServiceName: "db", + Connect: true, + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), }) // Create test cases using some of the common data above. @@ -185,7 +188,7 @@ func TestManager_BasicLifecycle(t *testing.T) { expectSnap: &ConfigSnapshot{ Kind: structs.ServiceKindConnectProxy, Service: webProxy.Service, - ProxyID: webProxy.ID, + ProxyID: webProxy.CompoundServiceID(), Address: webProxy.Address, Port: webProxy.Port, Proxy: webProxy.Proxy, @@ -206,8 +209,8 @@ func TestManager_BasicLifecycle(t *testing.T) { WatchedGatewayEndpoints: map[string]map[string]structs.CheckServiceNodes{ "db": {}, }, - UpstreamEndpoints: map[string]structs.CheckServiceNodes{}, - WatchedServiceChecks: map[string][]structs.CheckType{}, + PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{}, + WatchedServiceChecks: map[structs.ServiceID][]structs.CheckType{}, }, Datacenter: "dc1", }, @@ -229,7 +232,7 @@ func TestManager_BasicLifecycle(t *testing.T) { expectSnap: &ConfigSnapshot{ Kind: structs.ServiceKindConnectProxy, Service: webProxy.Service, - ProxyID: webProxy.ID, + ProxyID: webProxy.CompoundServiceID(), Address: webProxy.Address, Port: webProxy.Port, Proxy: webProxy.Proxy, @@ -251,8 +254,8 @@ func TestManager_BasicLifecycle(t *testing.T) { WatchedGatewayEndpoints: map[string]map[string]structs.CheckServiceNodes{ "db": {}, }, - UpstreamEndpoints: map[string]structs.CheckServiceNodes{}, - WatchedServiceChecks: map[string][]structs.CheckType{}, + PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{}, + WatchedServiceChecks: map[structs.ServiceID][]structs.CheckType{}, }, Datacenter: "dc1", }, @@ -331,7 +334,7 @@ func testManager_BasicLifecycle( }() // BEFORE we register, we should be able to get a watch channel - wCh, cancel := m.Watch(webProxy.ID) + wCh, cancel := m.Watch(webProxy.CompoundServiceID()) defer cancel() // And it should block with nothing sent on it yet @@ -355,7 +358,7 @@ func testManager_BasicLifecycle( assertWatchChanRecvs(t, wCh, expectSnap) // Register a second watcher - wCh2, cancel2 := m.Watch(webProxy.ID) + wCh2, cancel2 := m.Watch(webProxy.CompoundServiceID()) defer cancel2() // New watcher should immediately receive the current state @@ -463,11 +466,11 @@ func TestManager_deliverLatest(t *testing.T) { require.NoError(err) snap1 := &ConfigSnapshot{ - ProxyID: "test-proxy", + ProxyID: structs.NewServiceID("test-proxy", nil), Port: 1111, } snap2 := &ConfigSnapshot{ - ProxyID: "test-proxy", + ProxyID: structs.NewServiceID("test-proxy", nil), Port: 2222, } diff --git a/agent/proxycfg/snapshot.go b/agent/proxycfg/snapshot.go index 5f6ac693ea..1de0b0f3a4 100644 --- a/agent/proxycfg/snapshot.go +++ b/agent/proxycfg/snapshot.go @@ -14,9 +14,9 @@ type configSnapshotConnectProxy struct { WatchedUpstreamEndpoints map[string]map[string]structs.CheckServiceNodes WatchedGateways map[string]map[string]context.CancelFunc WatchedGatewayEndpoints map[string]map[string]structs.CheckServiceNodes - WatchedServiceChecks map[string][]structs.CheckType // TODO: missing garbage collection + WatchedServiceChecks map[structs.ServiceID][]structs.CheckType // TODO: missing garbage collection - UpstreamEndpoints map[string]structs.CheckServiceNodes // DEPRECATED:see:WatchedUpstreamEndpoints + PreparedQueryEndpoints map[string]structs.CheckServiceNodes // DEPRECATED:see:WatchedUpstreamEndpoints } func (c *configSnapshotConnectProxy) IsEmpty() bool { @@ -30,15 +30,15 @@ func (c *configSnapshotConnectProxy) IsEmpty() bool { len(c.WatchedGateways) == 0 && len(c.WatchedGatewayEndpoints) == 0 && len(c.WatchedServiceChecks) == 0 && - len(c.UpstreamEndpoints) == 0 + len(c.PreparedQueryEndpoints) == 0 } type configSnapshotMeshGateway struct { - WatchedServices map[string]context.CancelFunc + WatchedServices map[structs.ServiceID]context.CancelFunc WatchedServicesSet bool WatchedDatacenters map[string]context.CancelFunc - ServiceGroups map[string]structs.CheckServiceNodes - ServiceResolvers map[string]*structs.ServiceResolverConfigEntry + ServiceGroups map[structs.ServiceID]structs.CheckServiceNodes + ServiceResolvers map[structs.ServiceID]*structs.ServiceResolverConfigEntry GatewayGroups map[string]structs.CheckServiceNodes } @@ -60,13 +60,14 @@ func (c *configSnapshotMeshGateway) IsEmpty() bool { type ConfigSnapshot struct { Kind structs.ServiceKind Service string - ProxyID string + ProxyID structs.ServiceID Address string Port int TaggedAddresses map[string]structs.ServiceAddress Proxy structs.ConnectProxyConfig Datacenter string - Roots *structs.IndexedCARoots + + Roots *structs.IndexedCARoots // connect-proxy specific ConnectProxy configSnapshotConnectProxy diff --git a/agent/proxycfg/state.go b/agent/proxycfg/state.go index 9f0d3e4f87..eb43197da6 100644 --- a/agent/proxycfg/state.go +++ b/agent/proxycfg/state.go @@ -50,7 +50,7 @@ type state struct { kind structs.ServiceKind service string - proxyID string + proxyID structs.ServiceID address string port int taggedAddresses map[string]structs.ServiceAddress @@ -92,7 +92,7 @@ func newState(ns *structs.NodeService, token string) (*state, error) { return &state{ kind: ns.Kind, service: ns.Service, - proxyID: ns.ID, + proxyID: ns.CompoundServiceID(), address: ns.Address, port: ns.Port, taggedAddresses: taggedAddresses, @@ -156,10 +156,14 @@ func (s *state) watchMeshGateway(ctx context.Context, dc string, upstreamID stri ServiceKind: structs.ServiceKindMeshGateway, UseServiceKind: true, Source: *s.source, + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), }, "mesh-gateway:"+dc+":"+upstreamID, s.ch) } -func (s *state) watchConnectProxyService(ctx context.Context, correlationId string, service string, dc string, filter string) error { +func (s *state) watchConnectProxyService(ctx context.Context, correlationId string, service string, dc string, filter string, entMeta *structs.EnterpriseMeta) error { + var finalMeta structs.EnterpriseMeta + finalMeta.Merge(entMeta) + return s.cache.Notify(ctx, cachetype.HealthServicesName, &structs.ServiceSpecificRequest{ Datacenter: dc, QueryOptions: structs.QueryOptions{ @@ -171,7 +175,8 @@ func (s *state) watchConnectProxyService(ctx context.Context, correlationId stri // Note that Identifier doesn't type-prefix for service any more as it's // the default and makes metrics and other things much cleaner. It's // simpler for us if we have the type to make things unambiguous. - Source: *s.source, + Source: *s.source, + EnterpriseMeta: finalMeta, }, correlationId, s.ch) } @@ -190,9 +195,10 @@ func (s *state) initWatchesConnectProxy() error { // Watch the leaf cert err = s.cache.Notify(s.ctx, cachetype.ConnectCALeafName, &cachetype.ConnectCALeafRequest{ - Datacenter: s.source.Datacenter, - Token: s.token, - Service: s.proxyCfg.DestinationServiceName, + Datacenter: s.source.Datacenter, + Token: s.token, + Service: s.proxyCfg.DestinationServiceName, + EnterpriseMeta: s.proxyID.EnterpriseMeta, }, leafWatchID, s.ch) if err != nil { return err @@ -206,7 +212,7 @@ func (s *state) initWatchesConnectProxy() error { Type: structs.IntentionMatchDestination, Entries: []structs.IntentionMatchEntry{ { - Namespace: structs.IntentionDefaultNamespace, + Namespace: s.proxyID.NamespaceOrDefault(), Name: s.proxyCfg.DestinationServiceName, }, }, @@ -218,14 +224,15 @@ func (s *state) initWatchesConnectProxy() error { // Watch for service check updates err = s.cache.Notify(s.ctx, cachetype.ServiceHTTPChecksName, &cachetype.ServiceHTTPChecksRequest{ - ServiceID: s.proxyCfg.DestinationServiceID, - }, svcChecksWatchIDPrefix+s.proxyCfg.DestinationServiceID, s.ch) + ServiceID: s.proxyCfg.DestinationServiceID, + EnterpriseMeta: s.proxyID.EnterpriseMeta, + }, svcChecksWatchIDPrefix+structs.ServiceIDString(s.proxyCfg.DestinationServiceID, &s.proxyID.EnterpriseMeta), s.ch) if err != nil { return err } - // TODO(namespaces): pull this from something like s.source.Namespace? - currentNamespace := "default" + // let namespace inference happen server side + currentNamespace := "" // Watch for updates to service endpoints for all upstreams for _, u := range s.proxyCfg.Upstreams { @@ -317,10 +324,11 @@ func (s *state) initWatchesMeshGateway() error { } // Watch for all services - err = s.cache.Notify(s.ctx, cachetype.CatalogListServicesName, &structs.DCSpecificRequest{ - Datacenter: s.source.Datacenter, - QueryOptions: structs.QueryOptions{Token: s.token}, - Source: *s.source, + err = s.cache.Notify(s.ctx, cachetype.CatalogServiceListName, &structs.DCSpecificRequest{ + Datacenter: s.source.Datacenter, + QueryOptions: structs.QueryOptions{Token: s.token}, + Source: *s.source, + EnterpriseMeta: *structs.WildcardEnterpriseMeta(), }, serviceListWatchID, s.ch) if err != nil { @@ -342,9 +350,10 @@ func (s *state) initWatchesMeshGateway() error { // Watch service-resolvers so we can setup service subset clusters err = s.cache.Notify(s.ctx, cachetype.ConfigEntriesName, &structs.ConfigEntryQuery{ - Datacenter: s.source.Datacenter, - QueryOptions: structs.QueryOptions{Token: s.token}, - Kind: structs.ServiceResolver, + Datacenter: s.source.Datacenter, + QueryOptions: structs.QueryOptions{Token: s.token}, + Kind: structs.ServiceResolver, + EnterpriseMeta: *structs.WildcardEnterpriseMeta(), }, serviceResolversWatchID, s.ch) if err != nil { @@ -374,14 +383,15 @@ func (s *state) initialConfigSnapshot() ConfigSnapshot { snap.ConnectProxy.WatchedUpstreamEndpoints = make(map[string]map[string]structs.CheckServiceNodes) snap.ConnectProxy.WatchedGateways = make(map[string]map[string]context.CancelFunc) snap.ConnectProxy.WatchedGatewayEndpoints = make(map[string]map[string]structs.CheckServiceNodes) - snap.ConnectProxy.WatchedServiceChecks = make(map[string][]structs.CheckType) + snap.ConnectProxy.WatchedServiceChecks = make(map[structs.ServiceID][]structs.CheckType) - snap.ConnectProxy.UpstreamEndpoints = make(map[string]structs.CheckServiceNodes) // TODO(rb): deprecated + snap.ConnectProxy.PreparedQueryEndpoints = make(map[string]structs.CheckServiceNodes) // TODO(rb): deprecated case structs.ServiceKindMeshGateway: - snap.MeshGateway.WatchedServices = make(map[string]context.CancelFunc) + snap.MeshGateway.WatchedServices = make(map[structs.ServiceID]context.CancelFunc) snap.MeshGateway.WatchedDatacenters = make(map[string]context.CancelFunc) - snap.MeshGateway.ServiceGroups = make(map[string]structs.CheckServiceNodes) + snap.MeshGateway.ServiceGroups = make(map[structs.ServiceID]structs.CheckServiceNodes) snap.MeshGateway.GatewayGroups = make(map[string]structs.CheckServiceNodes) + snap.MeshGateway.ServiceResolvers = make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry) // there is no need to initialize the map of service resolvers as we // fully rebuild it every time we get updates } @@ -551,28 +561,20 @@ func (s *state) handleUpdateConnectProxy(u cache.UpdateEvent, snap *ConfigSnapsh } snap.ConnectProxy.WatchedGatewayEndpoints[svc][dc] = resp.Nodes - case strings.HasPrefix(u.CorrelationID, "upstream:"+serviceIDPrefix): - resp, ok := u.Result.(*structs.IndexedCheckServiceNodes) - if !ok { - return fmt.Errorf("invalid type for response: %T", u.Result) - } - svc := strings.TrimPrefix(u.CorrelationID, "upstream:"+serviceIDPrefix) - snap.ConnectProxy.UpstreamEndpoints[svc] = resp.Nodes - case strings.HasPrefix(u.CorrelationID, "upstream:"+preparedQueryIDPrefix): resp, ok := u.Result.(*structs.PreparedQueryExecuteResponse) if !ok { return fmt.Errorf("invalid type for response: %T", u.Result) } pq := strings.TrimPrefix(u.CorrelationID, "upstream:") - snap.ConnectProxy.UpstreamEndpoints[pq] = resp.Nodes + snap.ConnectProxy.PreparedQueryEndpoints[pq] = resp.Nodes case strings.HasPrefix(u.CorrelationID, svcChecksWatchIDPrefix): resp, ok := u.Result.([]structs.CheckType) if !ok { return fmt.Errorf("invalid type for service checks response: %T, want: []structs.CheckType", u.Result) } - svcID := strings.TrimPrefix(u.CorrelationID, svcChecksWatchIDPrefix) + svcID := structs.ServiceIDFromString(strings.TrimPrefix(u.CorrelationID, svcChecksWatchIDPrefix)) snap.ConnectProxy.WatchedServiceChecks[svcID] = resp default: @@ -644,6 +646,7 @@ func (s *state) resetWatchesFromChain( target.Service, target.Datacenter, target.Subset.Filter, + target.GetEnterpriseMetadata(), ) if err != nil { cancel() @@ -696,33 +699,37 @@ func (s *state) handleUpdateMeshGateway(u cache.UpdateEvent, snap *ConfigSnapsho } snap.Roots = roots case serviceListWatchID: - services, ok := u.Result.(*structs.IndexedServices) + services, ok := u.Result.(*structs.IndexedServiceList) if !ok { return fmt.Errorf("invalid type for response: %T", u.Result) } - for svcName := range services.Services { - if _, ok := snap.MeshGateway.WatchedServices[svcName]; !ok { + svcMap := make(map[structs.ServiceID]struct{}) + for _, svc := range services.Services { + sid := svc.ToServiceID() + if _, ok := snap.MeshGateway.WatchedServices[sid]; !ok { ctx, cancel := context.WithCancel(s.ctx) err := s.cache.Notify(ctx, cachetype.HealthServicesName, &structs.ServiceSpecificRequest{ - Datacenter: s.source.Datacenter, - QueryOptions: structs.QueryOptions{Token: s.token}, - ServiceName: svcName, - Connect: true, - }, fmt.Sprintf("connect-service:%s", svcName), s.ch) + Datacenter: s.source.Datacenter, + QueryOptions: structs.QueryOptions{Token: s.token}, + ServiceName: svc.Name, + Connect: true, + EnterpriseMeta: sid.EnterpriseMeta, + }, fmt.Sprintf("connect-service:%s", sid.String()), s.ch) if err != nil { - s.logger.Printf("[ERR] mesh-gateway: failed to register watch for connect-service:%s", svcName) + s.logger.Printf("[ERR] mesh-gateway: failed to register watch for connect-service:%s", sid.String()) cancel() return err } - snap.MeshGateway.WatchedServices[svcName] = cancel + snap.MeshGateway.WatchedServices[sid] = cancel + svcMap[sid] = struct{}{} } } - for svcName, cancelFn := range snap.MeshGateway.WatchedServices { - if _, ok := services.Services[svcName]; !ok { - delete(snap.MeshGateway.WatchedServices, svcName) + for sid, cancelFn := range snap.MeshGateway.WatchedServices { + if _, ok := svcMap[sid]; !ok { + delete(snap.MeshGateway.WatchedServices, sid) cancelFn() } } @@ -752,6 +759,7 @@ func (s *state) handleUpdateMeshGateway(u cache.UpdateEvent, snap *ConfigSnapsho ServiceKind: structs.ServiceKindMeshGateway, UseServiceKind: true, Source: *s.source, + EnterpriseMeta: *structs.DefaultEnterpriseMeta(), }, fmt.Sprintf("mesh-gateway:%s", dc), s.ch) if err != nil { @@ -784,10 +792,10 @@ func (s *state) handleUpdateMeshGateway(u cache.UpdateEvent, snap *ConfigSnapsho return fmt.Errorf("invalid type for response: %T", u.Result) } - resolvers := make(map[string]*structs.ServiceResolverConfigEntry) + resolvers := make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry) for _, entry := range configEntries.Entries { if resolver, ok := entry.(*structs.ServiceResolverConfigEntry); ok { - resolvers[resolver.Name] = resolver + resolvers[structs.NewServiceID(resolver.Name, &resolver.EnterpriseMeta)] = resolver } } snap.MeshGateway.ServiceResolvers = resolvers @@ -799,12 +807,12 @@ func (s *state) handleUpdateMeshGateway(u cache.UpdateEvent, snap *ConfigSnapsho return fmt.Errorf("invalid type for response: %T", u.Result) } - svc := strings.TrimPrefix(u.CorrelationID, "connect-service:") + sid := structs.ServiceIDFromString(strings.TrimPrefix(u.CorrelationID, "connect-service:")) if len(resp.Nodes) > 0 { - snap.MeshGateway.ServiceGroups[svc] = resp.Nodes - } else if _, ok := snap.MeshGateway.ServiceGroups[svc]; ok { - delete(snap.MeshGateway.ServiceGroups, svc) + snap.MeshGateway.ServiceGroups[sid] = resp.Nodes + } else if _, ok := snap.MeshGateway.ServiceGroups[sid]; ok { + delete(snap.MeshGateway.ServiceGroups, sid) } case strings.HasPrefix(u.CorrelationID, "mesh-gateway:"): resp, ok := u.Result.(*structs.IndexedCheckServiceNodes) @@ -845,7 +853,7 @@ func (s *state) Changed(ns *structs.NodeService, token string) bool { return true } return ns.Kind != s.kind || - s.proxyID != ns.ID || + s.proxyID != ns.CompoundServiceID() || s.address != ns.Address || s.port != ns.Port || !reflect.DeepEqual(s.proxyCfg, ns.Proxy) || diff --git a/agent/proxycfg/state_test.go b/agent/proxycfg/state_test.go index eb1be81f9b..8296607a3f 100644 --- a/agent/proxycfg/state_test.go +++ b/agent/proxycfg/state_test.go @@ -185,7 +185,7 @@ func genVerifyRootsWatch(expectedDatacenter string) verifyWatchRequest { } func genVerifyListServicesWatch(expectedDatacenter string) verifyWatchRequest { - return genVerifyDCSpecificWatch(cachetype.CatalogListServicesName, expectedDatacenter) + return genVerifyDCSpecificWatch(cachetype.CatalogServiceListName, expectedDatacenter) } func verifyDatacentersWatch(t testing.TB, cacheType string, request cache.Request) { @@ -383,7 +383,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { "discovery-chain:api": genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{ Name: "api", EvaluateInDatacenter: "dc1", - EvaluateInNamespace: "default", + EvaluateInNamespace: "", Datacenter: "dc1", OverrideMeshGateway: structs.MeshGatewayConfig{ Mode: meshGatewayProxyConfigValue, @@ -392,7 +392,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { "discovery-chain:api-failover-remote?dc=dc2": genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{ Name: "api-failover-remote", EvaluateInDatacenter: "dc2", - EvaluateInNamespace: "default", + EvaluateInNamespace: "", Datacenter: "dc1", OverrideMeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, @@ -401,7 +401,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { "discovery-chain:api-failover-local?dc=dc2": genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{ Name: "api-failover-local", EvaluateInDatacenter: "dc2", - EvaluateInNamespace: "default", + EvaluateInNamespace: "", Datacenter: "dc1", OverrideMeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeLocal, @@ -410,7 +410,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { "discovery-chain:api-failover-direct?dc=dc2": genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{ Name: "api-failover-direct", EvaluateInDatacenter: "dc2", - EvaluateInNamespace: "default", + EvaluateInNamespace: "", Datacenter: "dc1", OverrideMeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeNone, @@ -419,7 +419,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { "discovery-chain:api-dc2": genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{ Name: "api-dc2", EvaluateInDatacenter: "dc1", - EvaluateInNamespace: "default", + EvaluateInNamespace: "", Datacenter: "dc1", OverrideMeshGateway: structs.MeshGatewayConfig{ Mode: meshGatewayProxyConfigValue, @@ -506,7 +506,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 5, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) require.Len(t, snap.ConnectProxy.WatchedServiceChecks, 0, "%+v", snap.ConnectProxy.WatchedServiceChecks) - require.Len(t, snap.ConnectProxy.UpstreamEndpoints, 0, "%+v", snap.ConnectProxy.UpstreamEndpoints) + require.Len(t, snap.ConnectProxy.PreparedQueryEndpoints, 0, "%+v", snap.ConnectProxy.PreparedQueryEndpoints) }, } @@ -532,7 +532,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 5, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) require.Len(t, snap.ConnectProxy.WatchedServiceChecks, 0, "%+v", snap.ConnectProxy.WatchedServiceChecks) - require.Len(t, snap.ConnectProxy.UpstreamEndpoints, 0, "%+v", snap.ConnectProxy.UpstreamEndpoints) + require.Len(t, snap.ConnectProxy.PreparedQueryEndpoints, 0, "%+v", snap.ConnectProxy.PreparedQueryEndpoints) }, } @@ -589,8 +589,8 @@ func TestState_WatchesAndUpdates(t *testing.T) { events: []cache.UpdateEvent{ cache.UpdateEvent{ CorrelationID: serviceListWatchID, - Result: &structs.IndexedServices{ - Services: make(structs.Services), + Result: &structs.IndexedServiceList{ + Services: make(structs.ServiceList, 0), }, Err: nil, }, diff --git a/agent/proxycfg/testing.go b/agent/proxycfg/testing.go index 19f85ec942..510caabf7f 100644 --- a/agent/proxycfg/testing.go +++ b/agent/proxycfg/testing.go @@ -571,7 +571,7 @@ func TestConfigSnapshot(t testing.T) *ConfigSnapshot { return &ConfigSnapshot{ Kind: structs.ServiceKindConnectProxy, Service: "web-sidecar-proxy", - ProxyID: "web-sidecar-proxy", + ProxyID: structs.NewServiceID("web-sidecar-proxy", nil), Address: "0.0.0.0", Port: 9999, Proxy: structs.ConnectProxyConfig{ @@ -590,7 +590,7 @@ func TestConfigSnapshot(t testing.T) *ConfigSnapshot { DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{ "db": dbChain, }, - UpstreamEndpoints: map[string]structs.CheckServiceNodes{ + PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{ "prepared_query:geo-cache": TestUpstreamNodes(t), }, WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{ @@ -866,7 +866,7 @@ func testConfigSnapshotDiscoveryChain(t testing.T, variation string, additionalE snap := &ConfigSnapshot{ Kind: structs.ServiceKindConnectProxy, Service: "web-sidecar-proxy", - ProxyID: "web-sidecar-proxy", + ProxyID: structs.NewServiceID("web-sidecar-proxy", nil), Address: "0.0.0.0", Port: 9999, Proxy: structs.ConnectProxyConfig{ @@ -979,7 +979,7 @@ func testConfigSnapshotMeshGateway(t testing.T, populateServices bool) *ConfigSn snap := &ConfigSnapshot{ Kind: structs.ServiceKindMeshGateway, Service: "mesh-gateway", - ProxyID: "mesh-gateway", + ProxyID: structs.NewServiceID("mesh-gateway", nil), Address: "1.2.3.4", Port: 8443, Proxy: structs.ConnectProxyConfig{ @@ -1004,17 +1004,17 @@ func testConfigSnapshotMeshGateway(t testing.T, populateServices bool) *ConfigSn if populateServices { snap.MeshGateway = configSnapshotMeshGateway{ - WatchedServices: map[string]context.CancelFunc{ - "foo": nil, - "bar": nil, + WatchedServices: map[structs.ServiceID]context.CancelFunc{ + structs.NewServiceID("foo", nil): nil, + structs.NewServiceID("bar", nil): nil, }, WatchedServicesSet: true, WatchedDatacenters: map[string]context.CancelFunc{ "dc2": nil, }, - ServiceGroups: map[string]structs.CheckServiceNodes{ - "foo": TestGatewayServiceGroupFooDC1(t), - "bar": TestGatewayServiceGroupBarDC1(t), + ServiceGroups: map[structs.ServiceID]structs.CheckServiceNodes{ + structs.NewServiceID("foo", nil): TestGatewayServiceGroupFooDC1(t), + structs.NewServiceID("bar", nil): TestGatewayServiceGroupBarDC1(t), }, GatewayGroups: map[string]structs.CheckServiceNodes{ "dc2": TestGatewayNodesDC2(t), @@ -1029,7 +1029,7 @@ func TestConfigSnapshotExposeConfig(t testing.T) *ConfigSnapshot { return &ConfigSnapshot{ Kind: structs.ServiceKindConnectProxy, Service: "web-proxy", - ProxyID: "web-proxy", + ProxyID: structs.NewServiceID("web-proxy", nil), Address: "1.2.3.4", Port: 8080, Proxy: structs.ConnectProxyConfig{ diff --git a/agent/service_manager.go b/agent/service_manager.go index 7448576479..8df8281293 100644 --- a/agent/service_manager.go +++ b/agent/service_manager.go @@ -452,7 +452,7 @@ type asyncRegisterRequest struct { func makeConfigRequest(agent *Agent, registration *serviceRegistration) *structs.ServiceConfigRequest { ns := registration.service name := ns.Service - var upstreams []string + var upstreams []structs.ServiceID // Note that only sidecar proxies should even make it here for now although // later that will change to add the condition. @@ -465,16 +465,19 @@ func makeConfigRequest(agent *Agent, registration *serviceRegistration) *structs // learn about their configs. for _, us := range ns.Proxy.Upstreams { if us.DestinationType == "" || us.DestinationType == structs.UpstreamDestTypeService { - upstreams = append(upstreams, us.DestinationName) + sid := us.DestinationID() + sid.EnterpriseMeta.Merge(&ns.EnterpriseMeta) + upstreams = append(upstreams, sid) } } } req := &structs.ServiceConfigRequest{ - Name: name, - Datacenter: agent.config.Datacenter, - QueryOptions: structs.QueryOptions{Token: agent.tokens.AgentToken()}, - Upstreams: upstreams, + Name: name, + Datacenter: agent.config.Datacenter, + QueryOptions: structs.QueryOptions{Token: agent.tokens.AgentToken()}, + UpstreamIDs: upstreams, + EnterpriseMeta: ns.EnterpriseMeta, } if registration.token != "" { req.QueryOptions.Token = registration.token @@ -527,7 +530,7 @@ func (w *serviceConfigWatch) mergeServiceConfig() (*structs.NodeService, error) us.MeshGateway.Mode = ns.Proxy.MeshGateway.Mode } - usCfg, ok := w.defaults.UpstreamConfigs[us.DestinationName] + usCfg, ok := w.defaults.UpstreamIDConfigs.GetUpstreamConfig(us.DestinationID()) if !ok { // No config defaults to merge continue diff --git a/agent/service_manager_test.go b/agent/service_manager_test.go index 285e988105..cb38266018 100644 --- a/agent/service_manager_test.go +++ b/agent/service_manager_test.go @@ -338,9 +338,12 @@ func TestServiceManager_PersistService_API(t *testing.T) { "foo": 1, "protocol": "http", }, - UpstreamConfigs: map[string]map[string]interface{}{ - "redis": map[string]interface{}{ - "protocol": "tcp", + UpstreamIDConfigs: structs.UpstreamConfigs{ + structs.UpstreamConfig{ + Upstream: structs.NewServiceID("redis", nil), + Config: map[string]interface{}{ + "protocol": "tcp", + }, }, }, }, @@ -376,9 +379,12 @@ func TestServiceManager_PersistService_API(t *testing.T) { "foo": 1, "protocol": "http", }, - UpstreamConfigs: map[string]map[string]interface{}{ - "redis": map[string]interface{}{ - "protocol": "tcp", + UpstreamIDConfigs: structs.UpstreamConfigs{ + structs.UpstreamConfig{ + Upstream: structs.NewServiceID("redis", nil), + Config: map[string]interface{}{ + "protocol": "tcp", + }, }, }, }, @@ -549,9 +555,12 @@ func TestServiceManager_PersistService_ConfigFiles(t *testing.T) { "foo": 1, "protocol": "http", }, - UpstreamConfigs: map[string]map[string]interface{}{ - "redis": map[string]interface{}{ - "protocol": "tcp", + UpstreamIDConfigs: structs.UpstreamConfigs{ + structs.UpstreamConfig{ + Upstream: structs.NewServiceID("redis", nil), + Config: map[string]interface{}{ + "protocol": "tcp", + }, }, }, }, diff --git a/agent/structs/config_entry.go b/agent/structs/config_entry.go index 95ab4ee03f..742070983d 100644 --- a/agent/structs/config_entry.go +++ b/agent/structs/config_entry.go @@ -41,6 +41,7 @@ type ConfigEntry interface { CanRead(acl.Authorizer) bool CanWrite(acl.Authorizer) bool + GetEnterpriseMeta() *EnterpriseMeta GetRaftIndex() *RaftIndex } @@ -61,6 +62,7 @@ type ServiceConfigEntry struct { // // Connect ConnectConfiguration + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` RaftIndex } @@ -84,6 +86,8 @@ func (e *ServiceConfigEntry) Normalize() error { e.Kind = ServiceDefaults e.Protocol = strings.ToLower(e.Protocol) + e.EnterpriseMeta.Normalize() + return nil } @@ -91,12 +95,16 @@ func (e *ServiceConfigEntry) Validate() error { return nil } -func (e *ServiceConfigEntry) CanRead(rule acl.Authorizer) bool { - return rule.ServiceRead(e.Name, nil) == acl.Allow +func (e *ServiceConfigEntry) CanRead(authz acl.Authorizer) bool { + var authzContext acl.AuthorizerContext + e.FillAuthzContext(&authzContext) + return authz.ServiceRead(e.Name, &authzContext) == acl.Allow } -func (e *ServiceConfigEntry) CanWrite(rule acl.Authorizer) bool { - return rule.ServiceWrite(e.Name, nil) == acl.Allow +func (e *ServiceConfigEntry) CanWrite(authz acl.Authorizer) bool { + var authzContext acl.AuthorizerContext + e.FillAuthzContext(&authzContext) + return authz.ServiceWrite(e.Name, &authzContext) == acl.Allow } func (e *ServiceConfigEntry) GetRaftIndex() *RaftIndex { @@ -107,6 +115,14 @@ func (e *ServiceConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } +func (e *ServiceConfigEntry) GetEnterpriseMeta() *EnterpriseMeta { + if e == nil { + return nil + } + + return &e.EnterpriseMeta +} + type ConnectConfiguration struct { SidecarProxy bool } @@ -119,6 +135,7 @@ type ProxyConfigEntry struct { MeshGateway MeshGatewayConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"` + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` RaftIndex } @@ -142,6 +159,8 @@ func (e *ProxyConfigEntry) Normalize() error { e.Kind = ProxyDefaults e.Name = ProxyConfigGlobal + e.EnterpriseMeta.Normalize() + return nil } @@ -154,15 +173,17 @@ func (e *ProxyConfigEntry) Validate() error { return fmt.Errorf("invalid name (%q), only %q is supported", e.Name, ProxyConfigGlobal) } - return nil + return e.validateEnterpriseMeta() } -func (e *ProxyConfigEntry) CanRead(rule acl.Authorizer) bool { +func (e *ProxyConfigEntry) CanRead(authz acl.Authorizer) bool { return true } -func (e *ProxyConfigEntry) CanWrite(rule acl.Authorizer) bool { - return rule.OperatorWrite(nil) == acl.Allow +func (e *ProxyConfigEntry) CanWrite(authz acl.Authorizer) bool { + var authzContext acl.AuthorizerContext + e.FillAuthzContext(&authzContext) + return authz.OperatorWrite(&authzContext) == acl.Allow } func (e *ProxyConfigEntry) GetRaftIndex() *RaftIndex { @@ -173,6 +194,14 @@ func (e *ProxyConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } +func (e *ProxyConfigEntry) GetEnterpriseMeta() *EnterpriseMeta { + if e == nil { + return nil + } + + return &e.EnterpriseMeta +} + func (e *ProxyConfigEntry) MarshalBinary() (data []byte, err error) { // We mainly want to implement the BinaryMarshaller interface so that // we can fixup some msgpack types to coerce them into JSON compatible @@ -459,6 +488,7 @@ type ConfigEntryQuery struct { Name string Datacenter string + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` QueryOptions } @@ -496,7 +526,12 @@ func (r *ConfigEntryQuery) CacheInfo() cache.RequestInfo { type ServiceConfigRequest struct { Name string Datacenter string - Upstreams []string + // DEPRECATED + // Upstreams is a list of upstream service names to use for resolving the service config + // UpstreamIDs should be used instead which can encode more than just the name to + // uniquely identify a service. + Upstreams []string + UpstreamIDs []ServiceID EnterpriseMeta `hcl:",squash" mapstructure:",squash"` QueryOptions @@ -538,11 +573,29 @@ func (r *ServiceConfigRequest) CacheInfo() cache.RequestInfo { return info } +type UpstreamConfig struct { + Upstream ServiceID + Config map[string]interface{} +} + +type UpstreamConfigs []UpstreamConfig + +func (configs UpstreamConfigs) GetUpstreamConfig(sid ServiceID) (config map[string]interface{}, found bool) { + for _, usconf := range configs { + if usconf.Upstream.Matches(&sid) { + return usconf.Config, true + } + } + + return nil, false +} + type ServiceConfigResponse struct { - ProxyConfig map[string]interface{} - UpstreamConfigs map[string]map[string]interface{} - MeshGateway MeshGatewayConfig `json:",omitempty"` - Expose ExposeConfig `json:",omitempty"` + ProxyConfig map[string]interface{} + UpstreamConfigs map[string]map[string]interface{} + UpstreamIDConfigs UpstreamConfigs + MeshGateway MeshGatewayConfig `json:",omitempty"` + Expose ExposeConfig `json:",omitempty"` QueryMeta } @@ -597,6 +650,13 @@ func (r *ServiceConfigResponse) UnmarshalBinary(data []byte) error { return err } } + + for k := range r.UpstreamIDConfigs { + r.UpstreamIDConfigs[k].Config, err = lib.MapWalk(r.UpstreamIDConfigs[k].Config) + if err != nil { + return err + } + } return nil } diff --git a/agent/structs/config_entry_discoverychain.go b/agent/structs/config_entry_discoverychain.go index b8861ba318..1830fa375a 100644 --- a/agent/structs/config_entry_discoverychain.go +++ b/agent/structs/config_entry_discoverychain.go @@ -38,6 +38,7 @@ type ServiceRouterConfigEntry struct { // the default service. Routes []ServiceRoute + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` RaftIndex } @@ -60,6 +61,8 @@ func (e *ServiceRouterConfigEntry) Normalize() error { e.Kind = ServiceRouter + e.EnterpriseMeta.Normalize() + for _, route := range e.Routes { if route.Match == nil || route.Match.HTTP == nil { continue @@ -73,6 +76,9 @@ func (e *ServiceRouterConfigEntry) Normalize() error { for j := 0; j < len(httpMatch.Methods); j++ { httpMatch.Methods[j] = strings.ToUpper(httpMatch.Methods[j]) } + if route.Destination != nil && route.Destination.Namespace == "" { + route.Destination.Namespace = e.EnterpriseMeta.NamespaceOrDefault() + } } return nil @@ -194,15 +200,15 @@ func (e *ServiceRouterConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } -func (e *ServiceRouterConfigEntry) ListRelatedServices() []string { - found := make(map[string]struct{}) +func (e *ServiceRouterConfigEntry) ListRelatedServices() []ServiceID { + found := make(map[ServiceID]struct{}) // We always inject a default catch-all route to the same service as the router. - found[e.Name] = struct{}{} + found[NewServiceID(e.Name, &e.EnterpriseMeta)] = struct{}{} for _, route := range e.Routes { if route.Destination != nil && route.Destination.Service != "" { - found[route.Destination.Service] = struct{}{} + found[NewServiceID(route.Destination.Service, route.Destination.GetEnterpriseMeta(&e.EnterpriseMeta))] = struct{}{} } } @@ -210,14 +216,25 @@ func (e *ServiceRouterConfigEntry) ListRelatedServices() []string { return nil } - out := make([]string, 0, len(found)) + out := make([]ServiceID, 0, len(found)) for svc, _ := range found { out = append(out, svc) } - sort.Strings(out) + sort.Slice(out, func(i, j int) bool { + return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) || + out[i].ID < out[j].ID + }) return out } +func (e *ServiceRouterConfigEntry) GetEnterpriseMeta() *EnterpriseMeta { + if e == nil { + return nil + } + + return &e.EnterpriseMeta +} + // ServiceRoute is a single routing rule that routes traffic to the destination // when the match criteria applies. type ServiceRoute struct { @@ -386,6 +403,7 @@ type ServiceSplitterConfigEntry struct { // to the FIRST split. Splits []ServiceSplit + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` RaftIndex } @@ -411,8 +429,13 @@ func (e *ServiceSplitterConfigEntry) Normalize() error { // This slightly massages inputs to enforce that the smallest representable // weight is 1/10000 or .01% + e.EnterpriseMeta.Normalize() + if len(e.Splits) > 0 { for i, split := range e.Splits { + if split.Namespace == "" { + split.Namespace = e.EnterpriseMeta.NamespaceOrDefault() + } e.Splits[i].Weight = NormalizeServiceSplitWeight(split.Weight) } } @@ -493,12 +516,20 @@ func (e *ServiceSplitterConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } -func (e *ServiceSplitterConfigEntry) ListRelatedServices() []string { - found := make(map[string]struct{}) +func (e *ServiceSplitterConfigEntry) GetEnterpriseMeta() *EnterpriseMeta { + if e == nil { + return nil + } + + return &e.EnterpriseMeta +} + +func (e *ServiceSplitterConfigEntry) ListRelatedServices() []ServiceID { + found := make(map[ServiceID]struct{}) for _, split := range e.Splits { if split.Service != "" { - found[split.Service] = struct{}{} + found[NewServiceID(split.Service, split.GetEnterpriseMeta(&e.EnterpriseMeta))] = struct{}{} } } @@ -506,11 +537,14 @@ func (e *ServiceSplitterConfigEntry) ListRelatedServices() []string { return nil } - out := make([]string, 0, len(found)) + out := make([]ServiceID, 0, len(found)) for svc, _ := range found { out = append(out, svc) } - sort.Strings(out) + sort.Slice(out, func(i, j int) bool { + return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) || + out[i].ID < out[j].ID + }) return out } @@ -598,6 +632,7 @@ type ServiceResolverConfigEntry struct { // to this service. ConnectTimeout time.Duration `json:",omitempty"` + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` RaftIndex } @@ -675,6 +710,8 @@ func (e *ServiceResolverConfigEntry) Normalize() error { e.Kind = ServiceResolver + e.EnterpriseMeta.Normalize() + return nil } @@ -782,19 +819,27 @@ func (e *ServiceResolverConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } -func (e *ServiceResolverConfigEntry) ListRelatedServices() []string { - found := make(map[string]struct{}) +func (e *ServiceResolverConfigEntry) GetEnterpriseMeta() *EnterpriseMeta { + if e == nil { + return nil + } + + return &e.EnterpriseMeta +} + +func (e *ServiceResolverConfigEntry) ListRelatedServices() []ServiceID { + found := make(map[ServiceID]struct{}) if e.Redirect != nil { if e.Redirect.Service != "" { - found[e.Redirect.Service] = struct{}{} + found[NewServiceID(e.Redirect.Service, e.Redirect.GetEnterpriseMeta(&e.EnterpriseMeta))] = struct{}{} } } if len(e.Failover) > 0 { for _, failover := range e.Failover { if failover.Service != "" { - found[failover.Service] = struct{}{} + found[NewServiceID(failover.Service, failover.GetEnterpriseMeta(&e.EnterpriseMeta))] = struct{}{} } } } @@ -803,11 +848,14 @@ func (e *ServiceResolverConfigEntry) ListRelatedServices() []string { return nil } - out := make([]string, 0, len(found)) + out := make([]ServiceID, 0, len(found)) for svc, _ := range found { out = append(out, svc) } - sort.Strings(out) + sort.Slice(out, func(i, j int) bool { + return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) || + out[i].ID < out[j].ID + }) return out } @@ -889,28 +937,34 @@ type discoveryChainConfigEntry interface { ConfigEntry // ListRelatedServices returns a list of other names of services referenced // in this config entry. - ListRelatedServices() []string + ListRelatedServices() []ServiceID } -func canReadDiscoveryChain(entry discoveryChainConfigEntry, rule acl.Authorizer) bool { - return rule.ServiceRead(entry.GetName(), nil) == acl.Allow +func canReadDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorizer) bool { + var authzContext acl.AuthorizerContext + entry.GetEnterpriseMeta().FillAuthzContext(&authzContext) + return authz.ServiceRead(entry.GetName(), &authzContext) == acl.Allow } func canWriteDiscoveryChain(entry discoveryChainConfigEntry, rule acl.Authorizer) bool { + var authzContext acl.AuthorizerContext + entry.GetEnterpriseMeta().FillAuthzContext(&authzContext) + name := entry.GetName() - if rule.ServiceWrite(name, nil) != acl.Allow { + if rule.ServiceWrite(name, &authzContext) != acl.Allow { return false } for _, svc := range entry.ListRelatedServices() { - if svc == name { + if svc.ID == name { continue } + svc.FillAuthzContext(&authzContext) // You only need read on related services to redirect traffic flow for // your own service. - if rule.ServiceRead(svc, nil) != acl.Allow { + if rule.ServiceRead(svc.ID, &authzContext) != acl.Allow { return false } } @@ -920,46 +974,46 @@ func canWriteDiscoveryChain(entry discoveryChainConfigEntry, rule acl.Authorizer // DiscoveryChainConfigEntries wraps just the raw cross-referenced config // entries. None of these are defaulted. type DiscoveryChainConfigEntries struct { - Routers map[string]*ServiceRouterConfigEntry - Splitters map[string]*ServiceSplitterConfigEntry - Resolvers map[string]*ServiceResolverConfigEntry - Services map[string]*ServiceConfigEntry + Routers map[ServiceID]*ServiceRouterConfigEntry + Splitters map[ServiceID]*ServiceSplitterConfigEntry + Resolvers map[ServiceID]*ServiceResolverConfigEntry + Services map[ServiceID]*ServiceConfigEntry GlobalProxy *ProxyConfigEntry } func NewDiscoveryChainConfigEntries() *DiscoveryChainConfigEntries { return &DiscoveryChainConfigEntries{ - Routers: make(map[string]*ServiceRouterConfigEntry), - Splitters: make(map[string]*ServiceSplitterConfigEntry), - Resolvers: make(map[string]*ServiceResolverConfigEntry), - Services: make(map[string]*ServiceConfigEntry), + Routers: make(map[ServiceID]*ServiceRouterConfigEntry), + Splitters: make(map[ServiceID]*ServiceSplitterConfigEntry), + Resolvers: make(map[ServiceID]*ServiceResolverConfigEntry), + Services: make(map[ServiceID]*ServiceConfigEntry), } } -func (e *DiscoveryChainConfigEntries) GetRouter(name string) *ServiceRouterConfigEntry { +func (e *DiscoveryChainConfigEntries) GetRouter(sid ServiceID) *ServiceRouterConfigEntry { if e.Routers != nil { - return e.Routers[name] + return e.Routers[sid] } return nil } -func (e *DiscoveryChainConfigEntries) GetSplitter(name string) *ServiceSplitterConfigEntry { +func (e *DiscoveryChainConfigEntries) GetSplitter(sid ServiceID) *ServiceSplitterConfigEntry { if e.Splitters != nil { - return e.Splitters[name] + return e.Splitters[sid] } return nil } -func (e *DiscoveryChainConfigEntries) GetResolver(name string) *ServiceResolverConfigEntry { +func (e *DiscoveryChainConfigEntries) GetResolver(sid ServiceID) *ServiceResolverConfigEntry { if e.Resolvers != nil { - return e.Resolvers[name] + return e.Resolvers[sid] } return nil } -func (e *DiscoveryChainConfigEntries) GetService(name string) *ServiceConfigEntry { +func (e *DiscoveryChainConfigEntries) GetService(sid ServiceID) *ServiceConfigEntry { if e.Services != nil { - return e.Services[name] + return e.Services[sid] } return nil } @@ -967,40 +1021,40 @@ func (e *DiscoveryChainConfigEntries) GetService(name string) *ServiceConfigEntr // AddRouters adds router configs. Convenience function for testing. func (e *DiscoveryChainConfigEntries) AddRouters(entries ...*ServiceRouterConfigEntry) { if e.Routers == nil { - e.Routers = make(map[string]*ServiceRouterConfigEntry) + e.Routers = make(map[ServiceID]*ServiceRouterConfigEntry) } for _, entry := range entries { - e.Routers[entry.Name] = entry + e.Routers[NewServiceID(entry.Name, &entry.EnterpriseMeta)] = entry } } // AddSplitters adds splitter configs. Convenience function for testing. func (e *DiscoveryChainConfigEntries) AddSplitters(entries ...*ServiceSplitterConfigEntry) { if e.Splitters == nil { - e.Splitters = make(map[string]*ServiceSplitterConfigEntry) + e.Splitters = make(map[ServiceID]*ServiceSplitterConfigEntry) } for _, entry := range entries { - e.Splitters[entry.Name] = entry + e.Splitters[NewServiceID(entry.Name, entry.GetEnterpriseMeta())] = entry } } // AddResolvers adds resolver configs. Convenience function for testing. func (e *DiscoveryChainConfigEntries) AddResolvers(entries ...*ServiceResolverConfigEntry) { if e.Resolvers == nil { - e.Resolvers = make(map[string]*ServiceResolverConfigEntry) + e.Resolvers = make(map[ServiceID]*ServiceResolverConfigEntry) } for _, entry := range entries { - e.Resolvers[entry.Name] = entry + e.Resolvers[NewServiceID(entry.Name, entry.GetEnterpriseMeta())] = entry } } // AddServices adds service configs. Convenience function for testing. func (e *DiscoveryChainConfigEntries) AddServices(entries ...*ServiceConfigEntry) { if e.Services == nil { - e.Services = make(map[string]*ServiceConfigEntry) + e.Services = make(map[ServiceID]*ServiceConfigEntry) } for _, entry := range entries { - e.Services[entry.Name] = entry + e.Services[NewServiceID(entry.Name, entry.GetEnterpriseMeta())] = entry } } diff --git a/agent/structs/config_entry_discoverychain_oss.go b/agent/structs/config_entry_discoverychain_oss.go new file mode 100644 index 0000000000..a325c3d341 --- /dev/null +++ b/agent/structs/config_entry_discoverychain_oss.go @@ -0,0 +1,39 @@ +// +build !consulent + +package structs + +// GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from +// fields in the ServiceRouteDestination +func (dest *ServiceRouteDestination) GetEnterpriseMeta(_ *EnterpriseMeta) *EnterpriseMeta { + return DefaultEnterpriseMeta() +} + +// GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from +// fields in the ServiceSplit +func (split *ServiceSplit) GetEnterpriseMeta(_ *EnterpriseMeta) *EnterpriseMeta { + return DefaultEnterpriseMeta() +} + +// GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from +// fields in the ServiceResolverRedirect +func (redir *ServiceResolverRedirect) GetEnterpriseMeta(_ *EnterpriseMeta) *EnterpriseMeta { + return DefaultEnterpriseMeta() +} + +// GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from +// fields in the ServiceResolverFailover +func (failover *ServiceResolverFailover) GetEnterpriseMeta(_ *EnterpriseMeta) *EnterpriseMeta { + return DefaultEnterpriseMeta() +} + +// GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from +// fields in the DiscoveryChainRequest +func (req *DiscoveryChainRequest) GetEnterpriseMeta() *EnterpriseMeta { + return DefaultEnterpriseMeta() +} + +// WithEnterpriseMeta will populate the corresponding fields in the +// DiscoveryChainRequest from the EnterpriseMeta struct +func (req *DiscoveryChainRequest) WithEnterpriseMeta(_ *EnterpriseMeta) { + // do nothing +} diff --git a/agent/structs/config_entry_discoverychain_test.go b/agent/structs/config_entry_discoverychain_test.go index 9ae4aff3c5..d7afa75823 100644 --- a/agent/structs/config_entry_discoverychain_test.go +++ b/agent/structs/config_entry_discoverychain_test.go @@ -67,7 +67,7 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { for _, tc := range []struct { name string entry discoveryChainConfigEntry - expectServices []string + expectServices []ServiceID expectACLs []testACL }{ { @@ -92,7 +92,7 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { Service: "other", }, }, - expectServices: []string{"other"}, + expectServices: []ServiceID{NewServiceID("other", nil)}, expectACLs: []testACL{ defaultDenyCase, readTestCase, @@ -123,7 +123,7 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { }, }, }, - expectServices: []string{"other1", "other2"}, + expectServices: []ServiceID{NewServiceID("other1", nil), NewServiceID("other2", nil)}, expectACLs: []testACL{ defaultDenyCase, readTestCase, @@ -163,7 +163,7 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { {Weight: 50, Service: "c"}, }, }, - expectServices: []string{"a", "b", "c"}, + expectServices: []ServiceID{NewServiceID("a", nil), NewServiceID("b", nil), NewServiceID("c", nil)}, expectACLs: []testACL{ defaultDenyCase, readTestCase, @@ -182,7 +182,7 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { Kind: ServiceRouter, Name: "test", }, - expectServices: []string{"test"}, + expectServices: []ServiceID{NewServiceID("test", nil)}, expectACLs: []testACL{ defaultDenyCase, readTestCase, @@ -213,7 +213,7 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { }, }, }, - expectServices: []string{"bar", "foo", "test"}, + expectServices: []ServiceID{NewServiceID("bar", nil), NewServiceID("foo", nil), NewServiceID("test", nil)}, expectACLs: []testACL{ defaultDenyCase, readTestCase, diff --git a/agent/structs/config_entry_oss.go b/agent/structs/config_entry_oss.go new file mode 100644 index 0000000000..9e313c6e33 --- /dev/null +++ b/agent/structs/config_entry_oss.go @@ -0,0 +1,7 @@ +// +build !consulent + +package structs + +func (e *ProxyConfigEntry) validateEnterpriseMeta() error { + return nil +} diff --git a/agent/structs/connect_proxy_config_oss.go b/agent/structs/connect_proxy_config_oss.go new file mode 100644 index 0000000000..990c235bd7 --- /dev/null +++ b/agent/structs/connect_proxy_config_oss.go @@ -0,0 +1,13 @@ +// +build !consulent + +package structs + +func (us *Upstream) GetEnterpriseMeta() *EnterpriseMeta { + return DefaultEnterpriseMeta() +} + +func (us *Upstream) DestinationID() ServiceID { + return ServiceID{ + ID: us.DestinationName, + } +} diff --git a/agent/structs/discovery_chain.go b/agent/structs/discovery_chain.go index d148515adf..0bddb49e1e 100644 --- a/agent/structs/discovery_chain.go +++ b/agent/structs/discovery_chain.go @@ -238,3 +238,7 @@ func (t *DiscoveryTarget) setID() { func (t *DiscoveryTarget) String() string { return t.ID } + +func (t *DiscoveryTarget) ServiceID() ServiceID { + return NewServiceID(t.Service, t.GetEnterpriseMetadata()) +} diff --git a/agent/structs/discovery_chain_oss.go b/agent/structs/discovery_chain_oss.go new file mode 100644 index 0000000000..dbee83be4f --- /dev/null +++ b/agent/structs/discovery_chain_oss.go @@ -0,0 +1,7 @@ +// +build !consulent + +package structs + +func (t *DiscoveryTarget) GetEnterpriseMetadata() *EnterpriseMeta { + return DefaultEnterpriseMeta() +} diff --git a/agent/structs/structs.go b/agent/structs/structs.go index e3340165e3..da720cb4c4 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -803,13 +803,16 @@ func (s *ServiceNode) ToNodeService() *NodeService { } } -func (s *ServiceNode) CompoundServiceID() ServiceID { - id := s.ServiceID - if id == "" { - id = s.ServiceName +func (sn *ServiceNode) compoundID(preferName bool) ServiceID { + var id string + if sn.ServiceID == "" || (preferName && sn.ServiceName != "") { + id = sn.ServiceName + } else { + id = sn.ServiceID } - entMeta := s.EnterpriseMeta + // copy the ent meta and normalize it + entMeta := sn.EnterpriseMeta entMeta.Normalize() return ServiceID{ @@ -818,6 +821,14 @@ func (s *ServiceNode) CompoundServiceID() ServiceID { } } +func (sn *ServiceNode) CompoundServiceID() ServiceID { + return sn.compoundID(false) +} + +func (sn *ServiceNode) CompoundServiceName() ServiceID { + return sn.compoundID(true) +} + // Weights represent the weight used by DNS for a given status type Weights struct { Passing int @@ -938,11 +949,12 @@ func (ns *NodeService) BestAddress(wan bool) (string, int) { return addr, port } -func (ns *NodeService) CompoundServiceID() ServiceID { - id := ns.ID - - if id == "" { +func (ns *NodeService) compoundID(preferName bool) ServiceID { + var id string + if ns.ID == "" || (preferName && ns.Service != "") { id = ns.Service + } else { + id = ns.ID } // copy the ent meta and normalize it @@ -955,6 +967,14 @@ func (ns *NodeService) CompoundServiceID() ServiceID { } } +func (ns *NodeService) CompoundServiceID() ServiceID { + return ns.compoundID(false) +} + +func (ns *NodeService) CompoundServiceName() ServiceID { + return ns.compoundID(true) +} + // ServiceConnect are the shared Connect settings between all service // definitions from the agent to the state store. type ServiceConnect struct { @@ -1645,6 +1665,22 @@ type IndexedServices struct { QueryMeta } +type ServiceInfo struct { + Name string + EnterpriseMeta +} + +func (si *ServiceInfo) ToServiceID() ServiceID { + return ServiceID{ID: si.Name, EnterpriseMeta: si.EnterpriseMeta} +} + +type ServiceList []ServiceInfo + +type IndexedServiceList struct { + Services ServiceList + QueryMeta +} + type IndexedServiceNodes struct { ServiceNodes ServiceNodes QueryMeta diff --git a/agent/structs/structs_oss.go b/agent/structs/structs_oss.go index 71a58142b0..60b367fdb9 100644 --- a/agent/structs/structs_oss.go +++ b/agent/structs/structs_oss.go @@ -26,6 +26,10 @@ func (m *EnterpriseMeta) Merge(_ *EnterpriseMeta) { // do nothing } +func (m *EnterpriseMeta) MergeNoWildcard(_ *EnterpriseMeta) { + // do nothing +} + func (m *EnterpriseMeta) Matches(_ *EnterpriseMeta) bool { return true } @@ -38,6 +42,10 @@ func (m *EnterpriseMeta) LessThan(_ *EnterpriseMeta) bool { return false } +func (m *EnterpriseMeta) NamespaceOrDefault() string { + return "default" +} + // ReplicationEnterpriseMeta stub func ReplicationEnterpriseMeta() *EnterpriseMeta { return &emptyEnterpriseMeta @@ -81,10 +89,19 @@ func ServiceIDString(id string, _ *EnterpriseMeta) string { return id } +func ParseServiceIDString(input string) (string, *EnterpriseMeta) { + return input, DefaultEnterpriseMeta() +} + func (sid *ServiceID) String() string { return sid.ID } +func ServiceIDFromString(input string) ServiceID { + id, _ := ParseServiceIDString(input) + return ServiceID{ID: id} +} + func (cid *CheckID) String() string { return string(cid.ID) } diff --git a/agent/structs/testing_intention.go b/agent/structs/testing_intention.go index a946a3243c..10991f8841 100644 --- a/agent/structs/testing_intention.go +++ b/agent/structs/testing_intention.go @@ -7,9 +7,9 @@ import ( // TestIntention returns a valid, uninserted (no ID set) intention. func TestIntention(t testing.T) *Intention { return &Intention{ - SourceNS: "eng", + SourceNS: IntentionDefaultNamespace, SourceName: "api", - DestinationNS: "eng", + DestinationNS: IntentionDefaultNamespace, DestinationName: "db", Action: IntentionActionAllow, SourceType: IntentionSourceConsul, diff --git a/agent/xds/clusters.go b/agent/xds/clusters.go index e543800984..fa5106b02b 100644 --- a/agent/xds/clusters.go +++ b/agent/xds/clusters.go @@ -79,9 +79,7 @@ func (s *Server) clustersFromSnapshotConnectProxy(cfgSnap *proxycfg.ConfigSnapsh // Add service health checks to the list of paths to create clusters for if needed if cfgSnap.Proxy.Expose.Checks { - // TODO (namespaces) update with real entmeta - var psid structs.ServiceID - psid.Init(cfgSnap.Proxy.DestinationServiceID, structs.DefaultEnterpriseMeta()) + psid := structs.NewServiceID(cfgSnap.Proxy.DestinationServiceID, &cfgSnap.ProxyID.EnterpriseMeta) for _, check := range s.CheckFetcher.ServiceHTTPBasedChecks(psid) { p, err := parseCheckPath(check) if err != nil { @@ -131,7 +129,7 @@ func (s *Server) clustersFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapsho // generate the per-service clusters for svc, _ := range cfgSnap.MeshGateway.ServiceGroups { - clusterName := connect.ServiceSNI(svc, "", "default", cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) + clusterName := connect.ServiceSNI(svc.ID, "", svc.NamespaceOrDefault(), cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) cluster, err := s.makeMeshGatewayCluster(clusterName, cfgSnap) if err != nil { @@ -143,7 +141,7 @@ func (s *Server) clustersFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapsho // generate the service subset clusters for svc, resolver := range cfgSnap.MeshGateway.ServiceResolvers { for subsetName, _ := range resolver.Subsets { - clusterName := connect.ServiceSNI(svc, subsetName, "default", cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) + clusterName := connect.ServiceSNI(svc.ID, subsetName, svc.NamespaceOrDefault(), cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) cluster, err := s.makeMeshGatewayCluster(clusterName, cfgSnap) if err != nil { diff --git a/agent/xds/clusters_test.go b/agent/xds/clusters_test.go index 11dc7ebe2f..d445c3fdd4 100644 --- a/agent/xds/clusters_test.go +++ b/agent/xds/clusters_test.go @@ -259,8 +259,8 @@ func TestClustersFromSnapshot(t *testing.T) { name: "mesh-gateway-service-subsets", create: proxycfg.TestConfigSnapshotMeshGateway, setup: func(snap *proxycfg.ConfigSnapshot) { - snap.MeshGateway.ServiceResolvers = map[string]*structs.ServiceResolverConfigEntry{ - "bar": &structs.ServiceResolverConfigEntry{ + snap.MeshGateway.ServiceResolvers = map[structs.ServiceID]*structs.ServiceResolverConfigEntry{ + structs.NewServiceID("bar", nil): &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "bar", Subsets: map[string]structs.ServiceResolverSubset{ diff --git a/agent/xds/endpoints.go b/agent/xds/endpoints.go index a8f897a99b..607ccda7f6 100644 --- a/agent/xds/endpoints.go +++ b/agent/xds/endpoints.go @@ -37,7 +37,7 @@ func (s *Server) endpointsFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot, token s // (upstream instances) in the snapshot. func (s *Server) endpointsFromSnapshotConnectProxy(cfgSnap *proxycfg.ConfigSnapshot, token string) ([]proto.Message, error) { resources := make([]proto.Message, 0, - len(cfgSnap.ConnectProxy.UpstreamEndpoints)+len(cfgSnap.ConnectProxy.WatchedUpstreamEndpoints)) + len(cfgSnap.ConnectProxy.PreparedQueryEndpoints)+len(cfgSnap.ConnectProxy.WatchedUpstreamEndpoints)) for _, u := range cfgSnap.Proxy.Upstreams { id := u.Identifier() @@ -56,7 +56,7 @@ func (s *Server) endpointsFromSnapshotConnectProxy(cfgSnap *proxycfg.ConfigSnaps } clusterName := connect.UpstreamSNI(&u, "", dc, cfgSnap.Roots.TrustDomain) - endpoints, ok := cfgSnap.ConnectProxy.UpstreamEndpoints[id] + endpoints, ok := cfgSnap.ConnectProxy.PreparedQueryEndpoints[id] if ok { la := makeLoadAssignment( clusterName, @@ -167,7 +167,7 @@ func (s *Server) endpointsFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapsh // generate the endpoints for the local service groups for svc, endpoints := range cfgSnap.MeshGateway.ServiceGroups { - clusterName := connect.ServiceSNI(svc, "", "default", cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) + clusterName := connect.ServiceSNI(svc.ID, "", svc.NamespaceOrDefault(), cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) la := makeLoadAssignment( clusterName, []loadAssignmentEndpointGroup{ @@ -181,7 +181,7 @@ func (s *Server) endpointsFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapsh // generate the endpoints for the service subsets for svc, resolver := range cfgSnap.MeshGateway.ServiceResolvers { for subsetName, subset := range resolver.Subsets { - clusterName := connect.ServiceSNI(svc, subsetName, "default", cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) + clusterName := connect.ServiceSNI(svc.ID, subsetName, svc.NamespaceOrDefault(), cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain) endpoints := cfgSnap.MeshGateway.ServiceGroups[svc] diff --git a/agent/xds/endpoints_test.go b/agent/xds/endpoints_test.go index 6238a9903e..121619ae8a 100644 --- a/agent/xds/endpoints_test.go +++ b/agent/xds/endpoints_test.go @@ -311,8 +311,8 @@ func Test_endpointsFromSnapshot(t *testing.T) { name: "mesh-gateway-service-subsets", create: proxycfg.TestConfigSnapshotMeshGateway, setup: func(snap *proxycfg.ConfigSnapshot) { - snap.MeshGateway.ServiceResolvers = map[string]*structs.ServiceResolverConfigEntry{ - "bar": &structs.ServiceResolverConfigEntry{ + snap.MeshGateway.ServiceResolvers = map[structs.ServiceID]*structs.ServiceResolverConfigEntry{ + structs.NewServiceID("bar", nil): &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "bar", Subsets: map[string]structs.ServiceResolverSubset{ @@ -325,7 +325,7 @@ func Test_endpointsFromSnapshot(t *testing.T) { }, }, }, - "foo": &structs.ServiceResolverConfigEntry{ + structs.NewServiceID("foo", nil): &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "foo", Subsets: map[string]structs.ServiceResolverSubset{ diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index c07b002f52..09d624b0f1 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -81,9 +81,7 @@ func (s *Server) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg.ConfigSnaps // Add service health checks to the list of paths to create listeners for if needed if cfgSnap.Proxy.Expose.Checks { - // TODO (namespaces) update with real ent meta - var psid structs.ServiceID - psid.Init(cfgSnap.Proxy.DestinationServiceID, structs.DefaultEnterpriseMeta()) + psid := structs.NewServiceID(cfgSnap.Proxy.DestinationServiceID, &cfgSnap.ProxyID.EnterpriseMeta) for _, check := range s.CheckFetcher.ServiceHTTPBasedChecks(psid) { p, err := parseCheckPath(check) if err != nil { diff --git a/agent/xds/server.go b/agent/xds/server.go index f5d48ac0e7..44b7a9c288 100644 --- a/agent/xds/server.go +++ b/agent/xds/server.go @@ -115,7 +115,7 @@ type ConfigFetcher interface { // easier testing without several layers of mocked cache, local state and // proxycfg.Manager. type ConfigManager interface { - Watch(proxyID string) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc) + Watch(proxyID structs.ServiceID) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc) } // Server represents a gRPC server that can handle both XDS and ext_authz @@ -200,7 +200,7 @@ func (s *Server) process(stream ADSStream, reqCh <-chan *envoy.DiscoveryRequest) var ok bool var stateCh <-chan *proxycfg.ConfigSnapshot var watchCancel func() - var proxyID string + var proxyID structs.ServiceID // need to run a small state machine to get through initial authentication. var state = stateInit @@ -254,15 +254,16 @@ func (s *Server) process(stream ADSStream, reqCh <-chan *envoy.DiscoveryRequest) return err } + var authzContext acl.AuthorizerContext switch cfgSnap.Kind { case structs.ServiceKindConnectProxy: - // TODO (namespaces) - pass through a real ent authz ctx - if rule != nil && rule.ServiceWrite(cfgSnap.Proxy.DestinationServiceName, nil) != acl.Allow { + cfgSnap.ProxyID.EnterpriseMeta.FillAuthzContext(&authzContext) + if rule != nil && rule.ServiceWrite(cfgSnap.Proxy.DestinationServiceName, &authzContext) != acl.Allow { return status.Errorf(codes.PermissionDenied, "permission denied") } case structs.ServiceKindMeshGateway: - // TODO (namespaces) - pass through a real ent authz ctx - if rule != nil && rule.ServiceWrite(cfgSnap.Service, nil) != acl.Allow { + cfgSnap.ProxyID.EnterpriseMeta.FillAuthzContext(&authzContext) + if rule != nil && rule.ServiceWrite(cfgSnap.Service, &authzContext) != acl.Allow { return status.Errorf(codes.PermissionDenied, "permission denied") } default: @@ -310,7 +311,7 @@ func (s *Server) process(stream ADSStream, reqCh <-chan *envoy.DiscoveryRequest) continue } // Start authentication process, we need the proxyID - proxyID = req.Node.Id + proxyID = structs.NewServiceID(req.Node.Id, parseEnterpriseMeta(req.Node)) // Start watching config for that proxy stateCh, watchCancel = s.CfgMgr.Watch(proxyID) diff --git a/agent/xds/server_oss.go b/agent/xds/server_oss.go new file mode 100644 index 0000000000..c17988067a --- /dev/null +++ b/agent/xds/server_oss.go @@ -0,0 +1,12 @@ +// +build !consulent + +package xds + +import ( + envoycore "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" + "github.com/hashicorp/consul/agent/structs" +) + +func parseEnterpriseMeta(node *envoycore.Node) *structs.EnterpriseMeta { + return structs.DefaultEnterpriseMeta() +} diff --git a/agent/xds/server_test.go b/agent/xds/server_test.go index ecf8c31232..1069b7d393 100644 --- a/agent/xds/server_test.go +++ b/agent/xds/server_test.go @@ -27,8 +27,8 @@ import ( // testing. It also implements ConnectAuthz to allow control over authorization. type testManager struct { sync.Mutex - chans map[string]chan *proxycfg.ConfigSnapshot - cancels chan string + chans map[structs.ServiceID]chan *proxycfg.ConfigSnapshot + cancels chan structs.ServiceID authz map[string]connectAuthzResult } @@ -41,21 +41,21 @@ type connectAuthzResult struct { func newTestManager(t *testing.T) *testManager { return &testManager{ - chans: map[string]chan *proxycfg.ConfigSnapshot{}, - cancels: make(chan string, 10), + chans: map[structs.ServiceID]chan *proxycfg.ConfigSnapshot{}, + cancels: make(chan structs.ServiceID, 10), authz: make(map[string]connectAuthzResult), } } // RegisterProxy simulates a proxy registration -func (m *testManager) RegisterProxy(t *testing.T, proxyID string) { +func (m *testManager) RegisterProxy(t *testing.T, proxyID structs.ServiceID) { m.Lock() defer m.Unlock() m.chans[proxyID] = make(chan *proxycfg.ConfigSnapshot, 1) } // Deliver simulates a proxy registration -func (m *testManager) DeliverConfig(t *testing.T, proxyID string, cfg *proxycfg.ConfigSnapshot) { +func (m *testManager) DeliverConfig(t *testing.T, proxyID structs.ServiceID, cfg *proxycfg.ConfigSnapshot) { t.Helper() m.Lock() defer m.Unlock() @@ -67,7 +67,7 @@ func (m *testManager) DeliverConfig(t *testing.T, proxyID string, cfg *proxycfg. } // Watch implements ConfigManager -func (m *testManager) Watch(proxyID string) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc) { +func (m *testManager) Watch(proxyID structs.ServiceID) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc) { m.Lock() defer m.Unlock() // ch might be nil but then it will just block forever @@ -81,7 +81,7 @@ func (m *testManager) Watch(proxyID string) (<-chan *proxycfg.ConfigSnapshot, pr // probably won't work if you are running multiple Watches in parallel on // multiple proxyIDS due to timing/ordering issues but I don't think we need to // do that. -func (m *testManager) AssertWatchCancelled(t *testing.T, proxyID string) { +func (m *testManager) AssertWatchCancelled(t *testing.T, proxyID structs.ServiceID) { t.Helper() select { case got := <-m.cancels: @@ -120,13 +120,15 @@ func TestServer_StreamAggregatedResources_BasicProtocol(t *testing.T) { } s.Initialize() + sid := structs.NewServiceID("web-sidecar-proxy", nil) + go func() { err := s.StreamAggregatedResources(envoy.stream) require.NoError(t, err) }() // Register the proxy to create state needed to Watch() on - mgr.RegisterProxy(t, "web-sidecar-proxy") + mgr.RegisterProxy(t, sid) // Send initial cluster discover envoy.SendReq(t, ClusterType, 0, 0) @@ -136,7 +138,7 @@ func TestServer_StreamAggregatedResources_BasicProtocol(t *testing.T) { // Deliver a new snapshot snap := proxycfg.TestConfigSnapshot(t) - mgr.DeliverConfig(t, "web-sidecar-proxy", snap) + mgr.DeliverConfig(t, sid, snap) assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 1, 1)) @@ -179,7 +181,7 @@ func TestServer_StreamAggregatedResources_BasicProtocol(t *testing.T) { // doesn't know _what_ changed. We could do something trivial but let's // simulate a leaf cert expiring and being rotated. snap.ConnectProxy.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0]) - mgr.DeliverConfig(t, "web-sidecar-proxy", snap) + mgr.DeliverConfig(t, sid, snap) // All 3 response that have something to return should return with new version // note that the ordering is not deterministic in general. Trying to make this @@ -223,7 +225,7 @@ func TestServer_StreamAggregatedResources_BasicProtocol(t *testing.T) { // Change config again and make sure it's delivered to everyone! snap.ConnectProxy.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0]) - mgr.DeliverConfig(t, "web-sidecar-proxy", snap) + mgr.DeliverConfig(t, sid, snap) assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 3, 7)) assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 3, 8)) @@ -468,12 +470,13 @@ func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) { errCh <- s.StreamAggregatedResources(envoy.stream) }() + sid := structs.NewServiceID("web-sidecar-proxy", nil) // Register the proxy to create state needed to Watch() on - mgr.RegisterProxy(t, "web-sidecar-proxy") + mgr.RegisterProxy(t, sid) // Deliver a new snapshot snap := proxycfg.TestConfigSnapshot(t) - mgr.DeliverConfig(t, "web-sidecar-proxy", snap) + mgr.DeliverConfig(t, sid, snap) // Send initial listener discover, in real life Envoy always sends cluster // first but it doesn't really matter and listener has a response that @@ -493,7 +496,7 @@ func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) { if tt.wantDenied { require.Error(t, err) require.Contains(t, err.Error(), "permission denied") - mgr.AssertWatchCancelled(t, "web-sidecar-proxy") + mgr.AssertWatchCancelled(t, sid) } else { require.NoError(t, err) } @@ -549,8 +552,9 @@ func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedDuring } } + sid := structs.NewServiceID("web-sidecar-proxy", nil) // Register the proxy to create state needed to Watch() on - mgr.RegisterProxy(t, "web-sidecar-proxy") + mgr.RegisterProxy(t, sid) // Send initial cluster discover (OK) envoy.SendReq(t, ClusterType, 0, 0) @@ -570,7 +574,7 @@ func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedDuring // Deliver a new snapshot snap := proxycfg.TestConfigSnapshot(t) - mgr.DeliverConfig(t, "web-sidecar-proxy", snap) + mgr.DeliverConfig(t, sid, snap) assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1)) @@ -589,7 +593,7 @@ func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedDuring require.Equal(t, codes.Unauthenticated, gerr.Code()) require.Equal(t, "unauthenticated: ACL not found", gerr.Message()) - mgr.AssertWatchCancelled(t, "web-sidecar-proxy") + mgr.AssertWatchCancelled(t, sid) case <-time.After(50 * time.Millisecond): t.Fatalf("timed out waiting for handler to finish") } @@ -640,8 +644,9 @@ func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedInBack } } + sid := structs.NewServiceID("web-sidecar-proxy", nil) // Register the proxy to create state needed to Watch() on - mgr.RegisterProxy(t, "web-sidecar-proxy") + mgr.RegisterProxy(t, sid) // Send initial cluster discover (OK) envoy.SendReq(t, ClusterType, 0, 0) @@ -661,7 +666,7 @@ func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedInBack // Deliver a new snapshot snap := proxycfg.TestConfigSnapshot(t) - mgr.DeliverConfig(t, "web-sidecar-proxy", snap) + mgr.DeliverConfig(t, sid, snap) assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1)) @@ -688,7 +693,7 @@ func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedInBack require.Equal(t, codes.Unauthenticated, gerr.Code()) require.Equal(t, "unauthenticated: ACL not found", gerr.Message()) - mgr.AssertWatchCancelled(t, "web-sidecar-proxy") + mgr.AssertWatchCancelled(t, sid) case <-time.After(200 * time.Millisecond): t.Fatalf("timed out waiting for handler to finish") } diff --git a/api/config_entry.go b/api/config_entry.go index 5c05311be4..ae0d42797e 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -88,6 +88,7 @@ type ExposePath struct { type ServiceConfigEntry struct { Kind string Name string + Namespace string `json:",omitempty"` Protocol string `json:",omitempty"` MeshGateway MeshGatewayConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"` @@ -115,6 +116,7 @@ func (s *ServiceConfigEntry) GetModifyIndex() uint64 { type ProxyConfigEntry struct { Kind string Name string + Namespace string `json:",omitempty"` Config map[string]interface{} `json:",omitempty"` MeshGateway MeshGatewayConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"` diff --git a/api/config_entry_discoverychain.go b/api/config_entry_discoverychain.go index 77acfbddf1..885b78dc91 100644 --- a/api/config_entry_discoverychain.go +++ b/api/config_entry_discoverychain.go @@ -6,8 +6,9 @@ import ( ) type ServiceRouterConfigEntry struct { - Kind string - Name string + Kind string + Name string + Namespace string `json:",omitempty"` Routes []ServiceRoute `json:",omitempty"` @@ -104,8 +105,9 @@ func (e *ServiceRouteDestination) UnmarshalJSON(data []byte) error { } type ServiceSplitterConfigEntry struct { - Kind string - Name string + Kind string + Name string + Namespace string `json:",omitempty"` Splits []ServiceSplit `json:",omitempty"` @@ -126,8 +128,9 @@ type ServiceSplit struct { } type ServiceResolverConfigEntry struct { - Kind string - Name string + Kind string + Name string + Namespace string `json:",omitempty"` DefaultSubset string `json:",omitempty"` Subsets map[string]ServiceResolverSubset `json:",omitempty"` diff --git a/api/config_entry_discoverychain_test.go b/api/config_entry_discoverychain_test.go index a761d03678..f9321d36d4 100644 --- a/api/config_entry_discoverychain_test.go +++ b/api/config_entry_discoverychain_test.go @@ -130,6 +130,7 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { entry: &ServiceResolverConfigEntry{ Kind: ServiceResolver, Name: "test-failover", + Namespace: defaultNamespace, DefaultSubset: "v1", Subsets: map[string]ServiceResolverSubset{ "v1": ServiceResolverSubset{ @@ -144,7 +145,8 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { Datacenters: []string{"dc2"}, }, "v1": ServiceResolverFailover{ - Service: "alternate", + Service: "alternate", + Namespace: defaultNamespace, }, }, ConnectTimeout: 5 * time.Second, @@ -154,12 +156,13 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { { name: "redirect", entry: &ServiceResolverConfigEntry{ - Kind: ServiceResolver, - Name: "test-redirect", + Kind: ServiceResolver, + Name: "test-redirect", + Namespace: defaultNamespace, Redirect: &ServiceResolverRedirect{ Service: "test-failover", ServiceSubset: "v2", - Namespace: "c", + Namespace: defaultNamespace, Datacenter: "d", }, }, @@ -168,19 +171,20 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { { name: "mega splitter", // use one mega object to avoid multiple trips entry: &ServiceSplitterConfigEntry{ - Kind: ServiceSplitter, - Name: "test-split", + Kind: ServiceSplitter, + Name: "test-split", + Namespace: defaultNamespace, Splits: []ServiceSplit{ { Weight: 90, Service: "test-failover", ServiceSubset: "v1", - Namespace: "c", + Namespace: defaultNamespace, }, { Weight: 10, Service: "test-redirect", - Namespace: "z", + Namespace: defaultNamespace, }, }, }, @@ -189,8 +193,9 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { { name: "mega router", // use one mega object to avoid multiple trips entry: &ServiceRouterConfigEntry{ - Kind: ServiceRouter, - Name: "test-route", + Kind: ServiceRouter, + Name: "test-route", + Namespace: defaultNamespace, Routes: []ServiceRoute{ { Match: &ServiceRouteMatch{ @@ -207,7 +212,7 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { Destination: &ServiceRouteDestination{ Service: "test-failover", ServiceSubset: "v2", - Namespace: "sec", + Namespace: defaultNamespace, PrefixRewrite: "/", RequestTimeout: 5 * time.Second, NumRetries: 5, diff --git a/command/config/delete/config_delete.go b/command/config/delete/config_delete.go index 2f6cddbfbf..876092608f 100644 --- a/command/config/delete/config_delete.go +++ b/command/config/delete/config_delete.go @@ -31,6 +31,7 @@ func (c *cmd) init() { c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.NamespaceFlags()) c.help = flags.Usage(help, c.flags) } diff --git a/command/config/list/config_list.go b/command/config/list/config_list.go index 40a7f72fd0..3a6d77c548 100644 --- a/command/config/list/config_list.go +++ b/command/config/list/config_list.go @@ -29,6 +29,7 @@ func (c *cmd) init() { c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.NamespaceFlags()) c.help = flags.Usage(help, c.flags) } diff --git a/command/config/read/config_read.go b/command/config/read/config_read.go index 6047979b30..acba927abe 100644 --- a/command/config/read/config_read.go +++ b/command/config/read/config_read.go @@ -34,6 +34,7 @@ func (c *cmd) init() { c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.NamespaceFlags()) c.help = flags.Usage(help, c.flags) } diff --git a/command/config/write/config_write.go b/command/config/write/config_write.go index def2175f60..a25c47b4c0 100644 --- a/command/config/write/config_write.go +++ b/command/config/write/config_write.go @@ -44,6 +44,7 @@ func (c *cmd) init() { "This is used in combination with the -cas flag.") flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.NamespaceFlags()) c.help = flags.Usage(help, c.flags) } diff --git a/command/connect/envoy/bootstrap_tpl.go b/command/connect/envoy/bootstrap_tpl.go index 3270c0fe82..d646bfe930 100644 --- a/command/connect/envoy/bootstrap_tpl.go +++ b/command/connect/envoy/bootstrap_tpl.go @@ -92,6 +92,10 @@ type BootstrapTplArgs struct { // the bootstrap config. It's format may vary based on Envoy version used. // See https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/trace/v2/trace.proto. TracingConfigJSON string + + // Namespace is the Consul Enterprise Namespace of the proxy service instance as + // registered with the Consul agent. + Namespace string } const bootstrapTemplate = `{ @@ -106,7 +110,10 @@ const bootstrapTemplate = `{ }, "node": { "cluster": "{{ .ProxyCluster }}", - "id": "{{ .ProxyID }}" + "id": "{{ .ProxyID }}", + "metadata": { + "namespace": "{{if ne .Namespace ""}}{{ .Namespace }}{{else}}default{{end}}" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/envoy.go b/command/connect/envoy/envoy.go index a7443e960c..4bc5432ab4 100644 --- a/command/connect/envoy/envoy.go +++ b/command/connect/envoy/envoy.go @@ -131,6 +131,7 @@ func (c *cmd) init() { c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.NamespaceFlags()) c.help = flags.Usage(help, c.flags) } @@ -517,6 +518,7 @@ func (c *cmd) templateArgs() (*BootstrapTplArgs, error) { AdminBindPort: adminPort, Token: httpCfg.Token, LocalAgentClusterName: xds.LocalAgentClusterName, + Namespace: httpCfg.Namespace, }, nil } diff --git a/command/connect/envoy/testdata/access-log-path.golden b/command/connect/envoy/testdata/access-log-path.golden index 741ad87e89..2b8851377c 100644 --- a/command/connect/envoy/testdata/access-log-path.golden +++ b/command/connect/envoy/testdata/access-log-path.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/defaults.golden b/command/connect/envoy/testdata/defaults.golden index 261e6abb28..6d8c7980f6 100644 --- a/command/connect/envoy/testdata/defaults.golden +++ b/command/connect/envoy/testdata/defaults.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/existing-ca-file.golden b/command/connect/envoy/testdata/existing-ca-file.golden index 4f24963e97..9b8b656ea3 100644 --- a/command/connect/envoy/testdata/existing-ca-file.golden +++ b/command/connect/envoy/testdata/existing-ca-file.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/extra_-multiple.golden b/command/connect/envoy/testdata/extra_-multiple.golden index a2f592789f..7268289847 100644 --- a/command/connect/envoy/testdata/extra_-multiple.golden +++ b/command/connect/envoy/testdata/extra_-multiple.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/extra_-single.golden b/command/connect/envoy/testdata/extra_-single.golden index 4fc78113ec..57e660f1b2 100644 --- a/command/connect/envoy/testdata/extra_-single.golden +++ b/command/connect/envoy/testdata/extra_-single.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/grpc-addr-config.golden b/command/connect/envoy/testdata/grpc-addr-config.golden index 43810ee3dd..c58d4d32cd 100644 --- a/command/connect/envoy/testdata/grpc-addr-config.golden +++ b/command/connect/envoy/testdata/grpc-addr-config.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/grpc-addr-env.golden b/command/connect/envoy/testdata/grpc-addr-env.golden index 43810ee3dd..c58d4d32cd 100644 --- a/command/connect/envoy/testdata/grpc-addr-env.golden +++ b/command/connect/envoy/testdata/grpc-addr-env.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/grpc-addr-flag.golden b/command/connect/envoy/testdata/grpc-addr-flag.golden index 43810ee3dd..c58d4d32cd 100644 --- a/command/connect/envoy/testdata/grpc-addr-flag.golden +++ b/command/connect/envoy/testdata/grpc-addr-flag.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/grpc-addr-unix.golden b/command/connect/envoy/testdata/grpc-addr-unix.golden index 01c37ee8d1..76ccbc23ca 100644 --- a/command/connect/envoy/testdata/grpc-addr-unix.golden +++ b/command/connect/envoy/testdata/grpc-addr-unix.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/stats-config-override.golden b/command/connect/envoy/testdata/stats-config-override.golden index 5830b0228f..8b83fbba04 100644 --- a/command/connect/envoy/testdata/stats-config-override.golden +++ b/command/connect/envoy/testdata/stats-config-override.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/token-arg.golden b/command/connect/envoy/testdata/token-arg.golden index 13c803cc73..72693933b9 100644 --- a/command/connect/envoy/testdata/token-arg.golden +++ b/command/connect/envoy/testdata/token-arg.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/token-env.golden b/command/connect/envoy/testdata/token-env.golden index 13c803cc73..72693933b9 100644 --- a/command/connect/envoy/testdata/token-env.golden +++ b/command/connect/envoy/testdata/token-env.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/token-file-arg.golden b/command/connect/envoy/testdata/token-file-arg.golden index 13c803cc73..72693933b9 100644 --- a/command/connect/envoy/testdata/token-file-arg.golden +++ b/command/connect/envoy/testdata/token-file-arg.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/token-file-env.golden b/command/connect/envoy/testdata/token-file-env.golden index 13c803cc73..72693933b9 100644 --- a/command/connect/envoy/testdata/token-file-env.golden +++ b/command/connect/envoy/testdata/token-file-env.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/command/connect/envoy/testdata/zipkin-tracing-config.golden b/command/connect/envoy/testdata/zipkin-tracing-config.golden index fbff4b3e84..74bcab3be0 100644 --- a/command/connect/envoy/testdata/zipkin-tracing-config.golden +++ b/command/connect/envoy/testdata/zipkin-tracing-config.golden @@ -10,7 +10,10 @@ }, "node": { "cluster": "test-proxy", - "id": "test-proxy" + "id": "test-proxy", + "metadata": { + "namespace": "default" + } }, "static_resources": { "clusters": [ diff --git a/test/integration/connect/envoy/Dockerfile-tcpdump b/test/integration/connect/envoy/Dockerfile-tcpdump new file mode 100644 index 0000000000..8b56589523 --- /dev/null +++ b/test/integration/connect/envoy/Dockerfile-tcpdump @@ -0,0 +1,7 @@ +FROM alpine:latest + +RUN apk add --no-cache tcpdump +VOLUME [ "/data" ] + +CMD [ "-w", "/data/all.pcap" ] +ENTRYPOINT [ "/usr/sbin/tcpdump" ] diff --git a/test/integration/connect/envoy/docker-compose.yml b/test/integration/connect/envoy/docker-compose.yml index 635827c627..a89745395f 100644 --- a/test/integration/connect/envoy/docker-compose.yml +++ b/test/integration/connect/envoy/docker-compose.yml @@ -155,6 +155,22 @@ services: - "-redirect-port" - "disabled" network_mode: service:consul-primary + + s3-alt: + depends_on: + - consul-primary + image: "fortio/fortio" + environment: + - "FORTIO_NAME=s3-alt" + command: + - "server" + - "-http-port" + - ":8286" + - "-grpc-port" + - ":8280" + - "-redirect-port" + - "disabled" + network_mode: service:consul-primary s1-sidecar-proxy: depends_on: @@ -302,6 +318,27 @@ services: volumes: - *workdir-volume network_mode: service:consul-primary + + s3-alt-sidecar-proxy: + depends_on: + - consul-primary + image: "envoyproxy/envoy:v${ENVOY_VERSION:-1.8.0}" + command: + - "envoy" + - "-c" + - "/workdir/primary/envoy/s3-alt-bootstrap.json" + - "-l" + - "debug" + # Hot restart breaks since both envoys seem to interact with each other + # despite separate containers that don't share IPC namespace. Not quite + # sure how this happens but may be due to unix socket being in some shared + # location? + - "--disable-hot-restart" + - "--drain-time-s" + - "1" + volumes: + - *workdir-volume + network_mode: service:consul-primary verify-primary: depends_on: @@ -502,7 +539,7 @@ services: volumes: - *workdir-volume network_mode: service:consul-primary - + gateway-secondary: depends_on: - consul-secondary @@ -557,3 +594,35 @@ services: - *workdir-volume network_mode: service:consul-secondary pid: host + + tcpdump-primary: + depends_on: + - consul-primary + build: + context: . + dockerfile: Dockerfile-tcpdump + + # we cant do this in circle but its only here to temporarily enable. + volumes: + - type: bind + source: ./${LOG_DIR} + target: /data + command: -v -i any -w /data/primary.pcap + network_mode: service:consul-primary + + tcpdump-secondary: + depends_on: + - consul-secondary + build: + context: . + dockerfile: Dockerfile-tcpdump + + # we cant do this in circle but its only here to temporarily enable. + volumes: + - type: bind + source: ./${LOG_DIR} + target: /data + command: -v -i any -w /data/secondary.pcap + network_mode: service:consul-secondary + + diff --git a/test/integration/connect/envoy/helpers.bash b/test/integration/connect/envoy/helpers.bash index 700be55f4b..003c327979 100755 --- a/test/integration/connect/envoy/helpers.bash +++ b/test/integration/connect/envoy/helpers.bash @@ -19,7 +19,7 @@ function retry { for ((i=1;i<=$max;i++)) do - if $@ + if "$@" then if test $errtrace -eq 1 then @@ -42,13 +42,13 @@ function retry { function retry_default { set +E ret=0 - retry 5 1 $@ || ret=1 + retry 5 1 "$@" || ret=1 set -E return $ret } function retry_long { - retry 30 1 $@ + retry 30 1 "$@" } function echored { @@ -108,15 +108,16 @@ function assert_proxy_presents_cert_uri { local HOSTPORT=$1 local SERVICENAME=$2 local DC=${3:-primary} + local NS=${4:-default} CERT=$(retry_default get_cert $HOSTPORT) - echo "WANT SERVICE: $SERVICENAME" + echo "WANT SERVICE: ${NS}/${SERVICENAME}" echo "GOT CERT:" echo "$CERT" - echo "$CERT" | grep -Eo "URI:spiffe://([a-zA-Z0-9-]+).consul/ns/default/dc/${DC}/svc/$SERVICENAME" + echo "$CERT" | grep -Eo "URI:spiffe://([a-zA-Z0-9-]+).consul/ns/${NS}/dc/${DC}/svc/$SERVICENAME" } function assert_envoy_version { @@ -290,10 +291,41 @@ function assert_envoy_metric_at_least { fi } +function assert_envoy_aggregate_metric_at_least { + set -eEuo pipefail + local HOSTPORT=$1 + local METRIC=$2 + local EXPECT_COUNT=$3 + + METRICS=$(get_envoy_metrics $HOSTPORT "$METRIC") + + if [ -z "${METRICS}" ] + then + echo "Metric not found" 1>&2 + return 1 + fi + + GOT_COUNT=$(awk '{ sum += $2 } END { print sum }' <<< "$METRICS") + + if [ -z "$GOT_COUNT" ] + then + echo "Couldn't parse metric count" 1>&2 + return 1 + fi + + if [ $EXPECT_COUNT -gt $GOT_COUNT ] + then + echo "$METRIC - expected >= count: $EXPECT_COUNT, actual count: $GOT_COUNT" 1>&2 + return 1 + fi +} + function get_healthy_service_count { local SERVICE_NAME=$1 local DC=$2 - run retry_default curl -s -f "127.0.0.1:8500/v1/health/connect/${SERVICE_NAME}?dc=${DC}&passing" + local NS=$3 + + run retry_default curl -s -f ${HEADERS} "127.0.0.1:8500/v1/health/connect/${SERVICE_NAME}?dc=${DC}&passing&ns=${NS}" [ "$status" -eq 0 ] echo "$output" | jq --raw-output '. | length' } @@ -302,8 +334,9 @@ function assert_service_has_healthy_instances_once { local SERVICE_NAME=$1 local EXPECT_COUNT=$2 local DC=${3:-primary} + local NS=$4 - GOT_COUNT=$(get_healthy_service_count $SERVICE_NAME $DC) + GOT_COUNT=$(get_healthy_service_count "$SERVICE_NAME" "$DC" "$NS") [ "$GOT_COUNT" -eq $EXPECT_COUNT ] } @@ -312,8 +345,9 @@ function assert_service_has_healthy_instances { local SERVICE_NAME=$1 local EXPECT_COUNT=$2 local DC=${3:-primary} + local NS=$4 - run retry_long assert_service_has_healthy_instances_once $SERVICE_NAME $EXPECT_COUNT $DC + run retry_long assert_service_has_healthy_instances_once "$SERVICE_NAME" "$EXPECT_COUNT" "$DC" "$NS" [ "$status" -eq 0 ] } @@ -335,6 +369,20 @@ function docker_curl { docker run -ti --rm --network container:envoy_consul-${DC}_1 --entrypoint curl consul-dev "$@" } +function docker_exec { + if ! docker exec -i "$@" + then + echo "Failed to execute: docker exec -i $@" 1>&2 + return 1 + fi +} + +function docker_consul_exec { + local DC=$1 + shift 1 + docker_exec envoy_consul-${DC}_1 "$@" +} + function get_envoy_pid { local BOOTSTRAP_NAME=$1 local DC=${2:-primary} @@ -417,6 +465,7 @@ function gen_envoy_bootstrap { ADMIN_PORT=$2 DC=${3:-primary} IS_MGW=${4:-0} + EXTRA_ENVOY_BS_ARGS="${5-}" PROXY_ID="$SERVICE" if ! is_set "$IS_MGW" @@ -426,7 +475,7 @@ function gen_envoy_bootstrap { if output=$(docker_consul "$DC" connect envoy -bootstrap \ -proxy-id $PROXY_ID \ - -admin-bind 0.0.0.0:$ADMIN_PORT 2>&1); then + -admin-bind 0.0.0.0:$ADMIN_PORT ${EXTRA_ENVOY_BS_ARGS} 2>&1); then # All OK, write config to file echo "$output" > workdir/${DC}/envoy/$SERVICE-bootstrap.json diff --git a/test/integration/connect/envoy/run-tests.sh b/test/integration/connect/envoy/run-tests.sh index c966e01ec1..8ca3358074 100755 --- a/test/integration/connect/envoy/run-tests.sh +++ b/test/integration/connect/envoy/run-tests.sh @@ -54,11 +54,11 @@ function cleanup { trap cleanup EXIT function command_error { - echo "ERR: command exited with status $1" - echo " command: $2" - echo " line: $3" - echo " function: $4" - echo " called at: $5" + echo "ERR: command exited with status $1" 1>&2 + echo " command: $2" 1>&2 + echo " line: $3" 1>&2 + echo " function: $4" 1>&2 + echo " called at: $5" 1>&2 # printf '%s\n' "${FUNCNAME[@]}" # printf '%s\n' "${BASH_SOURCE[@]}" # printf '%s\n' "${BASH_LINENO[@]}" @@ -84,7 +84,7 @@ function init_workdir { # don't wipe logs between runs as they are already split and we need them to # upload as artifacts later. rm -rf workdir/${DC} - mkdir -p workdir/${DC}/{consul,envoy,bats,statsd} + mkdir -p workdir/${DC}/{consul,envoy,bats,statsd,data} # Reload consul config from defaults cp consul-base-cfg/* workdir/${DC}/consul/ @@ -103,7 +103,12 @@ function init_workdir { find ${CASE_DIR}/${DC} -type f -name '*.hcl' -exec cp -f {} workdir/${DC}/consul \; find ${CASE_DIR}/${DC} -type f -name '*.bats' -exec cp -f {} workdir/${DC}/bats \; fi - + + if test -d "${CASE_DIR}/data" + then + cp -r ${CASE_DIR}/data/* workdir/${DC}/data + fi + return 0 } @@ -131,7 +136,7 @@ function start_services { # Push the state to the shared docker volume (note this is because CircleCI # can't use shared volumes) docker cp workdir/. envoy_workdir_1:/workdir - + # Start containers required if [ ! -z "$REQUIRED_SERVICES" ] ; then docker-compose rm -s -v -f $REQUIRED_SERVICES || true @@ -243,23 +248,28 @@ function runTest { fi fi + echo "Setting up the primary datacenter" pre_service_setup primary if [ $? -ne 0 ] then + echo "Setting up the primary datacenter failed" capture_logs return 1 fi if is_set $REQUIRE_SECONDARY then + echo "Setting up the secondary datacenter" pre_service_setup secondary if [ $? -ne 0 ] then + echo "Setting up the secondary datacenter failed" capture_logs return 1 fi fi + echo "Starting services" start_services if [ $? -ne 0 ] then