diff --git a/.changelog/12399.txt b/.changelog/12399.txt new file mode 100644 index 000000000..10f4f87cf --- /dev/null +++ b/.changelog/12399.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +catalog: Add per-node indexes to reduce watchset firing for unrelated nodes and services. +``` \ No newline at end of file diff --git a/.changelog/13481.txt b/.changelog/13481.txt new file mode 100644 index 000000000..2cfd0da42 --- /dev/null +++ b/.changelog/13481.txt @@ -0,0 +1,4 @@ +```release-note:improvement +command: Add support for enabling TLS in the Envoy Prometheus endpoint via the `consul connect envoy` command. +Adds the `-prometheus-ca-file`, `-prometheus-ca-path`, `-prometheus-cert-file` and `-prometheus-key-file` flags. +``` diff --git a/.circleci/config.yml b/.circleci/config.yml index c22d04fb8..df9a60bde 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -930,21 +930,6 @@ jobs: path: *TEST_RESULTS_DIR - run: *notify-slack-failure - trigger-oss-merge: - docker: - - image: docker.mirror.hashicorp.services/alpine:3.12 - steps: - - run: apk add --no-cache --no-progress curl jq - - run: - name: trigger oss merge - command: | - curl -s -X POST \ - --header "Circle-Token: ${CIRCLECI_API_TOKEN}" \ - --header "Content-Type: application/json" \ - -d '{"build_parameters": {"CIRCLE_JOB": "oss-merge"}}' \ - "https://circleci.com/api/v1.1/project/github/hashicorp/consul-enterprise/tree/${CIRCLE_BRANCH}" | jq -r '.build_url' - - run: *notify-slack-failure - # Run load tests against a commit load-test: docker: @@ -1180,16 +1165,6 @@ workflows: requires: - ember-build-ent - noop - workflow-automation: - unless: << pipeline.parameters.trigger-load-test >> - jobs: - - trigger-oss-merge: - context: team-consul - filters: - branches: - only: - - main - - /release\/\d+\.\d+\.x$/ load-test: when: << pipeline.parameters.trigger-load-test >> diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4c38efc3..3a2ec423b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,6 +232,14 @@ jobs: steps: - uses: actions/checkout@v2 + + # Strip everything but MAJOR.MINOR from the version string and add a `-dev` suffix + # This naming convention will be used ONLY for per-commit dev images + - name: Set docker dev tag + run: | + version="${{ env.version }}" + echo "dev_tag=${version%.*}-dev" >> $GITHUB_ENV + - name: Docker Build (Action) uses: hashicorp/actions-docker-build@v1 with: @@ -242,8 +250,8 @@ jobs: docker.io/hashicorp/${{env.repo}}:${{env.version}} public.ecr.aws/hashicorp/${{env.repo}}:${{env.version}} dev_tags: | - docker.io/hashicorppreview/${{ env.repo }}:${{ env.version }} - docker.io/hashicorppreview/${{ env.repo }}:${{ env.version }}-${{ github.sha }} + docker.io/hashicorppreview/${{ env.repo }}:${{ env.dev_tag }} + docker.io/hashicorppreview/${{ env.repo }}:${{ env.dev_tag }}-${{ github.sha }} smoke_test: .github/scripts/verify_docker.sh v${{ env.version }} build-docker-redhat: diff --git a/CHANGELOG.md b/CHANGELOG.md index 04ac1f240..f9f3c9f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ +## 1.13.0-alpha2 (June 21, 2022) + +IMPROVEMENTS: + +* api: `merge-central-config` query parameter support added to `/catalog/node-services/:node-name` API, to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13450](https://github.com/hashicorp/consul/issues/13450)] +* connect: Update Envoy support matrix to latest patch releases (1.22.2, 1.21.3, 1.20.4, 1.19.5) [[GH-13431](https://github.com/hashicorp/consul/issues/13431)] + +BUG FIXES: + +* ui: Fix incorrect text on certain page empty states [[GH-13409](https://github.com/hashicorp/consul/issues/13409)] + +## 1.13.0-alpha1 (June 15, 2022) + +BREAKING CHANGES: + +* config-entry: Exporting a specific service name across all namespace is invalid. + +FEATURES: + +* acl: It is now possible to login and logout using the gRPC API [[GH-12935](https://github.com/hashicorp/consul/issues/12935)] +* agent: Added information about build date alongside other version information for Consul. Extended /agent/self endpoint and `consul version` commands +to report this. Agent also reports build date in log on startup. [[GH-13357](https://github.com/hashicorp/consul/issues/13357)] +* ca: Leaf certificates can now be obtained via the gRPC API: `Sign` [[GH-12787](https://github.com/hashicorp/consul/issues/12787)] +* checks: add UDP health checks.. [[GH-12722](https://github.com/hashicorp/consul/issues/12722)] +* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-12825](https://github.com/hashicorp/consul/issues/12825)] +* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-1717](https://github.com/hashicorp/consul/issues/1717)] +* grpc: New gRPC service and endpoint to return the list of supported consul dataplane features [[GH-12695](https://github.com/hashicorp/consul/issues/12695)] + +IMPROVEMENTS: + +* api: `merge-central-config` query parameter support added to some catalog and health endpoints to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13001](https://github.com/hashicorp/consul/issues/13001)] +* api: add the ability to specify a path prefix for when consul is behind a reverse proxy or API gateway [[GH-12914](https://github.com/hashicorp/consul/issues/12914)] +* connect: add validation to ensure connect native services have a port or socketpath specified on catalog registration. +This was the only missing piece to ensure all mesh services are validated for a port (or socketpath) specification on catalog registration. [[GH-12881](https://github.com/hashicorp/consul/issues/12881)] +* Support Vault namespaces in Connect CA by adding RootPKINamespace and +IntermediatePKINamespace fields to the config. [[GH-12904](https://github.com/hashicorp/consul/issues/12904)] +* acl: Clarify node/service identities must be lowercase [[GH-12807](https://github.com/hashicorp/consul/issues/12807)] +* connect: Added a `max_inbound_connections` setting to service-defaults for limiting the number of concurrent inbound connections to each service instance. [[GH-13143](https://github.com/hashicorp/consul/issues/13143)] +* dns: Added support for specifying admin partition in node lookups. [[GH-13421](https://github.com/hashicorp/consul/issues/13421)] +* grpc: Add a new ServerDiscovery.WatchServers gRPC endpoint for being notified when the set of ready servers has changed. [[GH-12819](https://github.com/hashicorp/consul/issues/12819)] +* telemetry: Added `consul.raft.thread.main.saturation` and `consul.raft.thread.fsm.saturation` metrics to measure approximate saturation of the Raft goroutines [[GH-12865](https://github.com/hashicorp/consul/issues/12865)] +* telemetry: Added a `consul.server.isLeader` metric to track if a server is a leader or not. [[GH-13304](https://github.com/hashicorp/consul/issues/13304)] +* ui: removed external dependencies for serving UI assets in favor of Go's native embed capabilities [[GH-10996](https://github.com/hashicorp/consul/issues/10996)] +* ui: upgrade ember-composable-helpers to v5.x [[GH-13394](https://github.com/hashicorp/consul/issues/13394)] + +BUG FIXES: + +* acl: Fixed a bug where the ACL down policy wasn't being applied on remote errors from the primary datacenter. [[GH-12885](https://github.com/hashicorp/consul/issues/12885)] +* agent: Fixed a bug in HTTP handlers where URLs were being decoded twice [[GH-13256](https://github.com/hashicorp/consul/issues/13256)] +* deps: Update go-grpc/grpc, resolving connection memory leak [[GH-13051](https://github.com/hashicorp/consul/issues/13051)] +* fix a bug that caused an error when creating `grpc` or `http2` ingress gateway listeners with multiple services [[GH-13127](https://github.com/hashicorp/consul/issues/13127)] +* proxycfg: Fixed a minor bug that would cause configuring a terminating gateway to watch too many service resolvers and waste resources doing filtering. [[GH-13012](https://github.com/hashicorp/consul/issues/13012)] +* raft: upgrade to v1.3.8 which fixes a bug where non cluster member can still be able to participate in an election. [[GH-12844](https://github.com/hashicorp/consul/issues/12844)] +* serf: upgrade serf to v0.9.8 which fixes a bug that crashes Consul when serf keyrings are listed [[GH-13062](https://github.com/hashicorp/consul/issues/13062)] + ## 1.12.2 (June 3, 2022) BUG FIXES: diff --git a/GNUmakefile b/GNUmakefile index 197a3186d..7fa86d234 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -333,12 +333,12 @@ ifeq ("$(GOTAGS)","") @docker tag consul-dev:latest consul:local @docker run --rm -t consul:local consul version @cd ./test/integration/consul-container && \ - go test -v -timeout=30m ./upgrade --target-version local --latest-version latest + go test -v -timeout=30m ./... --target-version local --latest-version latest else @docker tag consul-dev:latest hashicorp/consul-enterprise:local @docker run --rm -t hashicorp/consul-enterprise:local consul version @cd ./test/integration/consul-container && \ - go test -v -timeout=30m ./upgrade --tags $(GOTAGS) --target-version local --latest-version latest + go test -v -timeout=30m ./... --tags $(GOTAGS) --target-version local --latest-version latest endif .PHONY: test-metrics-integ diff --git a/agent/consul/catalog_endpoint.go b/agent/consul/catalog_endpoint.go index 9fe11800b..6508ba220 100644 --- a/agent/consul/catalog_endpoint.go +++ b/agent/consul/catalog_endpoint.go @@ -8,7 +8,7 @@ import ( "github.com/armon/go-metrics" "github.com/armon/go-metrics/prometheus" - bexpr "github.com/hashicorp/go-bexpr" + "github.com/hashicorp/go-bexpr" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-uuid" @@ -1036,6 +1036,7 @@ func (c *Catalog) VirtualIPForService(args *structs.ServiceSpecificRequest, repl } state := c.srv.fsm.State() - *reply, err = state.VirtualIPForService(structs.NewServiceName(args.ServiceName, &args.EnterpriseMeta)) + psn := structs.PeeredServiceName{Peer: args.PeerName, ServiceName: structs.NewServiceName(args.ServiceName, &args.EnterpriseMeta)} + *reply, err = state.VirtualIPForService(psn) return err } diff --git a/agent/consul/fsm/snapshot_oss_test.go b/agent/consul/fsm/snapshot_oss_test.go index 558abf4be..bb81d1627 100644 --- a/agent/consul/fsm/snapshot_oss_test.go +++ b/agent/consul/fsm/snapshot_oss_test.go @@ -451,7 +451,8 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) { Port: 8000, Connect: connectConf, }) - vip, err := fsm.state.VirtualIPForService(structs.NewServiceName("frontend", nil)) + psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName("frontend", nil)} + vip, err := fsm.state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, vip, "240.0.0.1") @@ -462,7 +463,8 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) { Port: 9000, Connect: connectConf, }) - vip, err = fsm.state.VirtualIPForService(structs.NewServiceName("backend", nil)) + psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("backend", nil)} + vip, err = fsm.state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, vip, "240.0.0.2") @@ -476,6 +478,7 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) { // Peerings require.NoError(t, fsm.state.PeeringWrite(31, &pbpeering.Peering{ + ID: "1fabcd52-1d46-49b0-b1d8-71559aee47f5", Name: "baz", })) @@ -591,10 +594,12 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) { require.Equal(t, uint64(25), checks[0].ModifyIndex) // Verify virtual IPs are consistent. - vip, err = fsm2.state.VirtualIPForService(structs.NewServiceName("frontend", nil)) + psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("frontend", nil)} + vip, err = fsm2.state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, vip, "240.0.0.1") - vip, err = fsm2.state.VirtualIPForService(structs.NewServiceName("backend", nil)) + psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("backend", nil)} + vip, err = fsm2.state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, vip, "240.0.0.2") diff --git a/agent/consul/internal_endpoint.go b/agent/consul/internal_endpoint.go index f1fda470f..5ed07524e 100644 --- a/agent/consul/internal_endpoint.go +++ b/agent/consul/internal_endpoint.go @@ -69,18 +69,60 @@ func (m *Internal) NodeDump(args *structs.DCSpecificRequest, &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { - index, dump, err := state.NodeDump(ws, &args.EnterpriseMeta, args.PeerName) - if err != nil { - return err + // we don't support calling this endpoint for a specific peer + if args.PeerName != "" { + return fmt.Errorf("this endpoint does not support specifying a peer: %q", args.PeerName) } - reply.Index, reply.Dump = index, dump + + // this maxIndex will be the max of the NodeDump calls and the PeeringList call + var maxIndex uint64 + // Get data for local nodes + index, dump, err := state.NodeDump(ws, &args.EnterpriseMeta, structs.DefaultPeerKeyword) + if err != nil { + return fmt.Errorf("could not get a node dump for local nodes: %w", err) + } + + if index > maxIndex { + maxIndex = index + } + reply.Dump = dump + + // get a list of all peerings + index, listedPeerings, err := state.PeeringList(ws, args.EnterpriseMeta) + if err != nil { + return fmt.Errorf("could not list peers for node dump %w", err) + } + + if index > maxIndex { + maxIndex = index + } + + // get node dumps for all peerings + for _, p := range listedPeerings { + index, importedDump, err := state.NodeDump(ws, &args.EnterpriseMeta, p.Name) + if err != nil { + return fmt.Errorf("could not get a node dump for peer %q: %w", p.Name, err) + } + reply.ImportedDump = append(reply.ImportedDump, importedDump...) + + if index > maxIndex { + maxIndex = index + } + } + reply.Index = maxIndex raw, err := filter.Execute(reply.Dump) if err != nil { - return err + return fmt.Errorf("could not filter local node dump: %w", err) } reply.Dump = raw.(structs.NodeDump) + importedRaw, err := filter.Execute(reply.ImportedDump) + if err != nil { + return fmt.Errorf("could not filter peer node dump: %w", err) + } + reply.ImportedDump = importedRaw.(structs.NodeDump) + // Note: we filter the results with ACLs *after* applying the user-supplied // bexpr filter, to ensure QueryMeta.ResultsFilteredByACLs does not include // results that would be filtered out even if the user did have permission. @@ -111,13 +153,47 @@ func (m *Internal) ServiceDump(args *structs.ServiceDumpRequest, reply *structs. &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { - // Get, store, and filter nodes - maxIdx, nodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, &args.EnterpriseMeta, args.PeerName) + // we don't support calling this endpoint for a specific peer + if args.PeerName != "" { + return fmt.Errorf("this endpoint does not support specifying a peer: %q", args.PeerName) + } + + // this maxIndex will be the max of the ServiceDump calls and the PeeringList call + var maxIndex uint64 + + // get a local dump for services + index, nodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, &args.EnterpriseMeta, structs.DefaultPeerKeyword) if err != nil { - return err + return fmt.Errorf("could not get a service dump for local nodes: %w", err) + } + + if index > maxIndex { + maxIndex = index } reply.Nodes = nodes + // get a list of all peerings + index, listedPeerings, err := state.PeeringList(ws, args.EnterpriseMeta) + if err != nil { + return fmt.Errorf("could not list peers for service dump %w", err) + } + + if index > maxIndex { + maxIndex = index + } + + for _, p := range listedPeerings { + index, importedNodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, &args.EnterpriseMeta, p.Name) + if err != nil { + return fmt.Errorf("could not get a service dump for peer %q: %w", p.Name, err) + } + + if index > maxIndex { + maxIndex = index + } + reply.ImportedNodes = append(reply.ImportedNodes, importedNodes...) + } + // Get, store, and filter gateway services idx, gatewayServices, err := state.DumpGatewayServices(ws) if err != nil { @@ -125,17 +201,23 @@ func (m *Internal) ServiceDump(args *structs.ServiceDumpRequest, reply *structs. } reply.Gateways = gatewayServices - if idx > maxIdx { - maxIdx = idx + if idx > maxIndex { + maxIndex = idx } - reply.Index = maxIdx + reply.Index = maxIndex raw, err := filter.Execute(reply.Nodes) if err != nil { - return err + return fmt.Errorf("could not filter local service dump: %w", err) } reply.Nodes = raw.(structs.CheckServiceNodes) + importedRaw, err := filter.Execute(reply.ImportedNodes) + if err != nil { + return fmt.Errorf("could not filter peer service dump: %w", err) + } + reply.ImportedNodes = importedRaw.(structs.CheckServiceNodes) + // Note: we filter the results with ACLs *after* applying the user-supplied // bexpr filter, to ensure QueryMeta.ResultsFilteredByACLs does not include // results that would be filtered out even if the user did have permission. diff --git a/agent/consul/internal_endpoint_test.go b/agent/consul/internal_endpoint_test.go index 3737f3a08..d24f08d1b 100644 --- a/agent/consul/internal_endpoint_test.go +++ b/agent/consul/internal_endpoint_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/hashicorp/consul-net-rpc/net/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,6 +18,7 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/lib/stringslice" + "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" @@ -29,56 +31,79 @@ func TestInternal_NodeInfo(t *testing.T) { } t.Parallel() - dir1, s1 := testServer(t) - defer os.RemoveAll(dir1) - defer s1.Shutdown() + _, s1 := testServer(t) codec := rpcClient(t, s1) - defer codec.Close() testrpc.WaitForLeader(t, s1.RPC, "dc1") - arg := structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - ID: "db", - Service: "db", - Tags: []string{"primary"}, + args := []*structs.RegisterRequest{ + { + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "db", + Service: "db", + Tags: []string{"primary"}, + }, + Check: &structs.HealthCheck{ + Name: "db connect", + Status: api.HealthPassing, + ServiceID: "db", + }, }, - Check: &structs.HealthCheck{ - Name: "db connect", - Status: api.HealthPassing, - ServiceID: "db", + { + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.3", + PeerName: "peer1", }, } - var out struct{} - if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out); err != nil { - t.Fatalf("err: %v", err) + + for _, reg := range args { + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", reg, nil) + require.NoError(t, err) } - var out2 structs.IndexedNodeDump - req := structs.NodeSpecificRequest{ - Datacenter: "dc1", - Node: "foo", - } - if err := msgpackrpc.CallWithCodec(codec, "Internal.NodeInfo", &req, &out2); err != nil { - t.Fatalf("err: %v", err) - } + t.Run("get local node", func(t *testing.T) { + var out structs.IndexedNodeDump + req := structs.NodeSpecificRequest{ + Datacenter: "dc1", + Node: "foo", + } + if err := msgpackrpc.CallWithCodec(codec, "Internal.NodeInfo", &req, &out); err != nil { + t.Fatalf("err: %v", err) + } - nodes := out2.Dump - if len(nodes) != 1 { - t.Fatalf("Bad: %v", nodes) - } - if nodes[0].Node != "foo" { - t.Fatalf("Bad: %v", nodes[0]) - } - if !stringslice.Contains(nodes[0].Services[0].Tags, "primary") { - t.Fatalf("Bad: %v", nodes[0]) - } - if nodes[0].Checks[0].Status != api.HealthPassing { - t.Fatalf("Bad: %v", nodes[0]) - } + nodes := out.Dump + if len(nodes) != 1 { + t.Fatalf("Bad: %v", nodes) + } + if nodes[0].Node != "foo" { + t.Fatalf("Bad: %v", nodes[0]) + } + if !stringslice.Contains(nodes[0].Services[0].Tags, "primary") { + t.Fatalf("Bad: %v", nodes[0]) + } + if nodes[0].Checks[0].Status != api.HealthPassing { + t.Fatalf("Bad: %v", nodes[0]) + } + }) + + t.Run("get peered node", func(t *testing.T) { + var out structs.IndexedNodeDump + req := structs.NodeSpecificRequest{ + Datacenter: "dc1", + Node: "foo", + PeerName: "peer1", + } + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeInfo", &req, &out)) + + nodes := out.Dump + require.Equal(t, 1, len(nodes)) + require.Equal(t, "foo", nodes[0].Node) + require.Equal(t, "peer1", nodes[0].PeerName) + }) } func TestInternal_NodeDump(t *testing.T) { @@ -87,53 +112,61 @@ func TestInternal_NodeDump(t *testing.T) { } t.Parallel() - dir1, s1 := testServer(t) - defer os.RemoveAll(dir1) - defer s1.Shutdown() + _, s1 := testServer(t) codec := rpcClient(t, s1) - defer codec.Close() testrpc.WaitForLeader(t, s1.RPC, "dc1") - arg := structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - ID: "db", - Service: "db", - Tags: []string{"primary"}, + args := []*structs.RegisterRequest{ + { + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "db", + Service: "db", + Tags: []string{"primary"}, + }, + Check: &structs.HealthCheck{ + Name: "db connect", + Status: api.HealthPassing, + ServiceID: "db", + }, }, - Check: &structs.HealthCheck{ - Name: "db connect", - Status: api.HealthPassing, - ServiceID: "db", + { + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.2", + Service: &structs.NodeService{ + ID: "db", + Service: "db", + Tags: []string{"replica"}, + }, + Check: &structs.HealthCheck{ + Name: "db connect", + Status: api.HealthWarning, + ServiceID: "db", + }, + }, + { + Datacenter: "dc1", + Node: "foo-peer", + Address: "127.0.0.3", + PeerName: "peer1", }, - } - var out struct{} - if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out); err != nil { - t.Fatalf("err: %v", err) } - arg = structs.RegisterRequest{ - Datacenter: "dc1", - Node: "bar", - Address: "127.0.0.2", - Service: &structs.NodeService{ - ID: "db", - Service: "db", - Tags: []string{"replica"}, - }, - Check: &structs.HealthCheck{ - Name: "db connect", - Status: api.HealthWarning, - ServiceID: "db", - }, - } - if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out); err != nil { - t.Fatalf("err: %v", err) + for _, reg := range args { + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", reg, nil) + require.NoError(t, err) } + err := s1.fsm.State().PeeringWrite(1, &pbpeering.Peering{ + ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + Name: "peer1", + }) + require.NoError(t, err) + var out2 structs.IndexedNodeDump req := structs.DCSpecificRequest{ Datacenter: "dc1", @@ -175,6 +208,10 @@ func TestInternal_NodeDump(t *testing.T) { if !foundFoo || !foundBar { t.Fatalf("missing foo or bar") } + + require.Len(t, out2.ImportedDump, 1) + require.Equal(t, "peer1", out2.ImportedDump[0].PeerName) + require.Equal(t, "foo-peer", out2.ImportedDump[0].Node) } func TestInternal_NodeDump_Filter(t *testing.T) { @@ -183,60 +220,107 @@ func TestInternal_NodeDump_Filter(t *testing.T) { } t.Parallel() - dir1, s1 := testServer(t) - defer os.RemoveAll(dir1) - defer s1.Shutdown() + _, s1 := testServer(t) codec := rpcClient(t, s1) - defer codec.Close() testrpc.WaitForLeader(t, s1.RPC, "dc1") - arg := structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - ID: "db", - Service: "db", - Tags: []string{"primary"}, + args := []*structs.RegisterRequest{ + { + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "db", + Service: "db", + Tags: []string{"primary"}, + }, + Check: &structs.HealthCheck{ + Name: "db connect", + Status: api.HealthPassing, + ServiceID: "db", + }, }, - Check: &structs.HealthCheck{ - Name: "db connect", - Status: api.HealthPassing, - ServiceID: "db", + { + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.2", + Service: &structs.NodeService{ + ID: "db", + Service: "db", + Tags: []string{"replica"}, + }, + Check: &structs.HealthCheck{ + Name: "db connect", + Status: api.HealthWarning, + ServiceID: "db", + }, }, - } - var out struct{} - require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out)) - - arg = structs.RegisterRequest{ - Datacenter: "dc1", - Node: "bar", - Address: "127.0.0.2", - Service: &structs.NodeService{ - ID: "db", - Service: "db", - Tags: []string{"replica"}, - }, - Check: &structs.HealthCheck{ - Name: "db connect", - Status: api.HealthWarning, - ServiceID: "db", + { + Datacenter: "dc1", + Node: "foo-peer", + Address: "127.0.0.3", + PeerName: "peer1", }, } - require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out)) - - var out2 structs.IndexedNodeDump - req := structs.DCSpecificRequest{ - Datacenter: "dc1", - QueryOptions: structs.QueryOptions{Filter: "primary in Services.Tags"}, + for _, reg := range args { + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", reg, nil) + require.NoError(t, err) } - require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req, &out2)) - nodes := out2.Dump - require.Len(t, nodes, 1) - require.Equal(t, "foo", nodes[0].Node) + err := s1.fsm.State().PeeringWrite(1, &pbpeering.Peering{ + ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + Name: "peer1", + }) + require.NoError(t, err) + + t.Run("filter on the local node", func(t *testing.T) { + var out2 structs.IndexedNodeDump + req := structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Filter: "primary in Services.Tags"}, + } + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req, &out2)) + + nodes := out2.Dump + require.Len(t, nodes, 1) + require.Equal(t, "foo", nodes[0].Node) + }) + + t.Run("filter on imported dump", func(t *testing.T) { + var out3 structs.IndexedNodeDump + req2 := structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Filter: "friend in PeerName"}, + } + + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req2, &out3)) + require.Len(t, out3.Dump, 0) + require.Len(t, out3.ImportedDump, 0) + }) + + t.Run("filter look for peer nodes (non local nodes)", func(t *testing.T) { + var out3 structs.IndexedNodeDump + req2 := structs.DCSpecificRequest{ + QueryOptions: structs.QueryOptions{Filter: "PeerName != \"\""}, + } + + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req2, &out3)) + require.Len(t, out3.Dump, 0) + require.Len(t, out3.ImportedDump, 1) + }) + + t.Run("filter look for a specific peer", func(t *testing.T) { + var out3 structs.IndexedNodeDump + req2 := structs.DCSpecificRequest{ + QueryOptions: structs.QueryOptions{Filter: "PeerName == peer1"}, + } + + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req2, &out3)) + require.Len(t, out3.Dump, 0) + require.Len(t, out3.ImportedDump, 1) + }) } func TestInternal_KeyringOperation(t *testing.T) { @@ -1665,6 +1749,89 @@ func TestInternal_GatewayServiceDump_Ingress_ACL(t *testing.T) { require.Equal(t, nodes[0].Checks[0].Status, api.HealthWarning) } +func TestInternal_ServiceDump_Peering(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + _, s1 := testServer(t) + codec := rpcClient(t, s1) + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // prep the cluster with some data we can use in our filters + registerTestCatalogEntries(t, codec) + + doRequest := func(t *testing.T, filter string) structs.IndexedNodesWithGateways { + t.Helper() + args := structs.DCSpecificRequest{ + QueryOptions: structs.QueryOptions{Filter: filter}, + } + + var out structs.IndexedNodesWithGateways + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.ServiceDump", &args, &out)) + + return out + } + + t.Run("No peerings", func(t *testing.T) { + nodes := doRequest(t, "") + // redis (3), web (3), critical (1), warning (1) and consul (1) + require.Len(t, nodes.Nodes, 9) + require.Len(t, nodes.ImportedNodes, 0) + }) + + addPeerService(t, codec) + + err := s1.fsm.State().PeeringWrite(1, &pbpeering.Peering{ + ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + Name: "peer1", + }) + require.NoError(t, err) + + t.Run("peerings", func(t *testing.T) { + nodes := doRequest(t, "") + // redis (3), web (3), critical (1), warning (1) and consul (1) + require.Len(t, nodes.Nodes, 9) + // service (1) + require.Len(t, nodes.ImportedNodes, 1) + }) + + t.Run("peerings w filter", func(t *testing.T) { + nodes := doRequest(t, "Node.PeerName == foo") + require.Len(t, nodes.Nodes, 0) + require.Len(t, nodes.ImportedNodes, 0) + + nodes2 := doRequest(t, "Node.PeerName == peer1") + require.Len(t, nodes2.Nodes, 0) + require.Len(t, nodes2.ImportedNodes, 1) + }) +} + +func addPeerService(t *testing.T, codec rpc.ClientCodec) { + // prep the cluster with some data we can use in our filters + registrations := map[string]*structs.RegisterRequest{ + "Peer node foo with peer service": { + Datacenter: "dc1", + Node: "foo", + ID: types.NodeID("e0155642-135d-4739-9853-a1ee6c9f945b"), + Address: "127.0.0.2", + PeerName: "peer1", + Service: &structs.NodeService{ + Kind: structs.ServiceKindTypical, + ID: "serviceID", + Service: "service", + Port: 1235, + Address: "198.18.1.2", + PeerName: "peer1", + }, + }, + } + + registerTestCatalogEntriesMap(t, codec, registrations) +} + func TestInternal_GatewayIntentions(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") diff --git a/agent/consul/leader_peering_test.go b/agent/consul/leader_peering_test.go index 3e2f6c8ff..169ca833f 100644 --- a/agent/consul/leader_peering_test.go +++ b/agent/consul/leader_peering_test.go @@ -7,13 +7,13 @@ import ( "testing" "time" - "github.com/hashicorp/consul/acl" - "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" @@ -62,6 +62,10 @@ func TestLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T) { _, found := s1.peeringService.StreamStatus(token.PeerID) require.False(t, found) + var ( + s2PeerID = "cc56f0b8-3885-4e78-8d7b-614a0c45712d" + ) + // Bring up s2 and store s1's token so that it attempts to dial. _, s2 := testServerWithConfig(t, func(c *Config) { c.NodeName = "s2.dc2" @@ -73,6 +77,7 @@ func TestLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T) { // Simulate a peering initiation event by writing a peering with data from a peering token. // Eventually the leader in dc2 should dial and connect to the leader in dc1. p := &pbpeering.Peering{ + ID: s2PeerID, Name: "my-peer-s1", PeerID: token.PeerID, PeerCAPems: token.CA, @@ -92,6 +97,7 @@ func TestLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T) { // Delete the peering to trigger the termination sequence. deleted := &pbpeering.Peering{ + ID: s2PeerID, Name: "my-peer-s1", DeletedAt: structs.TimeToProto(time.Now()), } @@ -151,6 +157,11 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) { var token structs.PeeringToken require.NoError(t, json.Unmarshal(tokenJSON, &token)) + var ( + s1PeerID = token.PeerID + s2PeerID = "cc56f0b8-3885-4e78-8d7b-614a0c45712d" + ) + // Bring up s2 and store s1's token so that it attempts to dial. _, s2 := testServerWithConfig(t, func(c *Config) { c.NodeName = "s2.dc2" @@ -162,6 +173,7 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) { // Simulate a peering initiation event by writing a peering with data from a peering token. // Eventually the leader in dc2 should dial and connect to the leader in dc1. p := &pbpeering.Peering{ + ID: s2PeerID, Name: "my-peer-s1", PeerID: token.PeerID, PeerCAPems: token.CA, @@ -181,6 +193,7 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) { // Delete the peering from the server peer to trigger the termination sequence. deleted := &pbpeering.Peering{ + ID: s1PeerID, Name: "my-peer-s2", DeletedAt: structs.TimeToProto(time.Now()), } @@ -216,6 +229,7 @@ func TestLeader_Peering_DeferredDeletion(t *testing.T) { testrpc.WaitForLeader(t, s1.RPC, "dc1") var ( + peerID = "cc56f0b8-3885-4e78-8d7b-614a0c45712d" peerName = "my-peer-s2" defaultMeta = acl.DefaultEnterpriseMeta() lastIdx = uint64(0) @@ -224,6 +238,7 @@ func TestLeader_Peering_DeferredDeletion(t *testing.T) { // Simulate a peering initiation event by writing a peering to the state store. lastIdx++ require.NoError(t, s1.fsm.State().PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: peerID, Name: peerName, })) @@ -233,6 +248,7 @@ func TestLeader_Peering_DeferredDeletion(t *testing.T) { // Mark the peering for deletion to trigger the termination sequence. lastIdx++ require.NoError(t, s1.fsm.State().PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: peerID, Name: peerName, DeletedAt: structs.TimeToProto(time.Now()), })) diff --git a/agent/consul/leader_test.go b/agent/consul/leader_test.go index 7b04518fc..b041d2f92 100644 --- a/agent/consul/leader_test.go +++ b/agent/consul/leader_test.go @@ -2258,7 +2258,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) { }) require.NoError(t, err) - vip, err := state.VirtualIPForService(structs.NewServiceName("api", nil)) + psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName("api", nil)} + vip, err := state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, "", vip) @@ -2287,7 +2288,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) { // Make sure the service referenced in the terminating gateway config doesn't have // a virtual IP yet. - vip, err = state.VirtualIPForService(structs.NewServiceName("bar", nil)) + psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("bar", nil)} + vip, err = state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, "", vip) @@ -2316,8 +2318,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) { }, }) require.NoError(t, err) - - vip, err = state.VirtualIPForService(structs.NewServiceName("api", nil)) + psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("api", nil)} + vip, err = state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, "240.0.0.1", vip) @@ -2345,7 +2347,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) { // Make sure the baz service (only referenced in the config entry so far) // has a virtual IP. - vip, err = state.VirtualIPForService(structs.NewServiceName("baz", nil)) + psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("baz", nil)} + vip, err = state.VirtualIPForService(psn) require.NoError(t, err) require.Equal(t, "240.0.0.2", vip) } diff --git a/agent/consul/peering_backend.go b/agent/consul/peering_backend.go index 0ba3463c8..047569f11 100644 --- a/agent/consul/peering_backend.go +++ b/agent/consul/peering_backend.go @@ -143,6 +143,17 @@ type peeringApply struct { srv *Server } +func (a *peeringApply) CheckPeeringUUID(id string) (bool, error) { + state := a.srv.fsm.State() + if _, existing, err := state.PeeringReadByID(nil, id); err != nil { + return false, err + } else if existing != nil { + return false, nil + } + + return true, nil +} + func (a *peeringApply) PeeringWrite(req *pbpeering.PeeringWriteRequest) error { _, err := a.srv.raftApplyProtobuf(structs.PeeringWriteType, req) return err diff --git a/agent/consul/state/acl_oss.go b/agent/consul/state/acl_oss.go index 67a272c24..686713071 100644 --- a/agent/consul/state/acl_oss.go +++ b/agent/consul/state/acl_oss.go @@ -7,7 +7,7 @@ import ( "fmt" "strings" - memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" @@ -209,18 +209,13 @@ func (s *Store) ACLAuthMethodUpsertValidateEnterprise(method *structs.ACLAuthMet return nil } -func indexAuthMethodFromACLToken(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLToken) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw) - } - - if p.AuthMethod == "" { +func indexAuthMethodFromACLToken(t *structs.ACLToken) ([]byte, error) { + if t.AuthMethod == "" { return nil, errMissingValueForIndex } var b indexBuilder - b.String(strings.ToLower(p.AuthMethod)) + b.String(strings.ToLower(t.AuthMethod)) return b.Bytes(), nil } diff --git a/agent/consul/state/acl_schema.go b/agent/consul/state/acl_schema.go index 5b9529bbd..485b5b92d 100644 --- a/agent/consul/state/acl_schema.go +++ b/agent/consul/state/acl_schema.go @@ -6,7 +6,6 @@ import ( "github.com/hashicorp/go-memdb" - "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" ) @@ -36,18 +35,18 @@ func tokensTableSchema() *memdb.TableSchema { // DEPRECATED (ACL-Legacy-Compat) - we should not AllowMissing here once legacy compat is removed AllowMissing: true, Unique: true, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromUUIDString), - writeIndex: writeIndex(indexAccessorIDFromACLToken), + Indexer: indexerSingle[string, *structs.ACLToken]{ + readIndex: indexFromUUIDString, + writeIndex: indexAccessorIDFromACLToken, }, }, indexID: { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromStringCaseSensitive), - writeIndex: writeIndex(indexSecretIDFromACLToken), + Indexer: indexerSingle[string, *structs.ACLToken]{ + readIndex: indexFromStringCaseSensitive, + writeIndex: indexSecretIDFromACLToken, }, }, indexPolicies: { @@ -55,58 +54,58 @@ func tokensTableSchema() *memdb.TableSchema { // Need to allow missing for the anonymous token AllowMissing: true, Unique: false, - Indexer: indexerMulti{ - readIndex: readIndex(indexFromUUIDQuery), - writeIndexMulti: writeIndexMulti(indexPoliciesFromACLToken), + Indexer: indexerMulti[Query, *structs.ACLToken]{ + readIndex: indexFromUUIDQuery, + writeIndexMulti: indexPoliciesFromACLToken, }, }, indexRoles: { Name: indexRoles, AllowMissing: true, Unique: false, - Indexer: indexerMulti{ - readIndex: readIndex(indexFromUUIDQuery), - writeIndexMulti: writeIndexMulti(indexRolesFromACLToken), + Indexer: indexerMulti[Query, *structs.ACLToken]{ + readIndex: indexFromUUIDQuery, + writeIndexMulti: indexRolesFromACLToken, }, }, indexAuthMethod: { Name: indexAuthMethod, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromAuthMethodQuery), - writeIndex: writeIndex(indexAuthMethodFromACLToken), + Indexer: indexerSingle[AuthMethodQuery, *structs.ACLToken]{ + readIndex: indexFromAuthMethodQuery, + writeIndex: indexAuthMethodFromACLToken, }, }, indexLocality: { Name: indexLocality, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromBoolQuery), - writeIndex: writeIndex(indexLocalFromACLToken), + Indexer: indexerSingle[BoolQuery, *structs.ACLToken]{ + readIndex: indexFromBoolQuery, + writeIndex: indexLocalFromACLToken, }, }, indexExpiresGlobal: { Name: indexExpiresGlobal, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromTimeQuery), - writeIndex: writeIndex(indexExpiresGlobalFromACLToken), + Indexer: indexerSingle[*TimeQuery, *structs.ACLToken]{ + readIndex: indexFromTimeQuery, + writeIndex: indexExpiresGlobalFromACLToken, }, }, indexExpiresLocal: { Name: indexExpiresLocal, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromTimeQuery), - writeIndex: writeIndex(indexExpiresLocalFromACLToken), + Indexer: indexerSingle[*TimeQuery, *structs.ACLToken]{ + readIndex: indexFromTimeQuery, + writeIndex: indexExpiresLocalFromACLToken, }, }, - //DEPRECATED (ACL-Legacy-Compat) - This index is only needed while we support upgrading v1 to v2 acls + // DEPRECATED (ACL-Legacy-Compat) - This index is only needed while we support upgrading v1 to v2 acls // This table indexes all the ACL tokens that do not have an AccessorID // TODO(ACL-Legacy-Compat): remove in phase 2 "needs-upgrade": { @@ -142,7 +141,7 @@ func policiesTableSchema() *memdb.TableSchema { Name: indexName, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[Query, *structs.ACLPolicy, any]{ readIndex: indexFromQuery, writeIndex: indexNameFromACLPolicy, prefixIndex: prefixIndexFromQuery, @@ -152,12 +151,7 @@ func policiesTableSchema() *memdb.TableSchema { } } -func indexNameFromACLPolicy(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLPolicy) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLPolicy index", raw) - } - +func indexNameFromACLPolicy(p *structs.ACLPolicy) ([]byte, error) { if p.Name == "" { return nil, errMissingValueForIndex } @@ -183,7 +177,7 @@ func rolesTableSchema() *memdb.TableSchema { Name: indexName, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[Query, *structs.ACLRole, any]{ readIndex: indexFromQuery, writeIndex: indexNameFromACLRole, prefixIndex: prefixIndexFromQuery, @@ -194,7 +188,7 @@ func rolesTableSchema() *memdb.TableSchema { // Need to allow missing for the anonymous token AllowMissing: true, Unique: false, - Indexer: indexerMulti{ + Indexer: indexerMulti[Query, *structs.ACLRole]{ readIndex: indexFromUUIDQuery, writeIndexMulti: multiIndexPolicyFromACLRole, }, @@ -203,75 +197,43 @@ func rolesTableSchema() *memdb.TableSchema { } } -func indexNameFromACLRole(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLRole) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLRole index", raw) - } - - if p.Name == "" { +func indexNameFromACLRole(r *structs.ACLRole) ([]byte, error) { + if r.Name == "" { return nil, errMissingValueForIndex } var b indexBuilder - b.String(strings.ToLower(p.Name)) + b.String(strings.ToLower(r.Name)) return b.Bytes(), nil } -func indexFromUUIDQuery(raw interface{}) ([]byte, error) { - q, ok := raw.(Query) - if !ok { - return nil, fmt.Errorf("unexpected type %T for UUIDQuery index", raw) - } +func indexFromUUIDQuery(q Query) ([]byte, error) { return uuidStringToBytes(q.Value) } -func prefixIndexFromUUIDQuery(arg interface{}) ([]byte, error) { - switch v := arg.(type) { - case *acl.EnterpriseMeta: - return nil, nil - case acl.EnterpriseMeta: - return nil, nil - case Query: - return variableLengthUUIDStringToBytes(v.Value) +func prefixIndexFromUUIDWithPeerQuery(q Query) ([]byte, error) { + var b indexBuilder + peername := q.PeerOrEmpty() + if peername == "" { + b.String(structs.LocalPeerKeyword) + } else { + b.String(strings.ToLower(peername)) } - - return nil, fmt.Errorf("unexpected type %T for Query prefix index", arg) + uuidBytes, err := variableLengthUUIDStringToBytes(q.Value) + if err != nil { + return nil, err + } + return append(b.Bytes(), uuidBytes...), nil } -func prefixIndexFromUUIDWithPeerQuery(arg interface{}) ([]byte, error) { - switch v := arg.(type) { - case Query: - var b indexBuilder - peername := v.PeerOrEmpty() - if peername == "" { - b.String(structs.LocalPeerKeyword) - } else { - b.String(strings.ToLower(peername)) - } - uuidBytes, err := variableLengthUUIDStringToBytes(v.Value) - if err != nil { - return nil, err - } - return append(b.Bytes(), uuidBytes...), nil - } - - return nil, fmt.Errorf("unexpected type %T for Query prefix index", arg) -} - -func multiIndexPolicyFromACLRole(raw interface{}) ([][]byte, error) { - role, ok := raw.(*structs.ACLRole) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLRole index", raw) - } - - count := len(role.Policies) +func multiIndexPolicyFromACLRole(r *structs.ACLRole) ([][]byte, error) { + count := len(r.Policies) if count == 0 { return nil, errMissingValueForIndex } vals := make([][]byte, 0, count) - for _, link := range role.Policies { + for _, link := range r.Policies { v, err := uuidStringToBytes(link.ID) if err != nil { return nil, err @@ -290,16 +252,16 @@ func bindingRulesTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromUUIDString), - writeIndex: writeIndex(indexIDFromACLBindingRule), + Indexer: indexerSingle[string, *structs.ACLBindingRule]{ + readIndex: indexFromUUIDString, + writeIndex: indexIDFromACLBindingRule, }, }, indexAuthMethod: { Name: indexAuthMethod, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.ACLBindingRule]{ readIndex: indexFromQuery, writeIndex: indexAuthMethodFromACLBindingRule, }, @@ -308,12 +270,8 @@ func bindingRulesTableSchema() *memdb.TableSchema { } } -func indexIDFromACLBindingRule(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLBindingRule) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLBindingRule index", raw) - } - vv, err := uuidStringToBytes(p.ID) +func indexIDFromACLBindingRule(r *structs.ACLBindingRule) ([]byte, error) { + vv, err := uuidStringToBytes(r.ID) if err != nil { return nil, err } @@ -321,27 +279,18 @@ func indexIDFromACLBindingRule(raw interface{}) ([]byte, error) { return vv, err } -func indexAuthMethodFromACLBindingRule(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLBindingRule) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLBindingRule index", raw) - } - - if p.AuthMethod == "" { +func indexAuthMethodFromACLBindingRule(r *structs.ACLBindingRule) ([]byte, error) { + if r.AuthMethod == "" { return nil, errMissingValueForIndex } var b indexBuilder - b.String(strings.ToLower(p.AuthMethod)) + b.String(strings.ToLower(r.AuthMethod)) return b.Bytes(), nil } -func indexFromUUIDString(raw interface{}) ([]byte, error) { - index, ok := raw.(string) - if !ok { - return nil, fmt.Errorf("unexpected type %T for UUID string index", raw) - } - uuid, err := uuidStringToBytes(index) +func indexFromUUIDString(raw string) ([]byte, error) { + uuid, err := uuidStringToBytes(raw) if err != nil { return nil, err } @@ -350,17 +299,12 @@ func indexFromUUIDString(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexAccessorIDFromACLToken(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLToken) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw) - } - - if p.AccessorID == "" { +func indexAccessorIDFromACLToken(t *structs.ACLToken) ([]byte, error) { + if t.AccessorID == "" { return nil, errMissingValueForIndex } - uuid, err := uuidStringToBytes(p.AccessorID) + uuid, err := uuidStringToBytes(t.AccessorID) if err != nil { return nil, err } @@ -369,37 +313,23 @@ func indexAccessorIDFromACLToken(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexSecretIDFromACLToken(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLToken) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw) - } - - if p.SecretID == "" { +func indexSecretIDFromACLToken(t *structs.ACLToken) ([]byte, error) { + if t.SecretID == "" { return nil, errMissingValueForIndex } var b indexBuilder - b.String(p.SecretID) + b.String(t.SecretID) return b.Bytes(), nil } -func indexFromStringCaseSensitive(raw interface{}) ([]byte, error) { - q, ok := raw.(string) - if !ok { - return nil, fmt.Errorf("unexpected type %T for string prefix query", raw) - } - +func indexFromStringCaseSensitive(s string) ([]byte, error) { var b indexBuilder - b.String(q) + b.String(s) return b.Bytes(), nil } -func indexPoliciesFromACLToken(raw interface{}) ([][]byte, error) { - token, ok := raw.(*structs.ACLToken) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw) - } +func indexPoliciesFromACLToken(token *structs.ACLToken) ([][]byte, error) { links := token.Policies numLinks := len(links) @@ -420,11 +350,7 @@ func indexPoliciesFromACLToken(raw interface{}) ([][]byte, error) { return vals, nil } -func indexRolesFromACLToken(raw interface{}) ([][]byte, error) { - token, ok := raw.(*structs.ACLToken) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw) - } +func indexRolesFromACLToken(token *structs.ACLToken) ([][]byte, error) { links := token.Roles numLinks := len(links) @@ -445,63 +371,45 @@ func indexRolesFromACLToken(raw interface{}) ([][]byte, error) { return vals, nil } -func indexFromBoolQuery(raw interface{}) ([]byte, error) { - q, ok := raw.(BoolQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for BoolQuery index", raw) - } +func indexFromBoolQuery(q BoolQuery) ([]byte, error) { var b indexBuilder b.Bool(q.Value) return b.Bytes(), nil } -func indexLocalFromACLToken(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLToken) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLPolicy index", raw) - } - +func indexLocalFromACLToken(token *structs.ACLToken) ([]byte, error) { var b indexBuilder - b.Bool(p.Local) + b.Bool(token.Local) return b.Bytes(), nil } -func indexFromTimeQuery(arg interface{}) ([]byte, error) { - p, ok := arg.(*TimeQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for TimeQuery index", arg) - } - +func indexFromTimeQuery(q *TimeQuery) ([]byte, error) { var b indexBuilder - b.Time(p.Value) + b.Time(q.Value) return b.Bytes(), nil } -func indexExpiresLocalFromACLToken(raw interface{}) ([]byte, error) { - return indexExpiresFromACLToken(raw, true) +func indexExpiresLocalFromACLToken(token *structs.ACLToken) ([]byte, error) { + return indexExpiresFromACLToken(token, true) } -func indexExpiresGlobalFromACLToken(raw interface{}) ([]byte, error) { - return indexExpiresFromACLToken(raw, false) +func indexExpiresGlobalFromACLToken(token *structs.ACLToken) ([]byte, error) { + return indexExpiresFromACLToken(token, false) } -func indexExpiresFromACLToken(raw interface{}, local bool) ([]byte, error) { - p, ok := raw.(*structs.ACLToken) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw) - } - if p.Local != local { +func indexExpiresFromACLToken(t *structs.ACLToken, local bool) ([]byte, error) { + if t.Local != local { return nil, errMissingValueForIndex } - if !p.HasExpirationTime() { + if !t.HasExpirationTime() { return nil, errMissingValueForIndex } - if p.ExpirationTime.Unix() < 0 { - return nil, fmt.Errorf("token expiration time cannot be before the unix epoch: %s", p.ExpirationTime) + if t.ExpirationTime.Unix() < 0 { + return nil, fmt.Errorf("token expiration time cannot be before the unix epoch: %s", t.ExpirationTime) } var b indexBuilder - b.Time(*p.ExpirationTime) + b.Time(*t.ExpirationTime) return b.Bytes(), nil } @@ -513,7 +421,7 @@ func authMethodsTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.ACLAuthMethod]{ readIndex: indexFromQuery, writeIndex: indexNameFromACLAuthMethod, }, @@ -522,17 +430,12 @@ func authMethodsTableSchema() *memdb.TableSchema { } } -func indexNameFromACLAuthMethod(raw interface{}) ([]byte, error) { - p, ok := raw.(*structs.ACLAuthMethod) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ACLAuthMethod index", raw) - } - - if p.Name == "" { +func indexNameFromACLAuthMethod(m *structs.ACLAuthMethod) ([]byte, error) { + if m.Name == "" { return nil, errMissingValueForIndex } var b indexBuilder - b.String(strings.ToLower(p.Name)) + b.String(strings.ToLower(m.Name)) return b.Bytes(), nil } diff --git a/agent/consul/state/acl_test.go b/agent/consul/state/acl_test.go index 358b1dea8..9634bf52f 100644 --- a/agent/consul/state/acl_test.go +++ b/agent/consul/state/acl_test.go @@ -7,14 +7,14 @@ import ( "testing" "time" - memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib" - pbacl "github.com/hashicorp/consul/proto/pbacl" + "github.com/hashicorp/consul/proto/pbacl" ) const ( @@ -3702,18 +3702,18 @@ func TestTokenPoliciesIndex(t *testing.T) { Name: "global", AllowMissing: true, Unique: false, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromTimeQuery), - writeIndex: writeIndex(indexExpiresGlobalFromACLToken), + Indexer: indexerSingle[*TimeQuery, *structs.ACLToken]{ + readIndex: indexFromTimeQuery, + writeIndex: indexExpiresGlobalFromACLToken, }, } localIndex := &memdb.IndexSchema{ Name: "local", AllowMissing: true, Unique: false, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromTimeQuery), - writeIndex: writeIndex(indexExpiresLocalFromACLToken), + Indexer: indexerSingle[*TimeQuery, *structs.ACLToken]{ + readIndex: indexFromTimeQuery, + writeIndex: indexExpiresLocalFromACLToken, }, } schema := &memdb.DBSchema{ diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index d76f87bf6..2777b2fd1 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -7,7 +7,7 @@ import ( "reflect" "strings" - memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-memdb" "github.com/mitchellh/copystructure" "github.com/hashicorp/consul/acl" @@ -17,9 +17,15 @@ import ( "github.com/hashicorp/consul/types" ) -// indexServiceExtinction keeps track of the last raft index when the last instance -// of any service was unregistered. This is used by blocking queries on missing services. -const indexServiceExtinction = "service_last_extinction" +const ( + // indexServiceExtinction keeps track of the last raft index when the last instance + // of any service was unregistered. This is used by blocking queries on missing services. + indexServiceExtinction = "service_last_extinction" + + // indexNodeExtinction keeps track of the last raft index when the last instance + // of any node was unregistered. This is used by blocking queries on missing nodes. + indexNodeExtinction = "node_last_extinction" +) const ( // minUUIDLookupLen is used as a minimum length of a node name required before @@ -414,8 +420,8 @@ func (s *Store) ensureNodeTxn(tx WriteTxn, idx uint64, preserveIndexes bool, nod // We are actually renaming a node, remove its reference first err := s.deleteNodeTxn(tx, idx, n.Node, n.GetEnterpriseMeta(), n.PeerName) if err != nil { - return fmt.Errorf("Error while renaming Node ID: %q (%s) from %s to %s", - node.ID, node.Address, n.Node, node.Node) + return fmt.Errorf("Error while renaming Node ID: %q (%s) from %s to %s: %w", + node.ID, node.Address, n.Node, node.Node, err) } } } else { @@ -764,6 +770,15 @@ func (s *Store) deleteNodeTxn(tx WriteTxn, idx uint64, nodeName string, entMeta return fmt.Errorf("failed updating index: %s", err) } + // Clean up node entry from index table + if err := tx.Delete(tableIndex, &IndexEntry{Key: nodeIndexName(nodeName, entMeta, node.PeerName)}); err != nil { + return fmt.Errorf("failed deleting nodeIndex %q: %w", nodeIndexName(nodeName, entMeta, node.PeerName), err) + } + + if err := catalogUpdateNodeExtinctionIndex(tx, idx, entMeta, node.PeerName); err != nil { + return err + } + if peerName == "" { // Invalidate any sessions for this node. toDelete, err := allNodeSessionsTxn(tx, nodeName, entMeta.PartitionOrDefault()) @@ -857,9 +872,10 @@ func ensureServiceTxn(tx WriteTxn, idx uint64, node string, preserveIndexes bool return fmt.Errorf("failed updating gateway mapping: %s", err) } } - } - if err := upsertKindServiceName(tx, idx, svc.Kind, svc.CompoundServiceName()); err != nil { - return fmt.Errorf("failed to persist service name: %v", err) + // Only upsert KindServiceName if service is local + if err := upsertKindServiceName(tx, idx, svc.Kind, svc.CompoundServiceName()); err != nil { + return fmt.Errorf("failed to persist service name: %v", err) + } } // Update upstream/downstream mappings if it's a connect service @@ -881,7 +897,8 @@ func ensureServiceTxn(tx WriteTxn, idx uint64, node string, preserveIndexes bool } sn := structs.ServiceName{Name: service, EnterpriseMeta: svc.EnterpriseMeta} - vip, err := assignServiceVirtualIP(tx, sn) + psn := structs.PeeredServiceName{Peer: svc.PeerName, ServiceName: sn} + vip, err := assignServiceVirtualIP(tx, psn) if err != nil { return fmt.Errorf("failed updating virtual IP: %s", err) } @@ -961,9 +978,8 @@ func ensureServiceTxn(tx WriteTxn, idx uint64, node string, preserveIndexes bool // assignServiceVirtualIP assigns a virtual IP to the target service and updates // the global virtual IP counter if necessary. -func assignServiceVirtualIP(tx WriteTxn, sn structs.ServiceName) (string, error) { - // TODO(peering): support VIPs - serviceVIP, err := tx.First(tableServiceVirtualIPs, indexID, sn) +func assignServiceVirtualIP(tx WriteTxn, psn structs.PeeredServiceName) (string, error) { + serviceVIP, err := tx.First(tableServiceVirtualIPs, indexID, psn) if err != nil { return "", fmt.Errorf("failed service virtual IP lookup: %s", err) } @@ -1034,7 +1050,7 @@ func assignServiceVirtualIP(tx WriteTxn, sn structs.ServiceName) (string, error) } assignedVIP := ServiceVirtualIP{ - Service: sn, + Service: psn, IP: newEntry.IP, } if err := tx.Insert(tableServiceVirtualIPs, assignedVIP); err != nil { @@ -1683,9 +1699,6 @@ func (s *Store) nodeServices(ws memdb.WatchSet, nodeNameOrID string, entMeta *ac entMeta = structs.DefaultEnterpriseMetaInDefaultPartition() } - // Get the table index. - idx := catalogMaxIndex(tx, entMeta, peerName, false) - // Query the node by node name watchCh, n, err := tx.FirstWatch(tableNodes, indexID, Query{ Value: nodeNameOrID, @@ -1712,16 +1725,16 @@ func (s *Store) nodeServices(ws memdb.WatchSet, nodeNameOrID string, entMeta *ac }) if err != nil { ws.Add(watchCh) - // TODO(sean@): We could/should log an error re: the uuid_prefix lookup - // failing once a logger has been introduced to the catalog. - return true, 0, nil, nil, nil + idx := catalogNodeLastExtinctionIndex(tx, entMeta, peerName) + return true, idx, nil, nil, nil } n = iter.Next() if n == nil { // No nodes matched, even with the Node ID: add a watch on the node name. ws.Add(watchCh) - return true, 0, nil, nil, nil + idx := catalogNodeLastExtinctionIndex(tx, entMeta, peerName) + return true, idx, nil, nil, nil } idWatchCh := iter.WatchCh() @@ -1745,6 +1758,9 @@ func (s *Store) nodeServices(ws memdb.WatchSet, nodeNameOrID string, entMeta *ac } ws.Add(services.WatchCh()) + // Get the table index. + idx := catalogNodeMaxIndex(tx, nodeName, entMeta, peerName) + return false, idx, node, services, nil } @@ -1862,10 +1878,6 @@ func (s *Store) deleteServiceTxn(tx WriteTxn, idx uint64, nodeName, serviceID st return nil } - // TODO: accept a non-pointer value for EnterpriseMeta - if entMeta == nil { - entMeta = structs.DefaultEnterpriseMetaInDefaultPartition() - } // Delete any checks associated with the service. This will invalidate // sessions as necessary. nsq := NodeServiceQuery{ @@ -1902,10 +1914,17 @@ func (s *Store) deleteServiceTxn(tx WriteTxn, idx uint64, nodeName, serviceID st svc := service.(*structs.ServiceNode) if err := catalogUpdateServicesIndexes(tx, idx, entMeta, svc.PeerName); err != nil { - return err + return fmt.Errorf("failed updating services indexes: %w", err) } if err := catalogUpdateServiceKindIndexes(tx, idx, svc.ServiceKind, &svc.EnterpriseMeta, svc.PeerName); err != nil { - return err + return fmt.Errorf("failed updating service-kind indexes: %w", err) + } + // Update the node indexes as the service information is included in node catalog queries. + if err := catalogUpdateNodesIndexes(tx, idx, entMeta, peerName); err != nil { + return fmt.Errorf("failed updating nodes indexes: %w", err) + } + if err := catalogUpdateNodeIndexes(tx, idx, nodeName, entMeta, peerName); err != nil { + return fmt.Errorf("failed updating node indexes: %w", err) } name := svc.CompoundServiceName() @@ -1930,7 +1949,7 @@ func (s *Store) deleteServiceTxn(tx WriteTxn, idx uint64, nodeName, serviceID st _, serviceIndex, err := catalogServiceMaxIndex(tx, svc.ServiceName, entMeta, svc.PeerName) if err == nil && serviceIndex != nil { // we found service. index, garbage collect it - if errW := tx.Delete(tableIndex, serviceIndex); errW != nil { + if err := tx.Delete(tableIndex, serviceIndex); err != nil { return fmt.Errorf("[FAILED] deleting serviceIndex %s: %s", svc.ServiceName, err) } } @@ -1943,7 +1962,8 @@ func (s *Store) deleteServiceTxn(tx WriteTxn, idx uint64, nodeName, serviceID st return fmt.Errorf("failed to clean up gateway-service associations for %q: %v", name.String(), err) } } - if err := freeServiceVirtualIP(tx, svc.ServiceName, nil, entMeta); err != nil { + psn := structs.PeeredServiceName{Peer: svc.PeerName, ServiceName: name} + if err := freeServiceVirtualIP(tx, psn, nil); err != nil { return fmt.Errorf("failed to clean up virtual IP for %q: %v", name.String(), err) } if err := cleanupKindServiceName(tx, idx, svc.CompoundServiceName(), svc.ServiceKind); err != nil { @@ -1959,7 +1979,11 @@ func (s *Store) deleteServiceTxn(tx WriteTxn, idx uint64, nodeName, serviceID st // freeServiceVirtualIP is used to free a virtual IP for a service after the last instance // is removed. -func freeServiceVirtualIP(tx WriteTxn, svc string, excludeGateway *structs.ServiceName, entMeta *acl.EnterpriseMeta) error { +func freeServiceVirtualIP( + tx WriteTxn, + psn structs.PeeredServiceName, + excludeGateway *structs.ServiceName, +) error { supported, err := virtualIPsSupported(tx, nil) if err != nil { return err @@ -1969,15 +1993,14 @@ func freeServiceVirtualIP(tx WriteTxn, svc string, excludeGateway *structs.Servi } // Don't deregister the virtual IP if at least one terminating gateway still references this service. - sn := structs.NewServiceName(svc, entMeta) termGatewaySupported, err := terminatingGatewayVirtualIPsSupported(tx, nil) if err != nil { return err } if termGatewaySupported { - svcGateways, err := tx.Get(tableGatewayServices, indexService, sn) + svcGateways, err := tx.Get(tableGatewayServices, indexService, psn.ServiceName) if err != nil { - return fmt.Errorf("failed gateway lookup for %q: %s", sn.Name, err) + return fmt.Errorf("failed gateway lookup for %q: %s", psn.ServiceName.Name, err) } for service := svcGateways.Next(); service != nil; service = svcGateways.Next() { @@ -1990,7 +2013,7 @@ func freeServiceVirtualIP(tx WriteTxn, svc string, excludeGateway *structs.Servi } } - serviceVIP, err := tx.First(tableServiceVirtualIPs, indexID, sn) + serviceVIP, err := tx.First(tableServiceVirtualIPs, indexID, psn) if err != nil { return fmt.Errorf("failed service virtual IP lookup: %s", err) } @@ -2857,11 +2880,11 @@ func (s *Store) GatewayServices(ws memdb.WatchSet, gateway string, entMeta *acl. return lib.MaxUint64(maxIdx, idx), results, nil } -func (s *Store) VirtualIPForService(sn structs.ServiceName) (string, error) { +func (s *Store) VirtualIPForService(psn structs.PeeredServiceName) (string, error) { tx := s.db.Txn(false) defer tx.Abort() - vip, err := tx.First(tableServiceVirtualIPs, indexID, sn) + vip, err := tx.First(tableServiceVirtualIPs, indexID, psn) if err != nil { return "", fmt.Errorf("failed service virtual IP lookup: %s", err) } @@ -3314,7 +3337,9 @@ func getTermGatewayVirtualIPs(tx WriteTxn, services []structs.LinkedService, ent addrs := make(map[string]structs.ServiceAddress, len(services)) for _, s := range services { sn := structs.ServiceName{Name: s.Name, EnterpriseMeta: *entMeta} - vip, err := assignServiceVirtualIP(tx, sn) + // Terminating Gateways cannot route to services in peered clusters + psn := structs.PeeredServiceName{ServiceName: sn, Peer: structs.DefaultPeerKeyword} + vip, err := assignServiceVirtualIP(tx, psn) if err != nil { return nil, err } @@ -3391,7 +3416,8 @@ func updateTerminatingGatewayVirtualIPs(tx WriteTxn, idx uint64, conf *structs.T return err } if len(nodes) == 0 { - if err := freeServiceVirtualIP(tx, sn.Name, &gatewayName, &sn.EnterpriseMeta); err != nil { + psn := structs.PeeredServiceName{Peer: structs.DefaultPeerKeyword, ServiceName: sn} + if err := freeServiceVirtualIP(tx, psn, &gatewayName); err != nil { return err } } diff --git a/agent/consul/state/catalog_oss.go b/agent/consul/state/catalog_oss.go index b0c0c5337..ee3a9e487 100644 --- a/agent/consul/state/catalog_oss.go +++ b/agent/consul/state/catalog_oss.go @@ -7,7 +7,7 @@ import ( "fmt" "strings" - memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" @@ -24,8 +24,12 @@ func serviceKindIndexName(kind structs.ServiceKind, _ *acl.EnterpriseMeta, peerN return peeredIndexEntryName(base, peerName) } +func nodeIndexName(name string, _ *acl.EnterpriseMeta, peerName string) string { + return peeredIndexEntryName(fmt.Sprintf("node.%s", name), peerName) +} + func catalogUpdateNodesIndexes(tx WriteTxn, idx uint64, _ *acl.EnterpriseMeta, peerName string) error { - // overall nodes index + // overall nodes index for snapshot and ListNodes RPC if err := indexUpdateMaxTxn(tx, idx, tableNodes); err != nil { return fmt.Errorf("failed updating index: %s", err) } @@ -38,12 +42,22 @@ func catalogUpdateNodesIndexes(tx WriteTxn, idx uint64, _ *acl.EnterpriseMeta, p return nil } +// catalogUpdateNodeIndexes upserts the max index for a single node +func catalogUpdateNodeIndexes(tx WriteTxn, idx uint64, nodeName string, _ *acl.EnterpriseMeta, peerName string) error { + // per-node index + if err := indexUpdateMaxTxn(tx, idx, nodeIndexName(nodeName, nil, peerName)); err != nil { + return fmt.Errorf("failed updating node index: %w", err) + } + + return nil +} + // catalogUpdateServicesIndexes upserts the max index for the entire services table with varying levels // of granularity (no-op if `idx` is lower than what exists for that index key): // - all services // - all services in a specified peer (including internal) func catalogUpdateServicesIndexes(tx WriteTxn, idx uint64, _ *acl.EnterpriseMeta, peerName string) error { - // overall services index + // overall services index for snapshot if err := indexUpdateMaxTxn(tx, idx, tableServices); err != nil { return fmt.Errorf("failed updating index for services table: %w", err) } @@ -84,14 +98,16 @@ func catalogUpdateServiceIndexes(tx WriteTxn, idx uint64, serviceName string, _ } func catalogUpdateServiceExtinctionIndex(tx WriteTxn, idx uint64, _ *acl.EnterpriseMeta, peerName string) error { - if err := indexUpdateMaxTxn(tx, idx, indexServiceExtinction); err != nil { - return fmt.Errorf("failed updating missing service extinction index: %w", err) - } - // update the peer index if err := indexUpdateMaxTxn(tx, idx, peeredIndexEntryName(indexServiceExtinction, peerName)); err != nil { return fmt.Errorf("failed updating missing service extinction peered index: %w", err) } + return nil +} +func catalogUpdateNodeExtinctionIndex(tx WriteTxn, idx uint64, _ *acl.EnterpriseMeta, peerName string) error { + if err := indexUpdateMaxTxn(tx, idx, peeredIndexEntryName(indexNodeExtinction, peerName)); err != nil { + return fmt.Errorf("failed updating missing node extinction peered index: %w", err) + } return nil } @@ -105,7 +121,10 @@ func catalogInsertNode(tx WriteTxn, node *structs.Node) error { } if err := catalogUpdateNodesIndexes(tx, node.ModifyIndex, node.GetEnterpriseMeta(), node.PeerName); err != nil { - return err + return fmt.Errorf("failed updating nodes indexes: %w", err) + } + if err := catalogUpdateNodeIndexes(tx, node.ModifyIndex, node.Node, node.GetEnterpriseMeta(), node.PeerName); err != nil { + return fmt.Errorf("failed updating node indexes: %w", err) } // Update the node's service indexes as the node information is included @@ -125,15 +144,23 @@ func catalogInsertService(tx WriteTxn, svc *structs.ServiceNode) error { } if err := catalogUpdateServicesIndexes(tx, svc.ModifyIndex, &svc.EnterpriseMeta, svc.PeerName); err != nil { - return err + return fmt.Errorf("failed updating services indexes: %w", err) } if err := catalogUpdateServiceIndexes(tx, svc.ModifyIndex, svc.ServiceName, &svc.EnterpriseMeta, svc.PeerName); err != nil { - return err + return fmt.Errorf("failed updating service indexes: %w", err) } if err := catalogUpdateServiceKindIndexes(tx, svc.ModifyIndex, svc.ServiceKind, &svc.EnterpriseMeta, svc.PeerName); err != nil { - return err + return fmt.Errorf("failed updating service-kind indexes: %w", err) + } + + // Update the node indexes as the service information is included in node catalog queries. + if err := catalogUpdateNodesIndexes(tx, svc.ModifyIndex, &svc.EnterpriseMeta, svc.PeerName); err != nil { + return fmt.Errorf("failed updating nodes indexes: %w", err) + } + if err := catalogUpdateNodeIndexes(tx, svc.ModifyIndex, svc.Node, &svc.EnterpriseMeta, svc.PeerName); err != nil { + return fmt.Errorf("failed updating node indexes: %w", err) } return nil @@ -143,6 +170,14 @@ func catalogNodesMaxIndex(tx ReadTxn, _ *acl.EnterpriseMeta, peerName string) ui return maxIndexTxn(tx, peeredIndexEntryName(tableNodes, peerName)) } +func catalogNodeMaxIndex(tx ReadTxn, nodeName string, _ *acl.EnterpriseMeta, peerName string) uint64 { + return maxIndexTxn(tx, nodeIndexName(nodeName, nil, peerName)) +} + +func catalogNodeLastExtinctionIndex(tx ReadTxn, _ *acl.EnterpriseMeta, peerName string) uint64 { + return maxIndexTxn(tx, peeredIndexEntryName(indexNodeExtinction, peerName)) +} + func catalogServicesMaxIndex(tx ReadTxn, _ *acl.EnterpriseMeta, peerName string) uint64 { return maxIndexTxn(tx, peeredIndexEntryName(tableServices, peerName)) } @@ -185,7 +220,6 @@ func catalogMaxIndex(tx ReadTxn, _ *acl.EnterpriseMeta, peerName string, checks } func catalogMaxIndexWatch(tx ReadTxn, ws memdb.WatchSet, _ *acl.EnterpriseMeta, peerName string, checks bool) uint64 { - // TODO(peering_indexes): pipe peerName here if checks { return maxIndexWatchTxn(tx, ws, peeredIndexEntryName(tableChecks, peerName), @@ -200,7 +234,7 @@ func catalogMaxIndexWatch(tx ReadTxn, ws memdb.WatchSet, _ *acl.EnterpriseMeta, } func catalogUpdateCheckIndexes(tx WriteTxn, idx uint64, _ *acl.EnterpriseMeta, peerName string) error { - // update the universal index entry + // update the overall index entry for snapshot if err := indexUpdateMaxTxn(tx, idx, tableChecks); err != nil { return fmt.Errorf("failed updating index: %s", err) } @@ -265,3 +299,15 @@ func updateKindServiceNamesIndex(tx WriteTxn, idx uint64, kind structs.ServiceKi } return nil } + +func indexFromPeeredServiceName(psn structs.PeeredServiceName) ([]byte, error) { + peer := structs.LocalPeerKeyword + if psn.Peer != "" { + peer = psn.Peer + } + + var b indexBuilder + b.String(strings.ToLower(peer)) + b.String(strings.ToLower(psn.ServiceName.Name)) + return b.Bytes(), nil +} diff --git a/agent/consul/state/catalog_oss_test.go b/agent/consul/state/catalog_oss_test.go index 7ed7429fc..36d15b954 100644 --- a/agent/consul/state/catalog_oss_test.go +++ b/agent/consul/state/catalog_oss_test.go @@ -669,8 +669,19 @@ func testIndexerTableServices() map[string]indexerTestCase { func testIndexerTableServiceVirtualIPs() map[string]indexerTestCase { obj := ServiceVirtualIP{ - Service: structs.ServiceName{ - Name: "foo", + Service: structs.PeeredServiceName{ + ServiceName: structs.ServiceName{ + Name: "foo", + }, + }, + IP: net.ParseIP("127.0.0.1"), + } + peeredObj := ServiceVirtualIP{ + Service: structs.PeeredServiceName{ + ServiceName: structs.ServiceName{ + Name: "foo", + }, + Peer: "Billing", }, IP: net.ParseIP("127.0.0.1"), } @@ -678,14 +689,33 @@ func testIndexerTableServiceVirtualIPs() map[string]indexerTestCase { return map[string]indexerTestCase{ indexID: { read: indexValue{ - source: structs.ServiceName{ - Name: "foo", + source: structs.PeeredServiceName{ + ServiceName: structs.ServiceName{ + Name: "foo", + }, }, - expected: []byte("foo\x00"), + expected: []byte("internal\x00foo\x00"), }, write: indexValue{ source: obj, - expected: []byte("foo\x00"), + expected: []byte("internal\x00foo\x00"), + }, + extra: []indexerTestCase{ + { + read: indexValue{ + source: structs.PeeredServiceName{ + ServiceName: structs.ServiceName{ + Name: "foo", + }, + Peer: "Billing", + }, + expected: []byte("billing\x00foo\x00"), + }, + write: indexValue{ + source: peeredObj, + expected: []byte("billing\x00foo\x00"), + }, + }, }, }, } diff --git a/agent/consul/state/catalog_schema.go b/agent/consul/state/catalog_schema.go index 9a2fbecad..d77487c4a 100644 --- a/agent/consul/state/catalog_schema.go +++ b/agent/consul/state/catalog_schema.go @@ -47,7 +47,7 @@ func nodesTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[Query, *structs.Node, any]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexFromNode), prefixIndex: prefixIndexFromQueryWithPeer, @@ -57,7 +57,7 @@ func nodesTableSchema() *memdb.TableSchema { Name: indexUUID, AllowMissing: true, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[Query, *structs.Node, Query]{ readIndex: indexWithPeerName(indexFromUUIDQuery), writeIndex: indexWithPeerName(indexIDFromNode), prefixIndex: prefixIndexFromUUIDWithPeerQuery, @@ -67,7 +67,7 @@ func nodesTableSchema() *memdb.TableSchema { Name: indexMeta, AllowMissing: true, Unique: false, - Indexer: indexerMulti{ + Indexer: indexerMulti[KeyValueQuery, *structs.Node]{ readIndex: indexWithPeerName(indexFromKeyValueQuery), writeIndexMulti: multiIndexWithPeerName(indexMetaFromNode), }, @@ -76,12 +76,7 @@ func nodesTableSchema() *memdb.TableSchema { } } -func indexFromNode(raw interface{}) ([]byte, error) { - n, ok := raw.(*structs.Node) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.Node index", raw) - } - +func indexFromNode(n *structs.Node) ([]byte, error) { if n.Node == "" { return nil, errMissingValueForIndex } @@ -91,12 +86,7 @@ func indexFromNode(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexIDFromNode(raw interface{}) ([]byte, error) { - n, ok := raw.(*structs.Node) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.Node index", raw) - } - +func indexIDFromNode(n *structs.Node) ([]byte, error) { if n.ID == "" { return nil, errMissingValueForIndex } @@ -109,12 +99,7 @@ func indexIDFromNode(raw interface{}) ([]byte, error) { return v, nil } -func indexMetaFromNode(raw interface{}) ([][]byte, error) { - n, ok := raw.(*structs.Node) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.Node index", raw) - } - +func indexMetaFromNode(n *structs.Node) ([][]byte, error) { // NOTE: this is case-sensitive! vals := make([][]byte, 0, len(n.Meta)) @@ -145,7 +130,7 @@ func servicesTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[NodeServiceQuery, *structs.ServiceNode, any]{ readIndex: indexWithPeerName(indexFromNodeServiceQuery), writeIndex: indexWithPeerName(indexFromServiceNode), prefixIndex: prefixIndexFromQueryWithPeer, @@ -155,7 +140,7 @@ func servicesTableSchema() *memdb.TableSchema { Name: indexNode, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, nodeIdentifier]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexFromNodeIdentity), }, @@ -164,7 +149,7 @@ func servicesTableSchema() *memdb.TableSchema { Name: indexService, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.ServiceNode]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexServiceNameFromServiceNode), }, @@ -173,7 +158,7 @@ func servicesTableSchema() *memdb.TableSchema { Name: indexConnect, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.ServiceNode]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexConnectNameFromServiceNode), }, @@ -182,7 +167,7 @@ func servicesTableSchema() *memdb.TableSchema { Name: indexKind, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.ServiceNode]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexKindFromServiceNode), }, @@ -191,24 +176,14 @@ func servicesTableSchema() *memdb.TableSchema { } } -func indexFromNodeServiceQuery(arg interface{}) ([]byte, error) { - q, ok := arg.(NodeServiceQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for NodeServiceQuery index", arg) - } - +func indexFromNodeServiceQuery(q NodeServiceQuery) ([]byte, error) { var b indexBuilder b.String(strings.ToLower(q.Node)) b.String(strings.ToLower(q.Service)) return b.Bytes(), nil } -func indexFromServiceNode(raw interface{}) ([]byte, error) { - n, ok := raw.(*structs.ServiceNode) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ServiceNode index", raw) - } - +func indexFromServiceNode(n *structs.ServiceNode) ([]byte, error) { if n.Node == "" { return nil, errMissingValueForIndex } @@ -219,14 +194,17 @@ func indexFromServiceNode(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexFromNodeIdentity(raw interface{}) ([]byte, error) { - n, ok := raw.(interface { - NodeIdentity() structs.Identity - }) - if !ok { - return nil, fmt.Errorf("unexpected type %T for index, type must provide NodeIdentity()", raw) - } +type nodeIdentifier interface { + partitionIndexable + peerIndexable + NodeIdentity() structs.Identity +} + +var _ nodeIdentifier = (*structs.HealthCheck)(nil) +var _ nodeIdentifier = (*structs.ServiceNode)(nil) + +func indexFromNodeIdentity(n nodeIdentifier) ([]byte, error) { id := n.NodeIdentity() if id.ID == "" { return nil, errMissingValueForIndex @@ -237,12 +215,7 @@ func indexFromNodeIdentity(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexServiceNameFromServiceNode(raw interface{}) ([]byte, error) { - n, ok := raw.(*structs.ServiceNode) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ServiceNode index", raw) - } - +func indexServiceNameFromServiceNode(n *structs.ServiceNode) ([]byte, error) { if n.Node == "" { return nil, errMissingValueForIndex } @@ -252,12 +225,7 @@ func indexServiceNameFromServiceNode(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexConnectNameFromServiceNode(raw interface{}) ([]byte, error) { - n, ok := raw.(*structs.ServiceNode) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ServiceNode index", raw) - } - +func indexConnectNameFromServiceNode(n *structs.ServiceNode) ([]byte, error) { name, ok := connectNameFromServiceNode(n) if !ok { return nil, errMissingValueForIndex @@ -284,33 +252,23 @@ func connectNameFromServiceNode(sn *structs.ServiceNode) (string, bool) { } } -func indexKindFromServiceNode(raw interface{}) ([]byte, error) { - n, ok := raw.(*structs.ServiceNode) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.ServiceNode index", raw) - } - +func indexKindFromServiceNode(n *structs.ServiceNode) ([]byte, error) { var b indexBuilder b.String(strings.ToLower(string(n.ServiceKind))) return b.Bytes(), nil } // indexWithPeerName adds peer name to the index. -func indexWithPeerName( - fn func(interface{}) ([]byte, error), -) func(interface{}) ([]byte, error) { - return func(raw interface{}) ([]byte, error) { - v, err := fn(raw) +func indexWithPeerName[T peerIndexable]( + fn func(T) ([]byte, error), +) func(T) ([]byte, error) { + return func(e T) ([]byte, error) { + v, err := fn(e) if err != nil { return nil, err } - n, ok := raw.(peerIndexable) - if !ok { - return nil, fmt.Errorf("type must be peerIndexable: %T", raw) - } - - peername := n.PeerOrEmpty() + peername := e.PeerOrEmpty() if peername == "" { peername = structs.LocalPeerKeyword } @@ -322,20 +280,20 @@ func indexWithPeerName( } // multiIndexWithPeerName adds peer name to multiple indices, and returns multiple indices. -func multiIndexWithPeerName( - fn func(interface{}) ([][]byte, error), -) func(interface{}) ([][]byte, error) { - return func(raw interface{}) ([][]byte, error) { +func multiIndexWithPeerName[T any]( + fn func(T) ([][]byte, error), +) func(T) ([][]byte, error) { + return func(raw T) ([][]byte, error) { + n, ok := any(raw).(peerIndexable) + if !ok { + return nil, fmt.Errorf("type must be peerIndexable: %T", raw) + } + results, err := fn(raw) if err != nil { return nil, err } - n, ok := raw.(peerIndexable) - if !ok { - return nil, fmt.Errorf("type must be peerIndexable: %T", raw) - } - peername := n.PeerOrEmpty() if peername == "" { peername = structs.LocalPeerKeyword @@ -361,7 +319,7 @@ func checksTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[NodeCheckQuery, *structs.HealthCheck, any]{ readIndex: indexWithPeerName(indexFromNodeCheckQuery), writeIndex: indexWithPeerName(indexFromHealthCheck), prefixIndex: prefixIndexFromQueryWithPeer, @@ -371,7 +329,7 @@ func checksTableSchema() *memdb.TableSchema { Name: indexStatus, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.HealthCheck]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexStatusFromHealthCheck), }, @@ -380,7 +338,7 @@ func checksTableSchema() *memdb.TableSchema { Name: indexService, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.HealthCheck]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexServiceNameFromHealthCheck), }, @@ -389,7 +347,7 @@ func checksTableSchema() *memdb.TableSchema { Name: indexNode, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, nodeIdentifier]{ readIndex: indexWithPeerName(indexFromQuery), writeIndex: indexWithPeerName(indexFromNodeIdentity), }, @@ -398,7 +356,7 @@ func checksTableSchema() *memdb.TableSchema { Name: indexNodeService, AllowMissing: true, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[NodeServiceQuery, *structs.HealthCheck]{ readIndex: indexWithPeerName(indexFromNodeServiceQuery), writeIndex: indexWithPeerName(indexNodeServiceFromHealthCheck), }, @@ -407,28 +365,18 @@ func checksTableSchema() *memdb.TableSchema { } } -func indexFromNodeCheckQuery(raw interface{}) ([]byte, error) { - hc, ok := raw.(NodeCheckQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for NodeCheckQuery index", raw) - } - - if hc.Node == "" || hc.CheckID == "" { +func indexFromNodeCheckQuery(q NodeCheckQuery) ([]byte, error) { + if q.Node == "" || q.CheckID == "" { return nil, errMissingValueForIndex } var b indexBuilder - b.String(strings.ToLower(hc.Node)) - b.String(strings.ToLower(hc.CheckID)) + b.String(strings.ToLower(q.Node)) + b.String(strings.ToLower(q.CheckID)) return b.Bytes(), nil } -func indexFromHealthCheck(raw interface{}) ([]byte, error) { - hc, ok := raw.(*structs.HealthCheck) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.HealthCheck index", raw) - } - +func indexFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) { if hc.Node == "" || hc.CheckID == "" { return nil, errMissingValueForIndex } @@ -439,12 +387,7 @@ func indexFromHealthCheck(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexNodeServiceFromHealthCheck(raw interface{}) ([]byte, error) { - hc, ok := raw.(*structs.HealthCheck) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.HealthCheck index", raw) - } - +func indexNodeServiceFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) { if hc.Node == "" { return nil, errMissingValueForIndex } @@ -455,12 +398,7 @@ func indexNodeServiceFromHealthCheck(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexStatusFromHealthCheck(raw interface{}) ([]byte, error) { - hc, ok := raw.(*structs.HealthCheck) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.HealthCheck index", raw) - } - +func indexStatusFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) { if hc.Status == "" { return nil, errMissingValueForIndex } @@ -470,12 +408,7 @@ func indexStatusFromHealthCheck(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexServiceNameFromHealthCheck(raw interface{}) ([]byte, error) { - hc, ok := raw.(*structs.HealthCheck) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.HealthCheck index", raw) - } - +func indexServiceNameFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) { if hc.ServiceName == "" { return nil, errMissingValueForIndex } @@ -672,7 +605,7 @@ func (q NodeCheckQuery) PartitionOrDefault() string { // ServiceVirtualIP is used to store a virtual IP associated with a service. // It is also used to store assigned virtual IPs when a snapshot is created. type ServiceVirtualIP struct { - Service structs.ServiceName + Service structs.PeeredServiceName IP net.IP } @@ -698,14 +631,22 @@ func serviceVirtualIPTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: &ServiceNameIndex{ - Field: "Service", + Indexer: indexerSingle[structs.PeeredServiceName, ServiceVirtualIP]{ + readIndex: indexFromPeeredServiceName, + writeIndex: indexFromServiceVirtualIP, }, }, }, } } +func indexFromServiceVirtualIP(vip ServiceVirtualIP) ([]byte, error) { + if vip.Service.ServiceName.Name == "" { + return nil, errMissingValueForIndex + } + return indexFromPeeredServiceName(vip.Service) +} + func freeVirtualIPTableSchema() *memdb.TableSchema { return &memdb.TableSchema{ Name: tableFreeVirtualIPs, @@ -761,7 +702,7 @@ func kindServiceNameTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingle{ + Indexer: indexerSingle[any, any]{ readIndex: indexFromKindServiceName, writeIndex: indexFromKindServiceName, }, @@ -770,7 +711,7 @@ func kindServiceNameTableSchema() *memdb.TableSchema { Name: indexKind, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[enterpriseIndexable, enterpriseIndexable]{ readIndex: indexFromKindServiceNameKindOnly, writeIndex: indexFromKindServiceNameKindOnly, }, @@ -798,7 +739,7 @@ func (q KindServiceNameQuery) PartitionOrDefault() string { return q.EnterpriseMeta.PartitionOrDefault() } -func indexFromKindServiceNameKindOnly(raw interface{}) ([]byte, error) { +func indexFromKindServiceNameKindOnly(raw enterpriseIndexable) ([]byte, error) { switch x := raw.(type) { case *KindServiceName: var b indexBuilder diff --git a/agent/consul/state/catalog_test.go b/agent/consul/state/catalog_test.go index b48e0a04d..d2a970b07 100644 --- a/agent/consul/state/catalog_test.go +++ b/agent/consul/state/catalog_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/hashicorp/go-memdb" - uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -555,7 +555,7 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) { ) run := func(t *testing.T, peerName string) { - verifyNode := func(t *testing.T, s *Store, nodeLookup string) { + verifyNode := func(t *testing.T, s *Store, nodeLookup string, expectIdx uint64) { idx, out, err := s.GetNode(nodeLookup, nil, peerName) require.NoError(t, err) byID := false @@ -566,7 +566,7 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) { } require.NotNil(t, out) - require.Equal(t, uint64(1), idx) + require.Equal(t, expectIdx, idx) require.Equal(t, "1.2.3.4", out.Address) if byID { @@ -661,8 +661,8 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) { require.NoError(t, restore.Commit()) // Retrieve the node and verify its contents. - verifyNode(t, s, nodeID) - verifyNode(t, s, nodeName) + verifyNode(t, s, nodeID, 1) + verifyNode(t, s, nodeName, 1) }) // Add in a service definition. @@ -686,8 +686,8 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) { require.NoError(t, restore.Commit()) // Verify that the service got registered. - verifyNode(t, s, nodeID) - verifyNode(t, s, nodeName) + verifyNode(t, s, nodeID, 2) + verifyNode(t, s, nodeName, 2) verifyService(t, s, nodeID) verifyService(t, s, nodeName) }) @@ -726,8 +726,8 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) { require.NoError(t, restore.Commit()) // Verify that the check got registered. - verifyNode(t, s, nodeID) - verifyNode(t, s, nodeName) + verifyNode(t, s, nodeID, 2) + verifyNode(t, s, nodeName, 2) verifyService(t, s, nodeID) verifyService(t, s, nodeName) verifyCheck(t, s) @@ -776,8 +776,8 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) { require.NoError(t, restore.Commit()) // Verify that the additional check got registered. - verifyNode(t, s, nodeID) - verifyNode(t, s, nodeName) + verifyNode(t, s, nodeID, 2) + verifyNode(t, s, nodeName, 2) verifyService(t, s, nodeID) verifyService(t, s, nodeName) verifyChecks(t, s) @@ -976,7 +976,7 @@ func TestNodeRenamingNodes(t *testing.T) { Address: "1.1.1.2", } if err := s.EnsureNode(10, in2Modify); err != nil { - t.Fatalf("Renaming node2 into node1 should fail") + t.Fatalf("Renaming node2 into node1 should not fail: " + err.Error()) } // Retrieve the node again @@ -1550,20 +1550,16 @@ func TestStateStore_DeleteNode(t *testing.T) { } // Indexes were updated. - for _, tbl := range []string{tableNodes, tableServices, tableChecks} { - if idx := s.maxIndex(tbl); idx != 3 { - t.Fatalf("bad index: %d (%s)", idx, tbl) - } - } + assert.Equal(t, uint64(3), catalogChecksMaxIndex(tx, nil, "")) + assert.Equal(t, uint64(3), catalogServicesMaxIndex(tx, nil, "")) + assert.Equal(t, uint64(3), catalogNodesMaxIndex(tx, nil, "")) // Deleting a nonexistent node should be idempotent and not return // an error if err := s.DeleteNode(4, "node1", nil, ""); err != nil { t.Fatalf("err: %s", err) } - if idx := s.maxIndex(tableNodes); idx != 3 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(3), catalogNodesMaxIndex(s.db.ReadTxn(), nil, "")) } func TestStateStore_Node_Snapshot(t *testing.T) { @@ -1690,7 +1686,8 @@ func TestStateStore_EnsureService(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - if idx != 30 { + // expect node1's max idx + if idx != 20 { t.Fatalf("bad index: %d", idx) } @@ -1713,9 +1710,7 @@ func TestStateStore_EnsureService(t *testing.T) { } // Index tables were updated. - if idx := s.maxIndex(tableServices); idx != 30 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(30), catalogServicesMaxIndex(s.db.ReadTxn(), nil, "")) // Update a service registration. ns1.Address = "1.1.1.2" @@ -1744,9 +1739,7 @@ func TestStateStore_EnsureService(t *testing.T) { } // Index tables were updated. - if idx := s.maxIndex(tableServices); idx != 40 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(40), catalogServicesMaxIndex(s.db.ReadTxn(), nil, "")) } func TestStateStore_EnsureService_connectProxy(t *testing.T) { @@ -1806,7 +1799,7 @@ func TestStateStore_EnsureService_VirtualIPAssign(t *testing.T) { require.NoError(t, s.EnsureService(10, "node1", ns1)) // Make sure there's a virtual IP for the foo service. - vip, err := s.VirtualIPForService(structs.ServiceName{Name: "foo"}) + vip, err := s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "foo"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.1", vip) @@ -1837,7 +1830,7 @@ func TestStateStore_EnsureService_VirtualIPAssign(t *testing.T) { require.NoError(t, s.EnsureService(11, "node1", ns2)) // Make sure the virtual IP has been incremented for the redis service. - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "redis"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "redis"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.2", vip) @@ -1853,7 +1846,7 @@ func TestStateStore_EnsureService_VirtualIPAssign(t *testing.T) { // Delete the first service and make sure it no longer has a virtual IP assigned. require.NoError(t, s.DeleteService(12, "node1", "foo", entMeta, "")) - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "connect-proxy"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "connect-proxy"}}) require.NoError(t, err) assert.Equal(t, "", vip) @@ -1874,7 +1867,7 @@ func TestStateStore_EnsureService_VirtualIPAssign(t *testing.T) { require.NoError(t, s.EnsureService(13, "node1", ns3)) // Make sure the virtual IP is unchanged for the redis service. - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "redis"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "redis"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.2", vip) @@ -1902,7 +1895,7 @@ func TestStateStore_EnsureService_VirtualIPAssign(t *testing.T) { require.NoError(t, s.EnsureService(14, "node1", ns4)) // Make sure the virtual IP has allocated from the previously freed service. - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "web"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "web"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.1", vip) @@ -1912,6 +1905,41 @@ func TestStateStore_EnsureService_VirtualIPAssign(t *testing.T) { taggedAddress = out.Services["web-proxy"].TaggedAddresses[structs.TaggedAddressVirtualIP] assert.Equal(t, vip, taggedAddress.Address) assert.Equal(t, ns4.Port, taggedAddress.Port) + + // Register a node1 in another peer (technically this node would be imported + // and stored through the peering stream handlers). + testRegisterNodeOpts(t, s, 15, "node1", func(node *structs.Node) error { + node.PeerName = "billing" + return nil + }) + // Register an identical service but imported from a peer + ns5 := &structs.NodeService{ + Kind: structs.ServiceKindConnectProxy, + ID: "web-proxy", + Service: "web-proxy", + Address: "4.4.4.4", + Port: 4444, + Weights: &structs.Weights{ + Passing: 1, + Warning: 1, + }, + Proxy: structs.ConnectProxyConfig{DestinationServiceName: "web"}, + EnterpriseMeta: *entMeta, + PeerName: "billing", + } + require.NoError(t, s.EnsureService(15, "node1", ns5)) + + // Make sure the virtual IP is different from the identically named local service. + vip, err = s.VirtualIPForService(structs.PeeredServiceName{Peer: "billing", ServiceName: structs.ServiceName{Name: "web"}}) + require.NoError(t, err) + assert.Equal(t, "240.0.0.3", vip) + + // Retrieve and verify + _, out, err = s.NodeServices(nil, "node1", nil, "billing") + require.NoError(t, err) + taggedAddress = out.Services["web-proxy"].TaggedAddresses[structs.TaggedAddressVirtualIP] + assert.Equal(t, vip, taggedAddress.Address) + assert.Equal(t, ns5.Port, taggedAddress.Port) } func TestStateStore_EnsureService_ReassignFreedVIPs(t *testing.T) { @@ -1938,7 +1966,7 @@ func TestStateStore_EnsureService_ReassignFreedVIPs(t *testing.T) { require.NoError(t, s.EnsureService(10, "node1", ns1)) // Make sure there's a virtual IP for the foo service. - vip, err := s.VirtualIPForService(structs.ServiceName{Name: "foo"}) + vip, err := s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "foo"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.1", vip) @@ -1968,7 +1996,7 @@ func TestStateStore_EnsureService_ReassignFreedVIPs(t *testing.T) { require.NoError(t, s.EnsureService(11, "node1", ns2)) // Make sure the virtual IP has been incremented for the redis service. - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "redis"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "redis"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.2", vip) @@ -1983,7 +2011,7 @@ func TestStateStore_EnsureService_ReassignFreedVIPs(t *testing.T) { // Delete the last service and make sure it no longer has a virtual IP assigned. require.NoError(t, s.DeleteService(12, "node1", "redis", entMeta, "")) - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "redis"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "redis"}}) require.NoError(t, err) assert.Equal(t, "", vip) @@ -2003,7 +2031,7 @@ func TestStateStore_EnsureService_ReassignFreedVIPs(t *testing.T) { } require.NoError(t, s.EnsureService(13, "node1", ns3)) - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "backend"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "backend"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.2", vip) @@ -2033,7 +2061,7 @@ func TestStateStore_EnsureService_ReassignFreedVIPs(t *testing.T) { require.NoError(t, s.EnsureService(14, "node1", ns4)) // Make sure the virtual IP has been incremented for the frontend service. - vip, err = s.VirtualIPForService(structs.ServiceName{Name: "frontend"}) + vip, err = s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: "frontend"}}) require.NoError(t, err) assert.Equal(t, "240.0.0.3", vip) @@ -2571,21 +2599,15 @@ func TestStateStore_DeleteService(t *testing.T) { } // Index tables were updated. - if idx := s.maxIndex(tableServices); idx != 4 { - t.Fatalf("bad index: %d", idx) - } - if idx := s.maxIndex(tableChecks); idx != 4 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(4), catalogChecksMaxIndex(tx, nil, "")) + assert.Equal(t, uint64(4), catalogServicesMaxIndex(tx, nil, "")) // Deleting a nonexistent service should be idempotent and not return an // error, nor fire a watch. if err := s.DeleteService(5, "node1", "service1", nil, ""); err != nil { t.Fatalf("err: %s", err) } - if idx := s.maxIndex(tableServices); idx != 4 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(4), catalogServicesMaxIndex(tx, nil, "")) if watchFired(ws) { t.Fatalf("bad") } @@ -2906,9 +2928,7 @@ func TestStateStore_EnsureCheck(t *testing.T) { testCheckOutput(t, 5, 5, "bbbmodified") // Index tables were updated - if idx := s.maxIndex(tableChecks); idx != 5 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(5), catalogChecksMaxIndex(s.db.ReadTxn(), nil, "")) } func TestStateStore_EnsureCheck_defaultStatus(t *testing.T) { @@ -3387,9 +3407,7 @@ func TestStateStore_DeleteCheck(t *testing.T) { if idx, check, err := s.NodeCheck("node1", "check1", nil, ""); idx != 3 || err != nil || check != nil { t.Fatalf("Node check should have been deleted idx=%d, node=%v, err=%s", idx, check, err) } - if idx := s.maxIndex(tableChecks); idx != 3 { - t.Fatalf("bad index for checks: %d", idx) - } + assert.Equal(t, uint64(3), catalogChecksMaxIndex(s.db.ReadTxn(), nil, "")) if !watchFired(ws) { t.Fatalf("bad") } @@ -3407,18 +3425,14 @@ func TestStateStore_DeleteCheck(t *testing.T) { } // Index tables were updated. - if idx := s.maxIndex(tableChecks); idx != 3 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(3), catalogChecksMaxIndex(s.db.ReadTxn(), nil, "")) // Deleting a nonexistent check should be idempotent and not return an // error. if err := s.DeleteCheck(4, "node1", "check1", nil, ""); err != nil { t.Fatalf("err: %s", err) } - if idx := s.maxIndex(tableChecks); idx != 3 { - t.Fatalf("bad index: %d", idx) - } + assert.Equal(t, uint64(3), catalogChecksMaxIndex(s.db.ReadTxn(), nil, "")) if watchFired(ws) { t.Fatalf("bad") } diff --git a/agent/consul/state/config_entry_schema.go b/agent/consul/state/config_entry_schema.go index 53c03d597..e99068d10 100644 --- a/agent/consul/state/config_entry_schema.go +++ b/agent/consul/state/config_entry_schema.go @@ -1,7 +1,6 @@ package state import ( - "fmt" "strings" "github.com/hashicorp/go-memdb" @@ -27,7 +26,7 @@ func configTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[any, structs.ConfigEntry, any]{ readIndex: indexFromConfigEntryKindName, writeIndex: indexFromConfigEntry, prefixIndex: indexFromConfigEntryKindName, @@ -55,12 +54,30 @@ func configTableSchema() *memdb.TableSchema { } } -func indexFromConfigEntry(raw interface{}) ([]byte, error) { - c, ok := raw.(structs.ConfigEntry) - if !ok { - return nil, fmt.Errorf("type must be structs.ConfigEntry: %T", raw) - } +// configEntryIndexable is required because while structs.ConfigEntry +// has a GetEnterpriseMeta method, it does not directly expose the +// required NamespaceOrDefault and PartitionOrDefault methods of +// enterpriseIndexable. +// +// Config entries that embed *acl.EnterpriseMeta will automatically +// implement this interface. +type configEntryIndexable interface { + structs.ConfigEntry + enterpriseIndexable +} +var _ configEntryIndexable = (*structs.ExportedServicesConfigEntry)(nil) +var _ configEntryIndexable = (*structs.IngressGatewayConfigEntry)(nil) +var _ configEntryIndexable = (*structs.MeshConfigEntry)(nil) +var _ configEntryIndexable = (*structs.ProxyConfigEntry)(nil) +var _ configEntryIndexable = (*structs.ServiceConfigEntry)(nil) +var _ configEntryIndexable = (*structs.ServiceIntentionsConfigEntry)(nil) +var _ configEntryIndexable = (*structs.ServiceResolverConfigEntry)(nil) +var _ configEntryIndexable = (*structs.ServiceRouterConfigEntry)(nil) +var _ configEntryIndexable = (*structs.ServiceSplitterConfigEntry)(nil) +var _ configEntryIndexable = (*structs.TerminatingGatewayConfigEntry)(nil) + +func indexFromConfigEntry(c structs.ConfigEntry) ([]byte, error) { if c.GetName() == "" || c.GetKind() == "" { return nil, errMissingValueForIndex } @@ -73,12 +90,7 @@ func indexFromConfigEntry(raw interface{}) ([]byte, error) { // indexKindFromConfigEntry indexes kinds without a namespace for any config // entries that span all namespaces. -func indexKindFromConfigEntry(raw interface{}) ([]byte, error) { - c, ok := raw.(structs.ConfigEntry) - if !ok { - return nil, fmt.Errorf("type must be structs.ConfigEntry: %T", raw) - } - +func indexKindFromConfigEntry(c configEntryIndexable) ([]byte, error) { if c.GetKind() == "" { return nil, errMissingValueForIndex } diff --git a/agent/consul/state/coordinate.go b/agent/consul/state/coordinate.go index 0cbccf25c..19ae722f8 100644 --- a/agent/consul/state/coordinate.go +++ b/agent/consul/state/coordinate.go @@ -13,12 +13,7 @@ import ( const tableCoordinates = "coordinates" -func indexFromCoordinate(raw interface{}) ([]byte, error) { - c, ok := raw.(*structs.Coordinate) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.Coordinate index", raw) - } - +func indexFromCoordinate(c *structs.Coordinate) ([]byte, error) { if c.Node == "" { return nil, errMissingValueForIndex } @@ -29,12 +24,7 @@ func indexFromCoordinate(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexNodeFromCoordinate(raw interface{}) ([]byte, error) { - c, ok := raw.(*structs.Coordinate) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.Coordinate index", raw) - } - +func indexNodeFromCoordinate(c *structs.Coordinate) ([]byte, error) { if c.Node == "" { return nil, errMissingValueForIndex } @@ -44,12 +34,7 @@ func indexNodeFromCoordinate(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexFromCoordinateQuery(raw interface{}) ([]byte, error) { - q, ok := raw.(CoordinateQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for CoordinateQuery index", raw) - } - +func indexFromCoordinateQuery(q CoordinateQuery) ([]byte, error) { if q.Node == "" { return nil, errMissingValueForIndex } @@ -80,7 +65,7 @@ func coordinatesTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[CoordinateQuery, *structs.Coordinate, any]{ readIndex: indexFromCoordinateQuery, writeIndex: indexFromCoordinate, prefixIndex: prefixIndexFromQueryNoNamespace, @@ -90,7 +75,7 @@ func coordinatesTableSchema() *memdb.TableSchema { Name: indexNode, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[Query, *structs.Coordinate]{ readIndex: indexFromQuery, writeIndex: indexNodeFromCoordinate, }, diff --git a/agent/consul/state/indexer.go b/agent/consul/state/indexer.go index 70b769c58..83f205a2f 100644 --- a/agent/consul/state/indexer.go +++ b/agent/consul/state/indexer.go @@ -15,32 +15,42 @@ import ( // indexerSingle implements both memdb.Indexer and memdb.SingleIndexer. It may // be used in a memdb.IndexSchema to specify functions that generate the index // value for memdb.Txn operations. -type indexerSingle struct { +// +// R represents the type used to generate the read index. +// W represents the type used to generate the write index. +type indexerSingle[R, W any] struct { // readIndex is used by memdb for Txn.Get, Txn.First, and other operations // that read data. - readIndex + readIndex[R] // writeIndex is used by memdb for Txn.Insert, Txn.Delete, for operations // that write data to the index. - writeIndex + writeIndex[W] } // indexerMulti implements both memdb.Indexer and memdb.MultiIndexer. It may // be used in a memdb.IndexSchema to specify functions that generate the index // value for memdb.Txn operations. -type indexerMulti struct { +// +// R represents the type used to generate the read index. +// W represents the type used to generate the write index. +type indexerMulti[R, W any] struct { // readIndex is used by memdb for Txn.Get, Txn.First, and other operations // that read data. - readIndex + readIndex[R] // writeIndexMulti is used by memdb for Txn.Insert, Txn.Delete, for operations // that write data to the index. - writeIndexMulti + writeIndexMulti[W] } // indexerSingleWithPrefix is a indexerSingle which also supports prefix queries. -type indexerSingleWithPrefix struct { - readIndex - writeIndex - prefixIndex +// +// R represents the type used to generate the read index. +// W represents the type used to generate the write index. +// P represents the type used to generate the prefix index. +type indexerSingleWithPrefix[R, W, P any] struct { + readIndex[R] + writeIndex[W] + prefixIndex[P] } // readIndex implements memdb.Indexer. It exists so that a function can be used @@ -48,13 +58,18 @@ type indexerSingleWithPrefix struct { // // Unlike memdb.Indexer, a readIndex function accepts only a single argument. To // generate an index from multiple values, use a struct type with multiple fields. -type readIndex func(arg interface{}) ([]byte, error) +type readIndex[R any] func(arg R) ([]byte, error) -func (f readIndex) FromArgs(args ...interface{}) ([]byte, error) { +func (f readIndex[R]) FromArgs(args ...interface{}) ([]byte, error) { if len(args) != 1 { return nil, fmt.Errorf("index supports only a single arg") } - return f(args[0]) + arg, ok := args[0].(R) + if !ok { + var typ R + return nil, fmt.Errorf("unexpected type %T, does not implement %T", args[0], typ) + } + return f(arg) } var errMissingValueForIndex = fmt.Errorf("object is missing a value for this index") @@ -65,10 +80,15 @@ var errMissingValueForIndex = fmt.Errorf("object is missing a value for this ind // Instead of a bool return value, writeIndex expects errMissingValueForIndex to // indicate that an index could not be build for the object. It will translate // this error into a false value to satisfy the memdb.SingleIndexer interface. -type writeIndex func(raw interface{}) ([]byte, error) +type writeIndex[W any] func(raw W) ([]byte, error) -func (f writeIndex) FromObject(raw interface{}) (bool, []byte, error) { - v, err := f(raw) +func (f writeIndex[W]) FromObject(raw interface{}) (bool, []byte, error) { + obj, ok := raw.(W) + if !ok { + var typ W + return false, nil, fmt.Errorf("unexpected type %T, does not implement %T", raw, typ) + } + v, err := f(obj) if errors.Is(err, errMissingValueForIndex) { return false, nil, nil } @@ -81,10 +101,15 @@ func (f writeIndex) FromObject(raw interface{}) (bool, []byte, error) { // Instead of a bool return value, writeIndexMulti expects errMissingValueForIndex to // indicate that an index could not be build for the object. It will translate // this error into a false value to satisfy the memdb.MultiIndexer interface. -type writeIndexMulti func(raw interface{}) ([][]byte, error) +type writeIndexMulti[W any] func(raw W) ([][]byte, error) -func (f writeIndexMulti) FromObject(raw interface{}) (bool, [][]byte, error) { - v, err := f(raw) +func (f writeIndexMulti[W]) FromObject(raw interface{}) (bool, [][]byte, error) { + obj, ok := raw.(W) + if !ok { + var typ W + return false, nil, fmt.Errorf("unexpected type %T, does not implement %T", raw, typ) + } + v, err := f(obj) if errors.Is(err, errMissingValueForIndex) { return false, nil, nil } @@ -93,13 +118,18 @@ func (f writeIndexMulti) FromObject(raw interface{}) (bool, [][]byte, error) { // prefixIndex implements memdb.PrefixIndexer. It exists so that a function // can be used to provide this interface. -type prefixIndex func(args interface{}) ([]byte, error) +type prefixIndex[P any] func(args P) ([]byte, error) -func (f prefixIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) { +func (f prefixIndex[P]) PrefixFromArgs(args ...interface{}) ([]byte, error) { if len(args) != 1 { return nil, fmt.Errorf("index supports only a single arg") } - return f(args[0]) + arg, ok := args[0].(P) + if !ok { + var typ P + return nil, fmt.Errorf("unexpected type %T, does not implement %T", args[0], typ) + } + return f(arg) } const null = "\x00" @@ -159,12 +189,7 @@ var _ singleValueID = (*Query)(nil) var _ singleValueID = (*structs.Session)(nil) // indexFromIDValue creates an index key from any struct that implements singleValueID -func indexFromIDValueLowerCase(raw interface{}) ([]byte, error) { - e, ok := raw.(singleValueID) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement singleValueID", raw) - } - +func indexFromIDValueLowerCase(e singleValueID) ([]byte, error) { v := strings.ToLower(e.IDValue()) if v == "" { return nil, errMissingValueForIndex @@ -176,11 +201,7 @@ func indexFromIDValueLowerCase(raw interface{}) ([]byte, error) { } // indexFromIDValue creates an index key from any struct that implements singleValueID -func indexFromMultiValueID(raw interface{}) ([]byte, error) { - e, ok := raw.(multiValueID) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement multiValueID", raw) - } +func indexFromMultiValueID(e multiValueID) ([]byte, error) { var b indexBuilder for _, v := range e.IDValue() { if v == "" { diff --git a/agent/consul/state/kvs.go b/agent/consul/state/kvs.go index 82aa842e8..62797125c 100644 --- a/agent/consul/state/kvs.go +++ b/agent/consul/state/kvs.go @@ -41,12 +41,7 @@ func kvsTableSchema() *memdb.TableSchema { } // indexFromIDValue creates an index key from any struct that implements singleValueID -func indexFromIDValue(raw interface{}) ([]byte, error) { - e, ok := raw.(singleValueID) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement singleValueID", raw) - } - +func indexFromIDValue(e singleValueID) ([]byte, error) { v := e.IDValue() if v == "" { return nil, errMissingValueForIndex diff --git a/agent/consul/state/kvs_oss.go b/agent/consul/state/kvs_oss.go index 3ded43255..149fe2c47 100644 --- a/agent/consul/state/kvs_oss.go +++ b/agent/consul/state/kvs_oss.go @@ -13,11 +13,11 @@ import ( "github.com/hashicorp/consul/agent/structs" ) -func kvsIndexer() indexerSingleWithPrefix { - return indexerSingleWithPrefix{ - readIndex: readIndex(indexFromIDValue), - writeIndex: writeIndex(indexFromIDValue), - prefixIndex: prefixIndex(prefixIndexForIDValue), +func kvsIndexer() indexerSingleWithPrefix[singleValueID, singleValueID, any] { + return indexerSingleWithPrefix[singleValueID, singleValueID, any]{ + readIndex: indexFromIDValue, + writeIndex: indexFromIDValue, + prefixIndex: prefixIndexForIDValue, } } diff --git a/agent/consul/state/peering.go b/agent/consul/state/peering.go index 3d115707d..78595e4e6 100644 --- a/agent/consul/state/peering.go +++ b/agent/consul/state/peering.go @@ -1,12 +1,12 @@ package state import ( + "errors" "fmt" "strings" "github.com/golang/protobuf/proto" "github.com/hashicorp/go-memdb" - "github.com/hashicorp/go-uuid" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" @@ -27,16 +27,16 @@ func peeringTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingle{ - readIndex: readIndex(indexFromUUIDString), - writeIndex: writeIndex(indexIDFromPeering), + Indexer: indexerSingle[string, *pbpeering.Peering]{ + readIndex: indexFromUUIDString, + writeIndex: indexIDFromPeering, }, }, indexName: { Name: indexName, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[Query, *pbpeering.Peering, any]{ readIndex: indexPeeringFromQuery, writeIndex: indexFromPeering, prefixIndex: prefixIndexFromQueryNoNamespace, @@ -46,7 +46,7 @@ func peeringTableSchema() *memdb.TableSchema { Name: indexDeleted, AllowMissing: false, Unique: false, - Indexer: indexerSingle{ + Indexer: indexerSingle[BoolQuery, *pbpeering.Peering]{ readIndex: indexDeletedFromBoolQuery, writeIndex: indexDeletedFromPeering, }, @@ -63,7 +63,7 @@ func peeringTrustBundlesTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingleWithPrefix{ + Indexer: indexerSingleWithPrefix[Query, *pbpeering.PeeringTrustBundle, any]{ readIndex: indexPeeringFromQuery, // same as peering table since we'll use the query.Value writeIndex: indexFromPeeringTrustBundle, prefixIndex: prefixIndexFromQueryNoNamespace, @@ -73,12 +73,7 @@ func peeringTrustBundlesTableSchema() *memdb.TableSchema { } } -func indexIDFromPeering(raw interface{}) ([]byte, error) { - p, ok := raw.(*pbpeering.Peering) - if !ok { - return nil, fmt.Errorf("unexpected type %T for pbpeering.Peering index", raw) - } - +func indexIDFromPeering(p *pbpeering.Peering) ([]byte, error) { if p.ID == "" { return nil, errMissingValueForIndex } @@ -92,12 +87,7 @@ func indexIDFromPeering(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexDeletedFromPeering(raw interface{}) ([]byte, error) { - p, ok := raw.(*pbpeering.Peering) - if !ok { - return nil, fmt.Errorf("unexpected type %T for *pbpeering.Peering index", raw) - } - +func indexDeletedFromPeering(p *pbpeering.Peering) ([]byte, error) { var b indexBuilder b.Bool(!p.IsActive()) return b.Bytes(), nil @@ -191,50 +181,47 @@ func (s *Store) peeringListTxn(ws memdb.WatchSet, tx ReadTxn, entMeta acl.Enterp return idx, result, nil } -func generatePeeringUUID(tx ReadTxn) (string, error) { - for { - uuid, err := uuid.GenerateUUID() - if err != nil { - return "", fmt.Errorf("failed to generate UUID: %w", err) - } - existing, err := peeringReadByIDTxn(tx, nil, uuid) - if err != nil { - return "", fmt.Errorf("failed to read peering: %w", err) - } - if existing == nil { - return uuid, nil - } - } -} - func (s *Store) PeeringWrite(idx uint64, p *pbpeering.Peering) error { tx := s.db.WriteTxn(idx) defer tx.Abort() - q := Query{ - Value: p.Name, - EnterpriseMeta: *structs.NodeEnterpriseMetaInPartition(p.Partition), + // Check that the ID and Name are set. + if p.ID == "" { + return errors.New("Missing Peering ID") } - existingRaw, err := tx.First(tablePeering, indexName, q) - if err != nil { - return fmt.Errorf("failed peering lookup: %w", err) + if p.Name == "" { + return errors.New("Missing Peering Name") } - existing, ok := existingRaw.(*pbpeering.Peering) - if existingRaw != nil && !ok { - return fmt.Errorf("invalid type %T", existingRaw) + // ensure the name is unique (cannot conflict with another peering with a different ID) + _, existing, err := peeringReadTxn(tx, nil, Query{ + Value: p.Name, + EnterpriseMeta: *structs.NodeEnterpriseMetaInPartition(p.Partition), + }) + if err != nil { + return err } if existing != nil { + if p.ID != existing.ID { + return fmt.Errorf("A peering already exists with the name %q and a different ID %q", p.Name, existing.ID) + } // Prevent modifications to Peering marked for deletion if !existing.IsActive() { return fmt.Errorf("cannot write to peering that is marked for deletion") } p.CreateIndex = existing.CreateIndex - p.ID = existing.ID - + p.ModifyIndex = idx } else { + idMatch, err := peeringReadByIDTxn(tx, nil, p.ID) + if err != nil { + return err + } + if idMatch != nil { + return fmt.Errorf("A peering already exists with the ID %q and a different name %q", p.Name, existing.ID) + } + if !p.IsActive() { return fmt.Errorf("cannot create a new peering marked for deletion") } @@ -242,13 +229,8 @@ func (s *Store) PeeringWrite(idx uint64, p *pbpeering.Peering) error { // TODO(peering): consider keeping PeeringState enum elsewhere? p.State = pbpeering.PeeringState_INITIAL p.CreateIndex = idx - - p.ID, err = generatePeeringUUID(tx) - if err != nil { - return fmt.Errorf("failed to generate peering id: %w", err) - } + p.ModifyIndex = idx } - p.ModifyIndex = idx if err := tx.Insert(tablePeering, p); err != nil { return fmt.Errorf("failed inserting peering: %w", err) diff --git a/agent/consul/state/peering_oss.go b/agent/consul/state/peering_oss.go index 8229d78a6..a331b5648 100644 --- a/agent/consul/state/peering_oss.go +++ b/agent/consul/state/peering_oss.go @@ -10,23 +10,13 @@ import ( "github.com/hashicorp/consul/proto/pbpeering" ) -func indexPeeringFromQuery(raw interface{}) ([]byte, error) { - q, ok := raw.(Query) - if !ok { - return nil, fmt.Errorf("unexpected type %T for Query index", raw) - } - +func indexPeeringFromQuery(q Query) ([]byte, error) { var b indexBuilder b.String(strings.ToLower(q.Value)) return b.Bytes(), nil } -func indexFromPeering(raw interface{}) ([]byte, error) { - p, ok := raw.(*pbpeering.Peering) - if !ok { - return nil, fmt.Errorf("unexpected type %T for structs.Peering index", raw) - } - +func indexFromPeering(p *pbpeering.Peering) ([]byte, error) { if p.Name == "" { return nil, errMissingValueForIndex } @@ -36,12 +26,7 @@ func indexFromPeering(raw interface{}) ([]byte, error) { return b.Bytes(), nil } -func indexFromPeeringTrustBundle(raw interface{}) ([]byte, error) { - ptb, ok := raw.(*pbpeering.PeeringTrustBundle) - if !ok { - return nil, fmt.Errorf("unexpected type %T for pbpeering.PeeringTrustBundle index", raw) - } - +func indexFromPeeringTrustBundle(ptb *pbpeering.PeeringTrustBundle) ([]byte, error) { if ptb.PeerName == "" { return nil, errMissingValueForIndex } diff --git a/agent/consul/state/peering_test.go b/agent/consul/state/peering_test.go index 4aba5c340..04389a8e9 100644 --- a/agent/consul/state/peering_test.go +++ b/agent/consul/state/peering_test.go @@ -1,13 +1,10 @@ package state import ( - "fmt" - "math/rand" "testing" "time" "github.com/hashicorp/go-memdb" - "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/acl" @@ -17,6 +14,12 @@ import ( "github.com/hashicorp/consul/sdk/testutil" ) +const ( + testFooPeerID = "9e650110-ac74-4c5a-a6a8-9348b2bed4e9" + testBarPeerID = "5ebcff30-5509-4858-8142-a8e580f1863f" + testBazPeerID = "432feb2f-5476-4ae2-b33c-e43640ca0e86" +) + func insertTestPeerings(t *testing.T, s *Store) { t.Helper() @@ -26,7 +29,7 @@ func insertTestPeerings(t *testing.T, s *Store) { err := tx.Insert(tablePeering, &pbpeering.Peering{ Name: "foo", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), - ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + ID: testFooPeerID, State: pbpeering.PeeringState_INITIAL, CreateIndex: 1, ModifyIndex: 1, @@ -36,7 +39,7 @@ func insertTestPeerings(t *testing.T, s *Store) { err = tx.Insert(tablePeering, &pbpeering.Peering{ Name: "bar", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), - ID: "5ebcff30-5509-4858-8142-a8e580f1863f", + ID: testBarPeerID, State: pbpeering.PeeringState_FAILING, CreateIndex: 2, ModifyIndex: 2, @@ -97,16 +100,16 @@ func TestStateStore_PeeringReadByID(t *testing.T) { run := func(t *testing.T, tc testcase) { _, peering, err := s.PeeringReadByID(nil, tc.id) require.NoError(t, err) - require.Equal(t, tc.expect, peering) + prototest.AssertDeepEqual(t, tc.expect, peering) } tcs := []testcase{ { name: "get foo", - id: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + id: testFooPeerID, expect: &pbpeering.Peering{ Name: "foo", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), - ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + ID: testFooPeerID, State: pbpeering.PeeringState_INITIAL, CreateIndex: 1, ModifyIndex: 1, @@ -114,11 +117,11 @@ func TestStateStore_PeeringReadByID(t *testing.T) { }, { name: "get bar", - id: "5ebcff30-5509-4858-8142-a8e580f1863f", + id: testBarPeerID, expect: &pbpeering.Peering{ Name: "bar", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), - ID: "5ebcff30-5509-4858-8142-a8e580f1863f", + ID: testBarPeerID, State: pbpeering.PeeringState_FAILING, CreateIndex: 2, ModifyIndex: 2, @@ -149,7 +152,7 @@ func TestStateStore_PeeringRead(t *testing.T) { run := func(t *testing.T, tc testcase) { _, peering, err := s.PeeringRead(nil, tc.query) require.NoError(t, err) - require.Equal(t, tc.expect, peering) + prototest.AssertDeepEqual(t, tc.expect, peering) } tcs := []testcase{ { @@ -160,7 +163,7 @@ func TestStateStore_PeeringRead(t *testing.T) { expect: &pbpeering.Peering{ Name: "foo", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), - ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + ID: testFooPeerID, State: pbpeering.PeeringState_INITIAL, CreateIndex: 1, ModifyIndex: 1, @@ -189,6 +192,7 @@ func TestStore_Peering_Watch(t *testing.T) { // set up initial write err := s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testFooPeerID, Name: "foo", }) require.NoError(t, err) @@ -210,6 +214,7 @@ func TestStore_Peering_Watch(t *testing.T) { lastIdx++ err := s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testBarPeerID, Name: "bar", }) require.NoError(t, err) @@ -229,6 +234,7 @@ func TestStore_Peering_Watch(t *testing.T) { // unrelated write shouldn't fire watch lastIdx++ err := s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testBarPeerID, Name: "bar", }) require.NoError(t, err) @@ -237,6 +243,7 @@ func TestStore_Peering_Watch(t *testing.T) { // foo write should fire watch lastIdx++ err = s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testFooPeerID, Name: "foo", DeletedAt: structs.TimeToProto(time.Now()), }) @@ -261,6 +268,7 @@ func TestStore_Peering_Watch(t *testing.T) { // mark for deletion before actually deleting lastIdx++ err := s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testBarPeerID, Name: "bar", DeletedAt: structs.TimeToProto(time.Now()), }) @@ -293,7 +301,7 @@ func TestStore_PeeringList(t *testing.T) { { Name: "foo", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), - ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + ID: testFooPeerID, State: pbpeering.PeeringState_INITIAL, CreateIndex: 1, ModifyIndex: 1, @@ -301,7 +309,7 @@ func TestStore_PeeringList(t *testing.T) { { Name: "bar", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), - ID: "5ebcff30-5509-4858-8142-a8e580f1863f", + ID: testBarPeerID, State: pbpeering.PeeringState_FAILING, CreateIndex: 2, ModifyIndex: 2, @@ -336,6 +344,7 @@ func TestStore_PeeringList_Watch(t *testing.T) { lastIdx++ // insert a peering err := s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testFooPeerID, Name: "foo", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }) @@ -357,6 +366,7 @@ func TestStore_PeeringList_Watch(t *testing.T) { // update peering lastIdx++ require.NoError(t, s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testFooPeerID, Name: "foo", DeletedAt: structs.TimeToProto(time.Now()), Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), @@ -422,6 +432,7 @@ func TestStore_PeeringWrite(t *testing.T) { { name: "create baz", input: &pbpeering.Peering{ + ID: testBazPeerID, Name: "baz", Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }, @@ -429,6 +440,7 @@ func TestStore_PeeringWrite(t *testing.T) { { name: "update baz", input: &pbpeering.Peering{ + ID: testBazPeerID, Name: "baz", State: pbpeering.PeeringState_FAILING, Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), @@ -437,6 +449,7 @@ func TestStore_PeeringWrite(t *testing.T) { { name: "mark baz for deletion", input: &pbpeering.Peering{ + ID: testBazPeerID, Name: "baz", State: pbpeering.PeeringState_TERMINATED, DeletedAt: structs.TimeToProto(time.Now()), @@ -446,6 +459,7 @@ func TestStore_PeeringWrite(t *testing.T) { { name: "cannot update peering marked for deletion", input: &pbpeering.Peering{ + ID: testBazPeerID, Name: "baz", // Attempt to add metadata Meta: map[string]string{ @@ -458,6 +472,7 @@ func TestStore_PeeringWrite(t *testing.T) { { name: "cannot create peering marked for deletion", input: &pbpeering.Peering{ + ID: testFooPeerID, Name: "foo", DeletedAt: structs.TimeToProto(time.Now()), Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), @@ -472,54 +487,6 @@ func TestStore_PeeringWrite(t *testing.T) { } } -func TestStore_PeeringWrite_GenerateUUID(t *testing.T) { - rand.Seed(1) - - s := NewStateStore(nil) - - entMeta := structs.NodeEnterpriseMetaInDefaultPartition() - partition := entMeta.PartitionOrDefault() - - for i := 1; i < 11; i++ { - require.NoError(t, s.PeeringWrite(uint64(i), &pbpeering.Peering{ - Name: fmt.Sprintf("peering-%d", i), - Partition: partition, - })) - } - - idx, peerings, err := s.PeeringList(nil, *entMeta) - require.NoError(t, err) - require.Equal(t, uint64(10), idx) - require.Len(t, peerings, 10) - - // Ensure that all assigned UUIDs are unique. - uniq := make(map[string]struct{}) - for _, p := range peerings { - uniq[p.ID] = struct{}{} - } - require.Len(t, uniq, 10) - - // Ensure that the ID of an existing peering cannot be overwritten. - updated := &pbpeering.Peering{ - Name: peerings[0].Name, - Partition: peerings[0].Partition, - } - - // Attempt to overwrite ID. - updated.ID, err = uuid.GenerateUUID() - require.NoError(t, err) - require.NoError(t, s.PeeringWrite(11, updated)) - - q := Query{ - Value: updated.Name, - EnterpriseMeta: *entMeta, - } - idx, got, err := s.PeeringRead(nil, q) - require.NoError(t, err) - require.Equal(t, uint64(11), idx) - require.Equal(t, peerings[0].ID, got.ID) -} - func TestStore_PeeringDelete(t *testing.T) { s := NewStateStore(nil) insertTestPeerings(t, s) @@ -532,6 +499,7 @@ func TestStore_PeeringDelete(t *testing.T) { testutil.RunStep(t, "can delete after marking for deletion", func(t *testing.T) { require.NoError(t, s.PeeringWrite(11, &pbpeering.Peering{ + ID: testFooPeerID, Name: "foo", DeletedAt: structs.TimeToProto(time.Now()), })) @@ -550,7 +518,7 @@ func TestStore_PeeringTerminateByID(t *testing.T) { insertTestPeerings(t, s) // id corresponding to default/foo - id := "9e650110-ac74-4c5a-a6a8-9348b2bed4e9" + const id = testFooPeerID require.NoError(t, s.PeeringTerminateByID(10, id)) @@ -607,7 +575,7 @@ func TestStateStore_PeeringTrustBundleRead(t *testing.T) { run := func(t *testing.T, tc testcase) { _, ptb, err := s.PeeringTrustBundleRead(nil, tc.query) require.NoError(t, err) - require.Equal(t, tc.expect, ptb) + prototest.AssertDeepEqual(t, tc.expect, ptb) } entMeta := structs.NodeEnterpriseMetaInDefaultPartition() @@ -708,6 +676,7 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { lastIdx++ require.NoError(t, s.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testUUID(), Name: "my-peering", })) @@ -1000,6 +969,9 @@ func TestStateStore_PeeringsForService(t *testing.T) { var lastIdx uint64 // Create peerings for _, tp := range tc.peerings { + if tp.peering.ID == "" { + tp.peering.ID = testUUID() + } lastIdx++ require.NoError(t, s.PeeringWrite(lastIdx, tp.peering)) @@ -1009,6 +981,7 @@ func TestStateStore_PeeringsForService(t *testing.T) { lastIdx++ copied := pbpeering.Peering{ + ID: tp.peering.ID, Name: tp.peering.Name, DeletedAt: structs.TimeToProto(time.Now()), } @@ -1247,6 +1220,11 @@ func TestStore_TrustBundleListByService(t *testing.T) { var lastIdx uint64 ws := memdb.NewWatchSet() + var ( + peerID1 = testUUID() + peerID2 = testUUID() + ) + testutil.RunStep(t, "no results on initial setup", func(t *testing.T) { idx, resp, err := store.TrustBundleListByService(ws, "foo", entMeta) require.NoError(t, err) @@ -1279,6 +1257,7 @@ func TestStore_TrustBundleListByService(t *testing.T) { testutil.RunStep(t, "creating peering does not yield trust bundles", func(t *testing.T) { lastIdx++ require.NoError(t, store.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: peerID1, Name: "peer1", })) @@ -1377,6 +1356,7 @@ func TestStore_TrustBundleListByService(t *testing.T) { testutil.RunStep(t, "bundles for other peers are ignored", func(t *testing.T) { lastIdx++ require.NoError(t, store.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: peerID2, Name: "peer2", })) @@ -1431,6 +1411,7 @@ func TestStore_TrustBundleListByService(t *testing.T) { testutil.RunStep(t, "deleting the peering excludes its trust bundle", func(t *testing.T) { lastIdx++ require.NoError(t, store.PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: peerID1, Name: "peer1", DeletedAt: structs.TimeToProto(time.Now()), })) @@ -1470,7 +1451,7 @@ func TestStateStore_Peering_ListDeleted(t *testing.T) { err := tx.Insert(tablePeering, &pbpeering.Peering{ Name: "foo", Partition: acl.DefaultPartitionName, - ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9", + ID: testFooPeerID, DeletedAt: structs.TimeToProto(time.Now()), CreateIndex: 1, ModifyIndex: 1, @@ -1480,7 +1461,7 @@ func TestStateStore_Peering_ListDeleted(t *testing.T) { err = tx.Insert(tablePeering, &pbpeering.Peering{ Name: "bar", Partition: acl.DefaultPartitionName, - ID: "5ebcff30-5509-4858-8142-a8e580f1863f", + ID: testBarPeerID, CreateIndex: 2, ModifyIndex: 2, }) @@ -1489,7 +1470,7 @@ func TestStateStore_Peering_ListDeleted(t *testing.T) { err = tx.Insert(tablePeering, &pbpeering.Peering{ Name: "baz", Partition: acl.DefaultPartitionName, - ID: "432feb2f-5476-4ae2-b33c-e43640ca0e86", + ID: testBazPeerID, DeletedAt: structs.TimeToProto(time.Now()), CreateIndex: 3, ModifyIndex: 3, diff --git a/agent/consul/state/query.go b/agent/consul/state/query.go index a4725b875..2e4a78869 100644 --- a/agent/consul/state/query.go +++ b/agent/consul/state/query.go @@ -60,12 +60,7 @@ func (q MultiQuery) PartitionOrDefault() string { // indexFromQuery builds an index key where Query.Value is lowercase, and is // a required value. -func indexFromQuery(arg interface{}) ([]byte, error) { - q, ok := arg.(Query) - if !ok { - return nil, fmt.Errorf("unexpected type %T for Query index", arg) - } - +func indexFromQuery(q Query) ([]byte, error) { var b indexBuilder b.String(strings.ToLower(q.Value)) return b.Bytes(), nil @@ -164,12 +159,8 @@ func (q KeyValueQuery) PartitionOrDefault() string { return q.EnterpriseMeta.PartitionOrDefault() } -func indexFromKeyValueQuery(arg interface{}) ([]byte, error) { +func indexFromKeyValueQuery(q KeyValueQuery) ([]byte, error) { // NOTE: this is case-sensitive! - q, ok := arg.(KeyValueQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for Query index", arg) - } var b indexBuilder b.String(q.Key) diff --git a/agent/consul/state/query_oss.go b/agent/consul/state/query_oss.go index 553e7aebe..e36850fe0 100644 --- a/agent/consul/state/query_oss.go +++ b/agent/consul/state/query_oss.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/consul/agent/structs" ) -func prefixIndexFromQuery(arg interface{}) ([]byte, error) { +func prefixIndexFromQuery(arg any) ([]byte, error) { var b indexBuilder switch v := arg.(type) { case *acl.EnterpriseMeta: @@ -29,7 +29,7 @@ func prefixIndexFromQuery(arg interface{}) ([]byte, error) { return nil, fmt.Errorf("unexpected type %T for Query prefix index", arg) } -func prefixIndexFromQueryWithPeer(arg interface{}) ([]byte, error) { +func prefixIndexFromQueryWithPeer(arg any) ([]byte, error) { var b indexBuilder switch v := arg.(type) { case *acl.EnterpriseMeta: @@ -58,12 +58,7 @@ func prefixIndexFromQueryNoNamespace(arg interface{}) ([]byte, error) { // indexFromAuthMethodQuery builds an index key where Query.Value is lowercase, and is // a required value. -func indexFromAuthMethodQuery(arg interface{}) ([]byte, error) { - q, ok := arg.(AuthMethodQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for Query index", arg) - } - +func indexFromAuthMethodQuery(q AuthMethodQuery) ([]byte, error) { var b indexBuilder b.String(strings.ToLower(q.Value)) return b.Bytes(), nil diff --git a/agent/consul/state/schema.go b/agent/consul/state/schema.go index c60bea856..428214bc0 100644 --- a/agent/consul/state/schema.go +++ b/agent/consul/state/schema.go @@ -84,7 +84,7 @@ func indexTableSchema() *memdb.TableSchema { Name: indexID, AllowMissing: false, Unique: true, - Indexer: indexerSingle{ + Indexer: indexerSingle[string, *IndexEntry]{ readIndex: indexFromString, writeIndex: indexNameFromIndexEntry, }, @@ -93,39 +93,37 @@ func indexTableSchema() *memdb.TableSchema { } } -func indexNameFromIndexEntry(raw interface{}) ([]byte, error) { - p, ok := raw.(*IndexEntry) - if !ok { - return nil, fmt.Errorf("unexpected type %T for IndexEntry index", raw) - } - - if p.Key == "" { +func indexNameFromIndexEntry(e *IndexEntry) ([]byte, error) { + if e.Key == "" { return nil, errMissingValueForIndex } var b indexBuilder - b.String(strings.ToLower(p.Key)) + b.String(strings.ToLower(e.Key)) return b.Bytes(), nil } -func indexFromString(raw interface{}) ([]byte, error) { - q, ok := raw.(string) - if !ok { - return nil, fmt.Errorf("unexpected type %T for string prefix query", raw) - } - +func indexFromString(s string) ([]byte, error) { var b indexBuilder - b.String(strings.ToLower(q)) + b.String(strings.ToLower(s)) return b.Bytes(), nil } -func indexDeletedFromBoolQuery(raw interface{}) ([]byte, error) { - q, ok := raw.(BoolQuery) - if !ok { - return nil, fmt.Errorf("unexpected type %T for BoolQuery index", raw) - } - +func indexDeletedFromBoolQuery(q BoolQuery) ([]byte, error) { var b indexBuilder b.Bool(q.Value) return b.Bytes(), nil } + +type enterpriseIndexable interface { + partitionIndexable + namespaceIndexable +} + +type partitionIndexable interface { + PartitionOrDefault() string +} + +type namespaceIndexable interface { + NamespaceOrDefault() string +} diff --git a/agent/consul/state/session.go b/agent/consul/state/session.go index cf2e78b6e..72d079166 100644 --- a/agent/consul/state/session.go +++ b/agent/consul/state/session.go @@ -19,12 +19,7 @@ const ( indexNodeCheck = "node_check" ) -func indexFromSession(raw interface{}) ([]byte, error) { - e, ok := raw.(*structs.Session) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement *structs.Session", raw) - } - +func indexFromSession(e *structs.Session) ([]byte, error) { v := strings.ToLower(e.ID) if v == "" { return nil, errMissingValueForIndex @@ -86,12 +81,7 @@ func sessionChecksTableSchema() *memdb.TableSchema { } // indexNodeFromSession creates an index key from *structs.Session -func indexNodeFromSession(raw interface{}) ([]byte, error) { - e, ok := raw.(*structs.Session) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement *structs.Session", raw) - } - +func indexNodeFromSession(e *structs.Session) ([]byte, error) { v := strings.ToLower(e.Node) if v == "" { return nil, errMissingValueForIndex @@ -103,12 +93,7 @@ func indexNodeFromSession(raw interface{}) ([]byte, error) { } // indexFromNodeCheckIDSession creates an index key from sessionCheck -func indexFromNodeCheckIDSession(raw interface{}) ([]byte, error) { - e, ok := raw.(*sessionCheck) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement sessionCheck", raw) - } - +func indexFromNodeCheckIDSession(e *sessionCheck) ([]byte, error) { var b indexBuilder v := strings.ToLower(e.Node) if v == "" { @@ -132,12 +117,7 @@ func indexFromNodeCheckIDSession(raw interface{}) ([]byte, error) { } // indexSessionCheckFromSession creates an index key from sessionCheck -func indexSessionCheckFromSession(raw interface{}) ([]byte, error) { - e, ok := raw.(*sessionCheck) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement *sessionCheck", raw) - } - +func indexSessionCheckFromSession(e *sessionCheck) ([]byte, error) { var b indexBuilder v := strings.ToLower(e.Session) if v == "" { diff --git a/agent/consul/state/session_oss.go b/agent/consul/state/session_oss.go index 96622387e..40e0b280a 100644 --- a/agent/consul/state/session_oss.go +++ b/agent/consul/state/session_oss.go @@ -14,48 +14,44 @@ import ( "github.com/hashicorp/consul/api" ) -func sessionIndexer() indexerSingleWithPrefix { - return indexerSingleWithPrefix{ - readIndex: readIndex(indexFromQuery), - writeIndex: writeIndex(indexFromSession), - prefixIndex: prefixIndex(prefixIndexFromQuery), +func sessionIndexer() indexerSingleWithPrefix[Query, *structs.Session, any] { + return indexerSingleWithPrefix[Query, *structs.Session, any]{ + readIndex: indexFromQuery, + writeIndex: indexFromSession, + prefixIndex: prefixIndexFromQuery, } } -func nodeSessionsIndexer() indexerSingle { - return indexerSingle{ - readIndex: readIndex(indexFromIDValueLowerCase), - writeIndex: writeIndex(indexNodeFromSession), +func nodeSessionsIndexer() indexerSingle[singleValueID, *structs.Session] { + return indexerSingle[singleValueID, *structs.Session]{ + readIndex: indexFromIDValueLowerCase, + writeIndex: indexNodeFromSession, } } -func idCheckIndexer() indexerSingle { - return indexerSingle{ +func idCheckIndexer() indexerSingle[*sessionCheck, *sessionCheck] { + return indexerSingle[*sessionCheck, *sessionCheck]{ readIndex: indexFromNodeCheckIDSession, writeIndex: indexFromNodeCheckIDSession, } } -func sessionCheckIndexer() indexerSingle { - return indexerSingle{ +func sessionCheckIndexer() indexerSingle[Query, *sessionCheck] { + return indexerSingle[Query, *sessionCheck]{ readIndex: indexFromQuery, writeIndex: indexSessionCheckFromSession, } } -func nodeChecksIndexer() indexerSingle { - return indexerSingle{ +func nodeChecksIndexer() indexerSingle[multiValueID, *sessionCheck] { + return indexerSingle[multiValueID, *sessionCheck]{ readIndex: indexFromMultiValueID, writeIndex: indexFromNodeCheckID, } } // indexFromNodeCheckID creates an index key from a sessionCheck structure -func indexFromNodeCheckID(raw interface{}) ([]byte, error) { - e, ok := raw.(*sessionCheck) - if !ok { - return nil, fmt.Errorf("unexpected type %T, does not implement *structs.Session", raw) - } +func indexFromNodeCheckID(e *sessionCheck) ([]byte, error) { var b indexBuilder v := strings.ToLower(e.Node) if v == "" { diff --git a/agent/consul/state/state_store.go b/agent/consul/state/state_store.go index d8aa98dd9..598409a2c 100644 --- a/agent/consul/state/state_store.go +++ b/agent/consul/state/state_store.go @@ -263,25 +263,25 @@ func (s *Store) Abandon() { } // maxIndex is a helper used to retrieve the highest known index -// amongst a set of tables in the db. -func (s *Store) maxIndex(tables ...string) uint64 { +// amongst a set of index keys (e.g. table names) in the db. +func (s *Store) maxIndex(keys ...string) uint64 { tx := s.db.Txn(false) defer tx.Abort() - return maxIndexTxn(tx, tables...) + return maxIndexTxn(tx, keys...) } // maxIndexTxn is a helper used to retrieve the highest known index -// amongst a set of tables in the db. -func maxIndexTxn(tx ReadTxn, tables ...string) uint64 { - return maxIndexWatchTxn(tx, nil, tables...) +// amongst a set of index keys (e.g. table names) in the db. +func maxIndexTxn(tx ReadTxn, keys ...string) uint64 { + return maxIndexWatchTxn(tx, nil, keys...) } -func maxIndexWatchTxn(tx ReadTxn, ws memdb.WatchSet, tables ...string) uint64 { +func maxIndexWatchTxn(tx ReadTxn, ws memdb.WatchSet, keys ...string) uint64 { var lindex uint64 - for _, table := range tables { - ch, ti, err := tx.FirstWatch(tableIndex, "id", table) + for _, key := range keys { + ch, ti, err := tx.FirstWatch(tableIndex, "id", key) if err != nil { - panic(fmt.Sprintf("unknown index: %s err: %s", table, err)) + panic(fmt.Sprintf("unknown index: %s err: %s", key, err)) } if idx, ok := ti.(*IndexEntry); ok && idx.Value > lindex { lindex = idx.Value diff --git a/agent/consul/watch/mock_StateStore_test.go b/agent/consul/watch/mock_StateStore_test.go new file mode 100644 index 000000000..08d58e2f0 --- /dev/null +++ b/agent/consul/watch/mock_StateStore_test.go @@ -0,0 +1,40 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package watch + +import ( + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// MockStateStore is an autogenerated mock type for the StateStore type +type MockStateStore struct { + mock.Mock +} + +// AbandonCh provides a mock function with given fields: +func (_m *MockStateStore) AbandonCh() <-chan struct{} { + ret := _m.Called() + + var r0 <-chan struct{} + if rf, ok := ret.Get(0).(func() <-chan struct{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan struct{}) + } + } + + return r0 +} + +// NewMockStateStore creates a new instance of MockStateStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockStateStore(t testing.TB) *MockStateStore { + mock := &MockStateStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/agent/consul/watch/server_local.go b/agent/consul/watch/server_local.go new file mode 100644 index 000000000..589f5d644 --- /dev/null +++ b/agent/consul/watch/server_local.go @@ -0,0 +1,342 @@ +package watch + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/hashicorp/consul/lib/retry" + "github.com/hashicorp/go-memdb" + hashstructure_v2 "github.com/mitchellh/hashstructure/v2" +) + +var ( + ErrorNotFound = errors.New("no data found for query") + ErrorNotChanged = errors.New("data did not change for query") + + errNilContext = errors.New("cannot call ServerLocalNotify with a nil context") + errNilGetStore = errors.New("cannot call ServerLocalNotify without a callback to get a StateStore") + errNilQuery = errors.New("cannot call ServerLocalNotify without a callback to perform the query") + errNilNotify = errors.New("cannot call ServerLocalNotify without a callback to send notifications") +) + +//go:generate mockery --name StateStore --inpackage --testonly +type StateStore interface { + AbandonCh() <-chan struct{} +} + +const ( + defaultWaiterMinFailures uint = 1 + defaultWaiterMinWait = time.Second + defaultWaiterMaxWait = 60 * time.Second + defaultWaiterFactor = 2 * time.Second +) + +var ( + defaultWaiterJitter = retry.NewJitter(100) +) + +func defaultWaiter() *retry.Waiter { + return &retry.Waiter{ + MinFailures: defaultWaiterMinFailures, + MinWait: defaultWaiterMinWait, + MaxWait: defaultWaiterMaxWait, + Jitter: defaultWaiterJitter, + Factor: defaultWaiterFactor, + } +} + +// noopDone can be passed to serverLocalNotifyWithWaiter +func noopDone() {} + +// ServerLocalBlockingQuery performs a blocking query similar to the pre-existing blockingQuery +// method on the agent/consul.Server type. There are a few key differences. +// +// 1. This function makes use of Go 1.18 generics. The function is parameterized with two +// types. The first is the ResultType which can be anything. Having this be parameterized +// instead of using interface{} allows us to simplify the call sites so that no type +// coercion from interface{} to the real type is necessary. The second parameterized type +// is something that VERY loosely resembles a agent/consul/state.Store type. The StateStore +// interface in this package has a single method to get the stores abandon channel so we +// know when a snapshot restore is occurring and can act accordingly. We could have not +// parameterized this type and used a real *state.Store instead but then we would have +// concrete dependencies on the state package and it would make it a little harder to +// test this function. +// +// We could have also avoided the need to use a ResultType parameter by taking the route +// the original blockingQuery method did and to just assume all callers close around +// a pointer to their results and can modify it as necessary. That way of doing things +// feels a little gross so I have taken this one a different direction. The old way +// also gets especially gross with how we have to push concerns of spurious wakeup +// suppression down into every call site. +// +// 2. This method has no internal timeout and can potentially run forever until a state +// change is observed. If there is a desire to have a timeout, that should be built into +// the context.Context passed as the first argument. +// +// 3. This method bakes in some newer functionality around hashing of results to prevent sending +// back data when nothing has actually changed. With the old blockingQuery method this has to +// be done within the closure passed to the method which means the same bit of code is duplicated +// in many places. As this functionality isn't necessary in many scenarios whether to opt-in to +// that behavior is a argument to this function. +// +// Similar to the older method: +// +// 1. Errors returned from the query will be propagated back to the caller. +// +// The query function must follow these rules: +// +// 1. To access data it must use the passed in StoreType (which will be a state.Store when +// everything gets stiched together outside of unit tests). +// 2. It must return an index greater than the minIndex if the results returned by the query +// have changed. +// 3. Any channels added to the memdb.WatchSet must unblock when the results +// returned by the query have changed. +// +// To ensure optimal performance of the query, the query function should make a +// best-effort attempt to follow these guidelines: +// +// 1. Only return an index greater than the minIndex. +// 2. Any channels added to the memdb.WatchSet should only unblock when the +// results returned by the query have changed. This might be difficult +// to do when blocking on non-existent data. +// +func ServerLocalBlockingQuery[ResultType any, StoreType StateStore]( + ctx context.Context, + getStore func() StoreType, + minIndex uint64, + suppressSpuriousWakeup bool, + query func(memdb.WatchSet, StoreType) (uint64, ResultType, error), +) (uint64, ResultType, error) { + var ( + notFound bool + ranOnce bool + priorHash uint64 + ) + + var zeroResult ResultType + if getStore == nil { + return 0, zeroResult, fmt.Errorf("no getStore function was provided to ServerLocalBlockingQuery") + } + if query == nil { + return 0, zeroResult, fmt.Errorf("no query function was provided to ServerLocalBlockingQuery") + } + + for { + state := getStore() + + ws := memdb.NewWatchSet() + + // Adding the AbandonCh to the WatchSet allows us to detect when + // a snapshot restore happens that would otherwise not modify anything + // within the individual state store. If we didn't do this then we + // could end up blocking indefinitely. + ws.Add(state.AbandonCh()) + + index, result, err := query(ws, state) + // Always set a non-zero index. Generally we expect the index + // to be set to Raft index which can never be 0. If the query + // returned no results we expect it to be set to the max index of the table, + // however we can't guarantee this always happens. + // To prevent a client from accidentally performing many non-blocking queries + // (which causes lots of unnecessary load), we always set a default value of 1. + // This is sufficient to prevent the unnecessary load in most cases. + if index < 1 { + index = 1 + } + + switch { + case errors.Is(err, ErrorNotFound): + // if minIndex is 0 then we should never block but we + // also should not propagate the error + if minIndex == 0 { + return index, result, nil + } + + // update the min index if the previous result was not found. This + // is an attempt to not return data unnecessarily when we end up + // watching the root of a memdb Radix tree because the data being + // watched doesn't exist yet. + if notFound { + minIndex = index + } + + notFound = true + case err != nil: + return index, result, err + } + + // when enabled we can prevent sending back data that hasn't changed. + if suppressSpuriousWakeup { + newHash, err := hashstructure_v2.Hash(result, hashstructure_v2.FormatV2, nil) + if err != nil { + return index, result, fmt.Errorf("error hashing data for spurious wakeup suppression: %w", err) + } + + // set minIndex to the returned index to prevent sending back identical data + if ranOnce && priorHash == newHash { + minIndex = index + } + ranOnce = true + priorHash = newHash + } + + // one final check if we should be considered unblocked and + // return the value. Some conditions in the switch above + // alter the minIndex and prevent this return if it would + // be desirable. One such case is when the actual data has + // not changed since the last round through the query and + // we would rather not do any further processing for unchanged + // data. This mostly protects against watches for data that + // doesn't exist from return the non-existant value constantly. + if index > minIndex { + return index, result, nil + } + + // Block until something changes. Because we have added the state + // stores AbandonCh to this watch set, a snapshot restore will + // cause things to unblock in addition to changes to the actual + // queried data. + if err := ws.WatchCtx(ctx); err != nil { + // exit if the context was cancelled + return index, result, nil + } + + select { + case <-state.AbandonCh(): + return index, result, nil + default: + } + } +} + +// ServerLocalNotify will watch for changes in the State Store using the provided +// query function and invoke the notify callback whenever the results of that query +// function have changed. This function will return an error if parameter validations +// fail but otherwise the background go routine to process the notifications will +// be spawned and nil will be returned. Just like ServerLocalBlockingQuery this makes +// use of Go Generics and for the same reasons as outlined in the documentation for +// that function. +func ServerLocalNotify[ResultType any, StoreType StateStore]( + ctx context.Context, + correlationID string, + getStore func() StoreType, + query func(memdb.WatchSet, StoreType) (uint64, ResultType, error), + notify func(ctx context.Context, correlationID string, result ResultType, err error), +) error { + return serverLocalNotify( + ctx, + correlationID, + getStore, + query, + notify, + // Public callers should not need to know when the internal go routines are finished. + // Being able to provide a done function to the internal version of this function is + // to allow our tests to be more determinstic and to eliminate arbitrary sleeps. + noopDone, + // Public callers do not get to override the error backoff configuration. Internally + // we want to allow for this to enable our unit tests to run much more quickly. + defaultWaiter(), + ) +} + +// serverLocalNotify is the internal version of ServerLocalNotify. It takes +// two additional arguments of the waiter to use and a function to call +// when the notification go routine has finished +func serverLocalNotify[ResultType any, StoreType StateStore]( + ctx context.Context, + correlationID string, + getStore func() StoreType, + query func(memdb.WatchSet, StoreType) (uint64, ResultType, error), + notify func(ctx context.Context, correlationID string, result ResultType, err error), + done func(), + waiter *retry.Waiter, +) error { + if ctx == nil { + return errNilContext + } + + if getStore == nil { + return errNilGetStore + } + + if query == nil { + return errNilQuery + } + + if notify == nil { + return errNilNotify + } + + go serverLocalNotifyRoutine( + ctx, + correlationID, + getStore, + query, + notify, + done, + waiter, + ) + return nil +} + +// serverLocalNotifyRoutine is the function intended to be run within a new +// go routine to process the updates. It will not check to ensure callbacks +// are non-nil nor perform other parameter validation. It is assumed that +// the in-package caller of this method will have already done that. It also +// takes the backoff waiter in as an argument so that unit tests within this +// package can override the default values that the exported ServerLocalNotify +// function would have set up. +func serverLocalNotifyRoutine[ResultType any, StoreType StateStore]( + ctx context.Context, + correlationID string, + getStore func() StoreType, + query func(memdb.WatchSet, StoreType) (uint64, ResultType, error), + notify func(ctx context.Context, correlationID string, result ResultType, err error), + done func(), + waiter *retry.Waiter, +) { + defer done() + + var minIndex uint64 + + for { + // Check if the context has been cancelled. Do not issue + // more queries if it has been cancelled. + if ctx.Err() != nil { + return + } + + // Perform the blocking query + index, result, err := ServerLocalBlockingQuery(ctx, getStore, minIndex, true, query) + + // Check if the context has been cancelled. If it has we should not send more + // notifications. + if ctx.Err() != nil { + return + } + + // Check the index to see if we should call notify + if minIndex == 0 || minIndex < index { + notify(ctx, correlationID, result, err) + minIndex = index + } + + // Handle errors with backoff. Badly behaved blocking calls that returned + // a zero index are considered as failures since we need to not get stuck + // in a busy loop. + if err == nil && index > 0 { + waiter.Reset() + } else { + if waiter.Wait(ctx) != nil { + return + } + } + + // ensure we don't use zero indexes + if err == nil && minIndex < 1 { + minIndex = 1 + } + } +} diff --git a/agent/consul/watch/server_local_test.go b/agent/consul/watch/server_local_test.go new file mode 100644 index 000000000..cd803ceba --- /dev/null +++ b/agent/consul/watch/server_local_test.go @@ -0,0 +1,454 @@ +package watch + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hashicorp/consul/lib/retry" + "github.com/hashicorp/go-memdb" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type mockStoreProvider struct { + mock.Mock +} + +func newMockStoreProvider(t *testing.T) *mockStoreProvider { + t.Helper() + provider := &mockStoreProvider{} + t.Cleanup(func() { + provider.AssertExpectations(t) + }) + return provider +} + +func (m *mockStoreProvider) getStore() *MockStateStore { + return m.Called().Get(0).(*MockStateStore) +} + +type testResult struct { + value string +} + +func (m *mockStoreProvider) query(ws memdb.WatchSet, store *MockStateStore) (uint64, *testResult, error) { + ret := m.Called(ws, store) + + index := ret.Get(0).(uint64) + result := ret.Get(1).(*testResult) + err := ret.Error(2) + + return index, result, err +} + +func (m *mockStoreProvider) notify(ctx context.Context, correlationID string, result *testResult, err error) { + m.Called(ctx, correlationID, result, err) +} + +func TestServerLocalBlockingQuery_getStoreNotProvided(t *testing.T) { + _, _, err := ServerLocalBlockingQuery( + context.Background(), + nil, + 0, + true, + func(memdb.WatchSet, *MockStateStore) (uint64, struct{}, error) { + return 0, struct{}{}, nil + }, + ) + + require.Error(t, err) + require.Contains(t, err.Error(), "no getStore function was provided") +} + +func TestServerLocalBlockingQuery_queryNotProvided(t *testing.T) { + var query func(memdb.WatchSet, *MockStateStore) (uint64, struct{}, error) + _, _, err := ServerLocalBlockingQuery( + context.Background(), + func() *MockStateStore { return nil }, + 0, + true, + query, + ) + + require.Error(t, err) + require.Contains(t, err.Error(), "no query function was provided") +} + +func TestServerLocalBlockingQuery_NonBlocking(t *testing.T) { + abandonCh := make(chan struct{}) + t.Cleanup(func() { close(abandonCh) }) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Once() + + provider := newMockStoreProvider(t) + provider.On("getStore").Return(store).Once() + provider.On("query", mock.Anything, store). + Return(uint64(1), &testResult{value: "foo"}, nil). + Once() + + idx, result, err := ServerLocalBlockingQuery( + context.Background(), + provider.getStore, + 0, + true, + provider.query, + ) + require.NoError(t, err) + require.EqualValues(t, 1, idx) + require.Equal(t, &testResult{value: "foo"}, result) +} + +func TestServerLocalBlockingQuery_Index0(t *testing.T) { + abandonCh := make(chan struct{}) + t.Cleanup(func() { close(abandonCh) }) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Once() + + provider := newMockStoreProvider(t) + provider.On("getStore").Return(store).Once() + provider.On("query", mock.Anything, store). + // the index 0 returned here should get translated to 1 by ServerLocalBlockingQuery + Return(uint64(0), &testResult{value: "foo"}, nil). + Once() + + idx, result, err := ServerLocalBlockingQuery( + context.Background(), + provider.getStore, + 0, + true, + provider.query, + ) + require.NoError(t, err) + require.EqualValues(t, 1, idx) + require.Equal(t, &testResult{value: "foo"}, result) +} + +func TestServerLocalBlockingQuery_NotFound(t *testing.T) { + abandonCh := make(chan struct{}) + t.Cleanup(func() { close(abandonCh) }) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Once() + + provider := newMockStoreProvider(t) + provider.On("getStore"). + Return(store). + Once() + + var nilResult *testResult + provider.On("query", mock.Anything, store). + Return(uint64(1), nilResult, ErrorNotFound). + Once() + + idx, result, err := ServerLocalBlockingQuery( + context.Background(), + provider.getStore, + 0, + true, + provider.query, + ) + require.NoError(t, err) + require.EqualValues(t, 1, idx) + require.Nil(t, result) +} + +func TestServerLocalBlockingQuery_NotFoundBlocks(t *testing.T) { + abandonCh := make(chan struct{}) + t.Cleanup(func() { close(abandonCh) }) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Times(5) + + provider := newMockStoreProvider(t) + provider.On("getStore"). + Return(store). + Times(3) + + var nilResult *testResult + // Initial data returned is not found and has an index less than the original + // blocking index. This should not return data to the caller. + provider.On("query", mock.Anything, store). + Return(uint64(4), nilResult, ErrorNotFound). + Run(addReadyWatchSet). + Once() + // There is an update to the data but the value still doesn't exist. Therefore + // we should not return data to the caller. + provider.On("query", mock.Anything, store). + Return(uint64(6), nilResult, ErrorNotFound). + Run(addReadyWatchSet). + Once() + // Finally we have some real data and can return it to the caller. + provider.On("query", mock.Anything, store). + Return(uint64(7), &testResult{value: "foo"}, nil). + Once() + + idx, result, err := ServerLocalBlockingQuery( + context.Background(), + provider.getStore, + 5, + true, + provider.query, + ) + require.NoError(t, err) + require.EqualValues(t, 7, idx) + require.Equal(t, &testResult{value: "foo"}, result) +} + +func TestServerLocalBlockingQuery_Error(t *testing.T) { + abandonCh := make(chan struct{}) + t.Cleanup(func() { close(abandonCh) }) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Once() + + provider := newMockStoreProvider(t) + provider.On("getStore"). + Return(store). + Once() + + var nilResult *testResult + provider.On("query", mock.Anything, store). + Return(uint64(10), nilResult, fmt.Errorf("synthetic error")). + Once() + + idx, result, err := ServerLocalBlockingQuery( + context.Background(), + provider.getStore, + 4, + true, + provider.query, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "synthetic error") + require.EqualValues(t, 10, idx) + require.Nil(t, result) +} + +func TestServerLocalBlockingQuery_ContextCancellation(t *testing.T) { + abandonCh := make(chan struct{}) + t.Cleanup(func() { close(abandonCh) }) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Once() + + provider := newMockStoreProvider(t) + provider.On("getStore"). + Return(store). + Once() + provider.On("query", mock.Anything, store). + // Return an index that should not cause the blocking query to return. + Return(uint64(4), &testResult{value: "foo"}, nil). + Once(). + Run(func(_ mock.Arguments) { + // Cancel the context so that the memdb WatchCtx call will error. + cancel() + }) + + idx, result, err := ServerLocalBlockingQuery( + ctx, + provider.getStore, + 8, + true, + provider.query, + ) + // The internal cancellation error should not be propagated. + require.NoError(t, err) + require.EqualValues(t, 4, idx) + require.Equal(t, &testResult{value: "foo"}, result) +} + +func TestServerLocalBlockingQuery_StateAbandoned(t *testing.T) { + abandonCh := make(chan struct{}) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Twice() + + provider := newMockStoreProvider(t) + provider.On("getStore"). + Return(store). + Once() + provider.On("query", mock.Anything, store). + // Return an index that should not cause the blocking query to return. + Return(uint64(4), &testResult{value: "foo"}, nil). + Once(). + Run(func(_ mock.Arguments) { + // Cancel the context so that the memdb WatchCtx call will error. + close(abandonCh) + }) + + idx, result, err := ServerLocalBlockingQuery( + context.Background(), + provider.getStore, + 8, + true, + provider.query, + ) + // The internal cancellation error should not be propagated. + require.NoError(t, err) + require.EqualValues(t, 4, idx) + require.Equal(t, &testResult{value: "foo"}, result) +} + +func TestServerLocalNotify_Validations(t *testing.T) { + provider := newMockStoreProvider(t) + + type testCase struct { + ctx context.Context + getStore func() *MockStateStore + query func(memdb.WatchSet, *MockStateStore) (uint64, *testResult, error) + notify func(context.Context, string, *testResult, error) + err error + } + + cases := map[string]testCase{ + "nil-context": { + getStore: provider.getStore, + query: provider.query, + notify: provider.notify, + err: errNilContext, + }, + "nil-getStore": { + ctx: context.Background(), + query: provider.query, + notify: provider.notify, + err: errNilGetStore, + }, + "nil-query": { + ctx: context.Background(), + getStore: provider.getStore, + notify: provider.notify, + err: errNilQuery, + }, + "nil-notify": { + ctx: context.Background(), + getStore: provider.getStore, + query: provider.query, + err: errNilNotify, + }, + } + + for name, tcase := range cases { + t.Run(name, func(t *testing.T) { + err := ServerLocalNotify(tcase.ctx, "test", tcase.getStore, tcase.query, tcase.notify) + require.ErrorIs(t, err, tcase.err) + }) + } +} + +func TestServerLocalNotify(t *testing.T) { + notifyCtx, notifyCancel := context.WithCancel(context.Background()) + t.Cleanup(notifyCancel) + + abandonCh := make(chan struct{}) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Times(3) + + provider := newMockStoreProvider(t) + provider.On("getStore"). + Return(store). + Times(3) + provider.On("query", mock.Anything, store). + Return(uint64(4), &testResult{value: "foo"}, nil). + Once() + provider.On("notify", notifyCtx, t.Name(), &testResult{value: "foo"}, nil).Once() + provider.On("query", mock.Anything, store). + Return(uint64(6), &testResult{value: "bar"}, nil). + Once() + provider.On("notify", notifyCtx, t.Name(), &testResult{value: "bar"}, nil).Once() + provider.On("query", mock.Anything, store). + Return(uint64(7), &testResult{value: "baz"}, context.Canceled). + Run(func(mock.Arguments) { + notifyCancel() + }) + + doneCtx, routineDone := context.WithCancel(context.Background()) + err := serverLocalNotify(notifyCtx, t.Name(), provider.getStore, provider.query, provider.notify, routineDone, defaultWaiter()) + require.NoError(t, err) + + // Wait for the context cancellation which will happen when the "query" func is run the third time. The doneCtx gets "cancelled" + // by the backgrounded go routine when it is actually finished. We need to wait for this to ensure that all mocked calls have been + // made and that no extra calls get made. + <-doneCtx.Done() +} + +func TestServerLocalNotify_internal(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + abandonCh := make(chan struct{}) + + store := NewMockStateStore(t) + store.On("AbandonCh"). + Return(closeChan(abandonCh)). + Times(4) + + var nilResult *testResult + + provider := newMockStoreProvider(t) + provider.On("getStore"). + Return(store). + Times(4) + provider.On("query", mock.Anything, store). + Return(uint64(0), nilResult, fmt.Errorf("injected error")). + Times(3) + // we should only notify the first time as the index of 1 wont exceed the min index + // after the second two queries. + provider.On("notify", ctx, "test", nilResult, fmt.Errorf("injected error")). + Once() + provider.On("query", mock.Anything, store). + Return(uint64(7), &testResult{value: "foo"}, nil). + Once() + provider.On("notify", ctx, "test", &testResult{value: "foo"}, nil). + Once(). + Run(func(mock.Arguments) { + cancel() + }) + waiter := retry.Waiter{ + MinFailures: 1, + MinWait: time.Millisecond, + MaxWait: 50 * time.Millisecond, + Jitter: retry.NewJitter(100), + Factor: 2 * time.Millisecond, + } + + // all the mock expectations should ensure things are working properly + serverLocalNotifyRoutine(ctx, "test", provider.getStore, provider.query, provider.notify, noopDone, &waiter) +} + +func addReadyWatchSet(args mock.Arguments) { + ws := args.Get(0).(memdb.WatchSet) + ch := make(chan struct{}) + ws.Add(ch) + close(ch) +} + +// small convenience to make this more readable. The alternative in a few +// cases would be to do something like (<-chan struct{})(ch). I find that +// syntax very difficult to read. +func closeChan(ch chan struct{}) <-chan struct{} { + return ch +} diff --git a/agent/intentions_endpoint.go b/agent/intentions_endpoint.go index d21833a8f..f43dc3ecf 100644 --- a/agent/intentions_endpoint.go +++ b/agent/intentions_endpoint.go @@ -145,15 +145,15 @@ func (s *HTTPHandlers) IntentionMatch(resp http.ResponseWriter, req *http.Reques // order of the returned responses. args.Match.Entries = make([]structs.IntentionMatchEntry, len(names)) for i, n := range names { - ap, ns, name, err := parseIntentionStringComponent(n, &entMeta) + parsed, err := parseIntentionStringComponent(n, &entMeta, false) if err != nil { return nil, fmt.Errorf("name %q is invalid: %s", n, err) } args.Match.Entries[i] = structs.IntentionMatchEntry{ - Partition: ap, - Namespace: ns, - Name: name, + Partition: parsed.ap, + Namespace: parsed.ns, + Name: parsed.name, } } @@ -235,23 +235,23 @@ func (s *HTTPHandlers) IntentionCheck(resp http.ResponseWriter, req *http.Reques // We parse them the same way as matches to extract partition/namespace/name args.Check.SourceName = source[0] if args.Check.SourceType == structs.IntentionSourceConsul { - ap, ns, name, err := parseIntentionStringComponent(source[0], &entMeta) + parsed, err := parseIntentionStringComponent(source[0], &entMeta, false) if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } - args.Check.SourcePartition = ap - args.Check.SourceNS = ns - args.Check.SourceName = name + args.Check.SourcePartition = parsed.ap + args.Check.SourceNS = parsed.ns + args.Check.SourceName = parsed.name } // The destination is always in the Consul format - ap, ns, name, err := parseIntentionStringComponent(destination[0], &entMeta) + parsed, err := parseIntentionStringComponent(destination[0], &entMeta, false) if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } - args.Check.DestinationPartition = ap - args.Check.DestinationNS = ns - args.Check.DestinationName = name + args.Check.DestinationPartition = parsed.ap + args.Check.DestinationNS = parsed.ns + args.Check.DestinationName = parsed.name var reply structs.IntentionQueryCheckResponse if err := s.agent.RPC("Intention.Check", args, &reply); err != nil { @@ -302,23 +302,25 @@ func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Req } { - ap, ns, name, err := parseIntentionStringComponent(source[0], &entMeta) + parsed, err := parseIntentionStringComponent(source[0], &entMeta, true) if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } - args.Exact.SourcePartition = ap - args.Exact.SourceNS = ns - args.Exact.SourceName = name + + args.Exact.SourcePeer = parsed.peer + args.Exact.SourcePartition = parsed.ap + args.Exact.SourceNS = parsed.ns + args.Exact.SourceName = parsed.name } { - ap, ns, name, err := parseIntentionStringComponent(destination[0], &entMeta) + parsed, err := parseIntentionStringComponent(destination[0], &entMeta, false) if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } - args.Exact.DestinationPartition = ap - args.Exact.DestinationNS = ns - args.Exact.DestinationName = name + args.Exact.DestinationPartition = parsed.ap + args.Exact.DestinationNS = parsed.ns + args.Exact.DestinationName = parsed.name } var reply structs.IndexedIntentions @@ -444,42 +446,67 @@ func parseIntentionQueryExact(req *http.Request, entMeta *acl.EnterpriseMeta) (* var exact structs.IntentionQueryExact { - ap, ns, name, err := parseIntentionStringComponent(source[0], entMeta) + parsed, err := parseIntentionStringComponent(source[0], entMeta, false) if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } - exact.SourcePartition = ap - exact.SourceNS = ns - exact.SourceName = name + exact.SourcePartition = parsed.ap + exact.SourceNS = parsed.ns + exact.SourceName = parsed.name } { - ap, ns, name, err := parseIntentionStringComponent(destination[0], entMeta) + parsed, err := parseIntentionStringComponent(destination[0], entMeta, false) if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } - exact.DestinationPartition = ap - exact.DestinationNS = ns - exact.DestinationName = name + exact.DestinationPartition = parsed.ap + exact.DestinationNS = parsed.ns + exact.DestinationName = parsed.name } return &exact, nil } -func parseIntentionStringComponent(input string, entMeta *acl.EnterpriseMeta) (string, string, string, error) { +type parsedIntentionInput struct { + peer, ap, ns, name string +} + +func parseIntentionStringComponent(input string, entMeta *acl.EnterpriseMeta, allowPeerKeyword bool) (*parsedIntentionInput, error) { + if strings.HasPrefix(input, "peer:") && !allowPeerKeyword { + return nil, fmt.Errorf("cannot specify a peer here") + } + ss := strings.Split(input, "/") switch len(ss) { case 1: // Name only + // need to specify at least the service name too + if strings.HasPrefix(ss[0], "peer:") { + return nil, fmt.Errorf("need to specify the service name as well") + } + ns := entMeta.NamespaceOrEmpty() ap := entMeta.PartitionOrEmpty() - return ap, ns, ss[0], nil - case 2: // namespace/name + return &parsedIntentionInput{ap: ap, ns: ns, name: ss[0]}, nil + case 2: // peer:peer/name OR namespace/name + if strings.HasPrefix(ss[0], "peer:") { + peerName := strings.TrimPrefix(ss[0], "peer:") + ns := entMeta.NamespaceOrEmpty() + + return &parsedIntentionInput{peer: peerName, ns: ns, name: ss[1]}, nil + } + ap := entMeta.PartitionOrEmpty() - return ap, ss[0], ss[1], nil - case 3: // partition/namespace/name - return ss[0], ss[1], ss[2], nil + return &parsedIntentionInput{ap: ap, ns: ss[0], name: ss[1]}, nil + case 3: // peer:peer/namespace/name OR partition/namespace/name + if strings.HasPrefix(ss[0], "peer:") { + peerName := strings.TrimPrefix(ss[0], "peer:") + return &parsedIntentionInput{peer: peerName, ns: ss[1], name: ss[2]}, nil + } else { + return &parsedIntentionInput{ap: ss[0], ns: ss[1], name: ss[2]}, nil + } default: - return "", "", "", fmt.Errorf("input can contain at most two '/'") + return nil, fmt.Errorf("input can contain at most two '/'") } } diff --git a/agent/intentions_endpoint_test.go b/agent/intentions_endpoint_test.go index ef54ccce3..148e48f8f 100644 --- a/agent/intentions_endpoint_test.go +++ b/agent/intentions_endpoint_test.go @@ -349,6 +349,57 @@ func TestIntentionCheck(t *testing.T) { }) } +func TestIntentionGetExact_PeerIntentions(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := NewTestAgent(t, "") + testrpc.WaitForTestAgent(t, a.RPC, "dc1") + + testutil.RunStep(t, "create a peer intentions", func(t *testing.T) { + configEntryIntention := structs.ServiceIntentionsConfigEntry{ + Kind: structs.ServiceIntentions, + Name: "bar", + Sources: []*structs.SourceIntention{ + { + Name: "foo", + Peer: "peer1", + Action: structs.IntentionActionAllow, + }, + }, + } + + req, err := http.NewRequest("PUT", "/v1/config", jsonReader(configEntryIntention)) + require.NoError(t, err) + resp := httptest.NewRecorder() + + obj, err := a.srv.ConfigApply(resp, req) + require.NoError(t, err) + + applied, ok := obj.(bool) + require.True(t, ok) + require.True(t, applied) + }) + + t.Run("get peer intention", func(t *testing.T) { + req, err := http.NewRequest("GET", "/v1/connect/intentions/exact?source=peer:peer1/foo&destination=bar", nil) + require.NoError(t, err) + + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionExact(resp, req) + require.NoError(t, err) + require.NotNil(t, obj) + + value, ok := obj.(*structs.Intention) + require.True(t, ok) + require.Equal(t, "peer1", value.SourcePeer) + require.Equal(t, "foo", value.SourceName) + require.Equal(t, "bar", value.DestinationName) + }) +} func TestIntentionGetExact(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -828,6 +879,8 @@ func TestParseIntentionStringComponent(t *testing.T) { cases := []struct { TestName string Input string + AllowsPeers bool + ExpectedPeer string ExpectedAP string ExpectedNS string ExpectedName string @@ -866,20 +919,47 @@ func TestParseIntentionStringComponent(t *testing.T) { Input: "uhoh/blah/foo/bar", Err: true, }, + { + TestName: "peered without namespace", + Input: "peer:peer1/service_name", + AllowsPeers: true, + ExpectedPeer: "peer1", + ExpectedAP: "", + ExpectedNS: "", + ExpectedName: "service_name", + }, + { + TestName: "need to specify at least a service", + Input: "peer:peer1", + Err: true, + }, + { + TestName: "peered not allowed error", + Input: "peer:peer1/service_name", + AllowsPeers: false, + Err: true, + }, } for _, tc := range cases { t.Run(tc.TestName, func(t *testing.T) { var entMeta acl.EnterpriseMeta - ap, ns, name, err := parseIntentionStringComponent(tc.Input, &entMeta) + parsed, err := parseIntentionStringComponent(tc.Input, &entMeta, tc.AllowsPeers) if tc.Err { require.Error(t, err) } else { require.NoError(t, err) - assert.Equal(t, tc.ExpectedAP, ap) - assert.Equal(t, tc.ExpectedNS, ns) - assert.Equal(t, tc.ExpectedName, name) + if tc.AllowsPeers { + assert.Equal(t, tc.ExpectedPeer, parsed.peer) + assert.Equal(t, "", parsed.ap) + } else { + assert.Equal(t, tc.ExpectedAP, parsed.ap) + assert.Equal(t, "", parsed.peer) + } + + assert.Equal(t, tc.ExpectedNS, parsed.ns) + assert.Equal(t, tc.ExpectedName, parsed.name) } }) } diff --git a/agent/proxycfg/connect_proxy.go b/agent/proxycfg/connect_proxy.go index 0f62767c6..cdeea5ead 100644 --- a/agent/proxycfg/connect_proxy.go +++ b/agent/proxycfg/connect_proxy.go @@ -22,8 +22,8 @@ func (s *handlerConnectProxy) initialize(ctx context.Context) (ConfigSnapshot, e snap.ConnectProxy.WatchedDiscoveryChains = make(map[UpstreamID]context.CancelFunc) snap.ConnectProxy.WatchedUpstreams = make(map[UpstreamID]map[string]context.CancelFunc) snap.ConnectProxy.WatchedUpstreamEndpoints = make(map[UpstreamID]map[string]structs.CheckServiceNodes) - snap.ConnectProxy.WatchedPeerTrustBundles = make(map[string]context.CancelFunc) - snap.ConnectProxy.PeerTrustBundles = make(map[string]*pbpeering.PeeringTrustBundle) + snap.ConnectProxy.WatchedUpstreamPeerTrustBundles = make(map[string]context.CancelFunc) + snap.ConnectProxy.UpstreamPeerTrustBundles = make(map[string]*pbpeering.PeeringTrustBundle) snap.ConnectProxy.WatchedGateways = make(map[UpstreamID]map[string]context.CancelFunc) snap.ConnectProxy.WatchedGatewayEndpoints = make(map[UpstreamID]map[string]structs.CheckServiceNodes) snap.ConnectProxy.WatchedServiceChecks = make(map[structs.ServiceID][]structs.CheckType) @@ -212,7 +212,7 @@ func (s *handlerConnectProxy) initialize(ctx context.Context) (ConfigSnapshot, e } // Check whether a watch for this peer exists to avoid duplicates. - if _, ok := snap.ConnectProxy.WatchedPeerTrustBundles[uid.Peer]; !ok { + if _, ok := snap.ConnectProxy.WatchedUpstreamPeerTrustBundles[uid.Peer]; !ok { peerCtx, cancel := context.WithCancel(ctx) if err := s.dataSources.TrustBundle.Notify(peerCtx, &pbpeering.TrustBundleReadRequest{ Name: uid.Peer, @@ -222,7 +222,7 @@ func (s *handlerConnectProxy) initialize(ctx context.Context) (ConfigSnapshot, e return snap, fmt.Errorf("error while watching trust bundle for peer %q: %w", uid.Peer, err) } - snap.ConnectProxy.WatchedPeerTrustBundles[uid.Peer] = cancel + snap.ConnectProxy.WatchedUpstreamPeerTrustBundles[uid.Peer] = cancel } continue } @@ -270,7 +270,7 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u UpdateEvent, s } peer := strings.TrimPrefix(u.CorrelationID, peerTrustBundleIDPrefix) if resp.Bundle != nil { - snap.ConnectProxy.PeerTrustBundles[peer] = resp.Bundle + snap.ConnectProxy.UpstreamPeerTrustBundles[peer] = resp.Bundle } case u.CorrelationID == peeringTrustBundlesWatchID: @@ -279,9 +279,9 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u UpdateEvent, s return fmt.Errorf("invalid type for response: %T", u.Result) } if len(resp.Bundles) > 0 { - snap.ConnectProxy.PeeringTrustBundles = resp.Bundles + snap.ConnectProxy.InboundPeerTrustBundles = resp.Bundles } - snap.ConnectProxy.PeeringTrustBundlesSet = true + snap.ConnectProxy.InboundPeerTrustBundlesSet = true case u.CorrelationID == intentionsWatchID: resp, ok := u.Result.(*structs.IndexedIntentionMatches) diff --git a/agent/proxycfg/manager_test.go b/agent/proxycfg/manager_test.go index 08a54b11b..a62bbf2d3 100644 --- a/agent/proxycfg/manager_test.go +++ b/agent/proxycfg/manager_test.go @@ -238,7 +238,7 @@ func TestManager_BasicLifecycle(t *testing.T) { }, PassthroughUpstreams: map[UpstreamID]map[string]map[string]struct{}{}, PassthroughIndices: map[string]indexedTarget{}, - PeerTrustBundles: map[string]*pbpeering.PeeringTrustBundle{}, + UpstreamPeerTrustBundles: map[string]*pbpeering.PeeringTrustBundle{}, PeerUpstreamEndpoints: map[UpstreamID]structs.CheckServiceNodes{}, PeerUpstreamEndpointsUseHostnames: map[UpstreamID]struct{}{}, }, @@ -299,7 +299,7 @@ func TestManager_BasicLifecycle(t *testing.T) { }, PassthroughUpstreams: map[UpstreamID]map[string]map[string]struct{}{}, PassthroughIndices: map[string]indexedTarget{}, - PeerTrustBundles: map[string]*pbpeering.PeeringTrustBundle{}, + UpstreamPeerTrustBundles: map[string]*pbpeering.PeeringTrustBundle{}, PeerUpstreamEndpoints: map[UpstreamID]structs.CheckServiceNodes{}, PeerUpstreamEndpointsUseHostnames: map[UpstreamID]struct{}{}, }, diff --git a/agent/proxycfg/snapshot.go b/agent/proxycfg/snapshot.go index e405dd369..e56c169e8 100644 --- a/agent/proxycfg/snapshot.go +++ b/agent/proxycfg/snapshot.go @@ -44,13 +44,13 @@ type ConfigSnapshotUpstreams struct { // endpoints of an upstream. WatchedUpstreamEndpoints map[UpstreamID]map[string]structs.CheckServiceNodes - // WatchedPeerTrustBundles is a map of (PeerName -> CancelFunc) in order to cancel + // WatchedUpstreamPeerTrustBundles is a map of (PeerName -> CancelFunc) in order to cancel // watches for peer trust bundles any time the list of upstream peers changes. - WatchedPeerTrustBundles map[string]context.CancelFunc + WatchedUpstreamPeerTrustBundles map[string]context.CancelFunc - // PeerTrustBundles is a map of (PeerName -> PeeringTrustBundle). + // UpstreamPeerTrustBundles is a map of (PeerName -> PeeringTrustBundle). // It is used to store trust bundles for upstream TLS transport sockets. - PeerTrustBundles map[string]*pbpeering.PeeringTrustBundle + UpstreamPeerTrustBundles map[string]*pbpeering.PeeringTrustBundle // WatchedGateways is a map of UpstreamID -> (map of GatewayKey.String() -> // CancelFunc) in order to cancel watches for mesh gateways @@ -128,8 +128,8 @@ func gatewayKeyFromString(s string) GatewayKey { type configSnapshotConnectProxy struct { ConfigSnapshotUpstreams - PeeringTrustBundlesSet bool - PeeringTrustBundles []*pbpeering.PeeringTrustBundle + InboundPeerTrustBundlesSet bool + InboundPeerTrustBundles []*pbpeering.PeeringTrustBundle WatchedServiceChecks map[structs.ServiceID][]structs.CheckType // TODO: missing garbage collection PreparedQueryEndpoints map[UpstreamID]structs.CheckServiceNodes // DEPRECATED:see:WatchedUpstreamEndpoints @@ -152,8 +152,8 @@ func (c *configSnapshotConnectProxy) isEmpty() bool { len(c.WatchedDiscoveryChains) == 0 && len(c.WatchedUpstreams) == 0 && len(c.WatchedUpstreamEndpoints) == 0 && - len(c.WatchedPeerTrustBundles) == 0 && - len(c.PeerTrustBundles) == 0 && + len(c.WatchedUpstreamPeerTrustBundles) == 0 && + len(c.UpstreamPeerTrustBundles) == 0 && len(c.WatchedGateways) == 0 && len(c.WatchedGatewayEndpoints) == 0 && len(c.WatchedServiceChecks) == 0 && @@ -161,7 +161,7 @@ func (c *configSnapshotConnectProxy) isEmpty() bool { len(c.UpstreamConfig) == 0 && len(c.PassthroughUpstreams) == 0 && len(c.IntentionUpstreams) == 0 && - !c.PeeringTrustBundlesSet && + !c.InboundPeerTrustBundlesSet && !c.MeshConfigSet && len(c.PeerUpstreamEndpoints) == 0 && len(c.PeerUpstreamEndpointsUseHostnames) == 0 @@ -637,7 +637,7 @@ func (s *ConfigSnapshot) Clone() (*ConfigSnapshot, error) { snap.ConnectProxy.WatchedUpstreams = nil snap.ConnectProxy.WatchedGateways = nil snap.ConnectProxy.WatchedDiscoveryChains = nil - snap.ConnectProxy.WatchedPeerTrustBundles = nil + snap.ConnectProxy.WatchedUpstreamPeerTrustBundles = nil case structs.ServiceKindTerminatingGateway: snap.TerminatingGateway.WatchedServices = nil snap.TerminatingGateway.WatchedIntentions = nil @@ -652,7 +652,7 @@ func (s *ConfigSnapshot) Clone() (*ConfigSnapshot, error) { snap.IngressGateway.WatchedUpstreams = nil snap.IngressGateway.WatchedGateways = nil snap.IngressGateway.WatchedDiscoveryChains = nil - snap.IngressGateway.WatchedPeerTrustBundles = nil + snap.IngressGateway.WatchedUpstreamPeerTrustBundles = nil // only ingress-gateway snap.IngressGateway.LeafCertWatchCancel = nil } @@ -676,7 +676,7 @@ func (s *ConfigSnapshot) Leaf() *structs.IssuedCert { func (s *ConfigSnapshot) PeeringTrustBundles() []*pbpeering.PeeringTrustBundle { switch s.Kind { case structs.ServiceKindConnectProxy: - return s.ConnectProxy.PeeringTrustBundles + return s.ConnectProxy.InboundPeerTrustBundles case structs.ServiceKindMeshGateway: return s.MeshGateway.PeeringTrustBundles default: @@ -755,7 +755,7 @@ func (u *ConfigSnapshotUpstreams) PeeredUpstreamIDs() []UpstreamID { continue } - if _, ok := u.PeerTrustBundles[uid.Peer]; uid.Peer != "" && !ok { + if _, ok := u.UpstreamPeerTrustBundles[uid.Peer]; uid.Peer != "" && !ok { // The trust bundle for this upstream is not available yet, skip for now. continue } diff --git a/agent/proxycfg/state_test.go b/agent/proxycfg/state_test.go index 667946283..b61bc5eab 100644 --- a/agent/proxycfg/state_test.go +++ b/agent/proxycfg/state_test.go @@ -2572,15 +2572,15 @@ func TestState_WatchesAndUpdates(t *testing.T) { require.Len(t, snap.ConnectProxy.WatchedGateways, 0, "%+v", snap.ConnectProxy.WatchedGateways) require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 0, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) - require.Contains(t, snap.ConnectProxy.WatchedPeerTrustBundles, "peer-a", "%+v", snap.ConnectProxy.WatchedPeerTrustBundles) - require.Len(t, snap.ConnectProxy.PeerTrustBundles, 0, "%+v", snap.ConnectProxy.PeerTrustBundles) + require.Contains(t, snap.ConnectProxy.WatchedUpstreamPeerTrustBundles, "peer-a", "%+v", snap.ConnectProxy.WatchedUpstreamPeerTrustBundles) + require.Len(t, snap.ConnectProxy.UpstreamPeerTrustBundles, 0, "%+v", snap.ConnectProxy.UpstreamPeerTrustBundles) require.Len(t, snap.ConnectProxy.PeerUpstreamEndpoints, 0, "%+v", snap.ConnectProxy.PeerUpstreamEndpoints) require.Len(t, snap.ConnectProxy.WatchedServiceChecks, 0, "%+v", snap.ConnectProxy.WatchedServiceChecks) require.Len(t, snap.ConnectProxy.PreparedQueryEndpoints, 0, "%+v", snap.ConnectProxy.PreparedQueryEndpoints) - require.Len(t, snap.ConnectProxy.PeeringTrustBundles, 0, "%+v", snap.ConnectProxy.PeeringTrustBundles) - require.False(t, snap.ConnectProxy.PeeringTrustBundlesSet) + require.Len(t, snap.ConnectProxy.InboundPeerTrustBundles, 0, "%+v", snap.ConnectProxy.InboundPeerTrustBundles) + require.False(t, snap.ConnectProxy.InboundPeerTrustBundlesSet) }, }, { @@ -2655,7 +2655,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { require.Equal(t, indexedRoots, snap.Roots) require.Equal(t, issuedCert, snap.ConnectProxy.Leaf) - prototest.AssertDeepEqual(t, peerTrustBundles.Bundles, snap.ConnectProxy.PeeringTrustBundles) + prototest.AssertDeepEqual(t, peerTrustBundles.Bundles, snap.ConnectProxy.InboundPeerTrustBundles) require.Len(t, snap.ConnectProxy.DiscoveryChain, 1, "%+v", snap.ConnectProxy.DiscoveryChain) require.Len(t, snap.ConnectProxy.WatchedUpstreams, 1, "%+v", snap.ConnectProxy.WatchedUpstreams) @@ -2663,8 +2663,8 @@ func TestState_WatchesAndUpdates(t *testing.T) { require.Len(t, snap.ConnectProxy.WatchedGateways, 1, "%+v", snap.ConnectProxy.WatchedGateways) require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 1, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) - require.Contains(t, snap.ConnectProxy.WatchedPeerTrustBundles, "peer-a", "%+v", snap.ConnectProxy.WatchedPeerTrustBundles) - require.Equal(t, peerTrustBundles.Bundles[0], snap.ConnectProxy.PeerTrustBundles["peer-a"], "%+v", snap.ConnectProxy.WatchedPeerTrustBundles) + require.Contains(t, snap.ConnectProxy.WatchedUpstreamPeerTrustBundles, "peer-a", "%+v", snap.ConnectProxy.WatchedUpstreamPeerTrustBundles) + require.Equal(t, peerTrustBundles.Bundles[0], snap.ConnectProxy.UpstreamPeerTrustBundles["peer-a"], "%+v", snap.ConnectProxy.WatchedUpstreamPeerTrustBundles) require.Len(t, snap.ConnectProxy.PeerUpstreamEndpoints, 1, "%+v", snap.ConnectProxy.PeerUpstreamEndpoints) require.NotNil(t, snap.ConnectProxy.PeerUpstreamEndpoints[extApiUID]) diff --git a/agent/rpc/peering/service.go b/agent/rpc/peering/service.go index 6d8de85d3..5638702aa 100644 --- a/agent/rpc/peering/service.go +++ b/agent/rpc/peering/service.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/dns" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/proto/pbpeering" ) @@ -140,6 +141,7 @@ type Store interface { // Apply provides a write-only interface for persisting Peering data. type Apply interface { + CheckPeeringUUID(id string) (bool, error) PeeringWrite(req *pbpeering.PeeringWriteRequest) error PeeringTerminateByID(req *pbpeering.PeeringTerminateByIDRequest) error PeeringTrustBundleWrite(req *pbpeering.PeeringTrustBundleWriteRequest) error @@ -189,8 +191,16 @@ func (s *Service) GenerateToken( return nil, err } + canRetry := true +RETRY_ONCE: + id, err := s.getExistingOrCreateNewPeerID(req.PeerName, req.Partition) + if err != nil { + return nil, err + } + writeReq := pbpeering.PeeringWriteRequest{ Peering: &pbpeering.Peering{ + ID: id, Name: req.PeerName, // TODO(peering): Normalize from ACL token once this endpoint is guarded by ACLs. Partition: req.PartitionOrDefault(), @@ -198,6 +208,15 @@ func (s *Service) GenerateToken( }, } if err := s.Backend.Apply().PeeringWrite(&writeReq); err != nil { + // There's a possible race where two servers call Generate Token at the + // same time with the same peer name for the first time. They both + // generate an ID and try to insert and only one wins. This detects the + // collision and forces the loser to discard its generated ID and use + // the one from the other server. + if canRetry && strings.Contains(err.Error(), "A peering already exists with the name") { + canRetry = false + goto RETRY_ONCE + } return nil, fmt.Errorf("failed to write peering: %w", err) } @@ -270,6 +289,11 @@ func (s *Service) Establish( serverAddrs[i] = addr } + id, err := s.getExistingOrCreateNewPeerID(req.PeerName, req.Partition) + if err != nil { + return nil, err + } + // as soon as a peering is written with a list of ServerAddresses that is // non-empty, the leader routine will see the peering and attempt to // establish a connection with the remote peer. @@ -278,6 +302,7 @@ func (s *Service) Establish( // RemotePeerID(PeerID) but at this point the other peer does not. writeReq := &pbpeering.PeeringWriteRequest{ Peering: &pbpeering.Peering{ + ID: id, Name: req.PeerName, PeerCAPems: tok.CA, PeerServerAddresses: serverAddrs, @@ -368,6 +393,16 @@ func (s *Service) PeeringWrite(ctx context.Context, req *pbpeering.PeeringWriteR defer metrics.MeasureSince([]string{"peering", "write"}, time.Now()) // TODO(peering): ACL check request token + if req.Peering == nil { + return nil, fmt.Errorf("missing required peering body") + } + + id, err := s.getExistingOrCreateNewPeerID(req.Peering.Name, req.Peering.Partition) + if err != nil { + return nil, err + } + req.Peering.ID = id + // TODO(peering): handle blocking queries err = s.Backend.Apply().PeeringWrite(req) if err != nil { @@ -418,6 +453,7 @@ func (s *Service) PeeringDelete(ctx context.Context, req *pbpeering.PeeringDelet // We only need to include the name and partition for the peering to be identified. // All other data associated with the peering can be discarded because once marked // for deletion the peering is effectively gone. + ID: existing.ID, Name: req.Name, Partition: req.Partition, DeletedAt: structs.TimeToProto(time.Now().UTC()), @@ -837,6 +873,26 @@ func getTrustDomain(store Store, logger hclog.Logger) (string, error) { return connect.SpiffeIDSigningForCluster(cfg.ClusterID).Host(), nil } +func (s *Service) getExistingOrCreateNewPeerID(peerName, partition string) (string, error) { + q := state.Query{ + Value: strings.ToLower(peerName), + EnterpriseMeta: *structs.NodeEnterpriseMetaInPartition(partition), + } + _, peering, err := s.Backend.Store().PeeringRead(nil, q) + if err != nil { + return "", err + } + if peering != nil { + return peering.ID, nil + } + + id, err := lib.GenerateUUID(s.Backend.Apply().CheckPeeringUUID) + if err != nil { + return "", err + } + return id, nil +} + func (s *Service) StreamStatus(peer string) (resp StreamStatus, found bool) { return s.streams.streamStatus(peer) } diff --git a/agent/rpc/peering/service_test.go b/agent/rpc/peering/service_test.go index aba7973d0..af089f56c 100644 --- a/agent/rpc/peering/service_test.go +++ b/agent/rpc/peering/service_test.go @@ -30,6 +30,7 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/proto/pbservice" "github.com/hashicorp/consul/proto/prototest" @@ -224,6 +225,7 @@ func TestPeeringService_Read(t *testing.T) { // insert peering directly to state store p := &pbpeering.Peering{ + ID: testUUID(t), Name: "foo", State: pbpeering.PeeringState_INITIAL, PeerCAPems: nil, @@ -279,6 +281,7 @@ func TestPeeringService_Delete(t *testing.T) { s := newTestServer(t, nil) p := &pbpeering.Peering{ + ID: testUUID(t), Name: "foo", State: pbpeering.PeeringState_INITIAL, PeerCAPems: nil, @@ -316,6 +319,7 @@ func TestPeeringService_List(t *testing.T) { // Note that the state store holds reference to the underlying // variables; do not modify them after writing. foo := &pbpeering.Peering{ + ID: testUUID(t), Name: "foo", State: pbpeering.PeeringState_INITIAL, PeerCAPems: nil, @@ -324,6 +328,7 @@ func TestPeeringService_List(t *testing.T) { } require.NoError(t, s.Server.FSM().State().PeeringWrite(10, foo)) bar := &pbpeering.Peering{ + ID: testUUID(t), Name: "bar", State: pbpeering.PeeringState_ACTIVE, PeerCAPems: nil, @@ -405,6 +410,7 @@ func TestPeeringService_TrustBundleListByService(t *testing.T) { lastIdx++ require.NoError(t, s.Server.FSM().State().PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testUUID(t), Name: "foo", State: pbpeering.PeeringState_INITIAL, PeerServerName: "test", @@ -413,6 +419,7 @@ func TestPeeringService_TrustBundleListByService(t *testing.T) { lastIdx++ require.NoError(t, s.Server.FSM().State().PeeringWrite(lastIdx, &pbpeering.Peering{ + ID: testUUID(t), Name: "bar", State: pbpeering.PeeringState_INITIAL, PeerServerName: "test-bar", @@ -513,6 +520,7 @@ func Test_StreamHandler_UpsertServices(t *testing.T) { ) require.NoError(t, s.Server.FSM().State().PeeringWrite(0, &pbpeering.Peering{ + ID: testUUID(t), Name: "my-peer", })) @@ -998,7 +1006,9 @@ func newDefaultDeps(t *testing.T, c *consul.Config) consul.Deps { } func setupTestPeering(t *testing.T, store *state.Store, name string, index uint64) string { + t.Helper() err := store.PeeringWrite(index, &pbpeering.Peering{ + ID: testUUID(t), Name: name, }) require.NoError(t, err) @@ -1009,3 +1019,9 @@ func setupTestPeering(t *testing.T, store *state.Store, name string, index uint6 return p.ID } + +func testUUID(t *testing.T) string { + v, err := lib.GenerateUUID(nil) + require.NoError(t, err) + return v +} diff --git a/agent/rpc/peering/stream_test.go b/agent/rpc/peering/stream_test.go index dc30fa686..9bc8eff4e 100644 --- a/agent/rpc/peering/stream_test.go +++ b/agent/rpc/peering/stream_test.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/proto/pbcommon" "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/proto/pbservice" @@ -1030,6 +1031,10 @@ type testApplier struct { store *state.Store } +func (a *testApplier) CheckPeeringUUID(id string) (bool, error) { + panic("not implemented") +} + func (a *testApplier) PeeringWrite(req *pbpeering.PeeringWriteRequest) error { panic("not implemented") } @@ -1216,6 +1221,7 @@ func writeEstablishedPeering(t *testing.T, store *state.Store, idx uint64, peerN require.NoError(t, err) peering := pbpeering.Peering{ + ID: testUUID(t), Name: peerName, PeerID: remotePeerID, } @@ -2169,5 +2175,10 @@ func requireEqualInstances(t *testing.T, expect, got structs.CheckServiceNodes) require.Equal(t, expect[i].Checks[j].PartitionOrDefault(), got[i].Checks[j].PartitionOrDefault(), "partition mismatch") } } - +} + +func testUUID(t *testing.T) string { + v, err := lib.GenerateUUID(nil) + require.NoError(t, err) + return v } diff --git a/agent/rpc/peering/subscription_manager_test.go b/agent/rpc/peering/subscription_manager_test.go index a7c49090b..d556ff23e 100644 --- a/agent/rpc/peering/subscription_manager_test.go +++ b/agent/rpc/peering/subscription_manager_test.go @@ -589,6 +589,7 @@ func (b *testSubscriptionBackend) ensureCARoots(t *testing.T, roots ...*structs. func setupTestPeering(t *testing.T, store *state.Store, name string, index uint64) string { err := store.PeeringWrite(index, &pbpeering.Peering{ + ID: testUUID(t), Name: name, }) require.NoError(t, err) diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 918597b8d..4039b547b 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -2239,8 +2239,9 @@ type IndexedCheckServiceNodes struct { } type IndexedNodesWithGateways struct { - Nodes CheckServiceNodes - Gateways GatewayServices + ImportedNodes CheckServiceNodes + Nodes CheckServiceNodes + Gateways GatewayServices QueryMeta } @@ -2250,7 +2251,8 @@ type DatacenterIndexedCheckServiceNodes struct { } type IndexedNodeDump struct { - Dump NodeDump + ImportedDump NodeDump + Dump NodeDump QueryMeta } diff --git a/agent/ui_endpoint.go b/agent/ui_endpoint.go index 70d5d9def..2f74d8e59 100644 --- a/agent/ui_endpoint.go +++ b/agent/ui_endpoint.go @@ -37,6 +37,8 @@ type ServiceSummary struct { transparentProxySet bool ConnectNative bool + PeerName string `json:",omitempty"` + acl.EnterpriseMeta } @@ -117,7 +119,18 @@ RPC: if out.Dump == nil { out.Dump = make(structs.NodeDump, 0) } - return out.Dump, nil + + // Use empty list instead of nil + for _, info := range out.ImportedDump { + if info.Services == nil { + info.Services = make([]*structs.NodeService, 0) + } + if info.Checks == nil { + info.Checks = make([]*structs.HealthCheck, 0) + } + } + + return append(out.Dump, out.ImportedDump...), nil } // UINodeInfo is used to get info on a single node in a given datacenter. We return a @@ -139,6 +152,10 @@ func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) ( return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing node name"} } + if peer := req.URL.Query().Get("peer"); peer != "" { + args.PeerName = peer + } + // Make the RPC request var out structs.IndexedNodeDump defer setMeta(resp, &out.QueryMeta) @@ -216,15 +233,17 @@ RPC: // Store the names of the gateways associated with each service var ( - serviceGateways = make(map[structs.ServiceName][]structs.ServiceName) - numLinkedServices = make(map[structs.ServiceName]int) + serviceGateways = make(map[structs.PeeredServiceName][]structs.PeeredServiceName) + numLinkedServices = make(map[structs.PeeredServiceName]int) ) for _, gs := range out.Gateways { - serviceGateways[gs.Service] = append(serviceGateways[gs.Service], gs.Gateway) - numLinkedServices[gs.Gateway] += 1 + psn := structs.PeeredServiceName{Peer: structs.DefaultPeerKeyword, ServiceName: gs.Service} + gpsn := structs.PeeredServiceName{Peer: structs.DefaultPeerKeyword, ServiceName: gs.Gateway} + serviceGateways[psn] = append(serviceGateways[psn], gpsn) + numLinkedServices[gpsn] += 1 } - summaries, hasProxy := summarizeServices(out.Nodes.ToServiceDump(), nil, "") + summaries, hasProxy := summarizeServices(append(out.Nodes, out.ImportedNodes...).ToServiceDump(), nil, "") sorted := prepSummaryOutput(summaries, false) // Ensure at least a zero length slice @@ -233,17 +252,18 @@ RPC: sum := ServiceListingSummary{ServiceSummary: *svc} sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta) - if hasProxy[sn] { + psn := structs.PeeredServiceName{Peer: svc.PeerName, ServiceName: sn} + if hasProxy[psn] { sum.ConnectedWithProxy = true } // Verify that at least one of the gateways linked by config entry has an instance registered in the catalog - for _, gw := range serviceGateways[sn] { + for _, gw := range serviceGateways[psn] { if s := summaries[gw]; s != nil && sum.InstanceCount > 0 { sum.ConnectedWithGateway = true } } - sum.GatewayConfig.AssociatedServiceCount = numLinkedServices[sn] + sum.GatewayConfig.AssociatedServiceCount = numLinkedServices[psn] result = append(result, &sum) } @@ -389,31 +409,43 @@ RPC: return topo, nil } -func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.ServiceName]*ServiceSummary, map[structs.ServiceName]bool) { +func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.PeeredServiceName]*ServiceSummary, map[structs.PeeredServiceName]bool) { var ( - summary = make(map[structs.ServiceName]*ServiceSummary) - hasProxy = make(map[structs.ServiceName]bool) + summary = make(map[structs.PeeredServiceName]*ServiceSummary) + hasProxy = make(map[structs.PeeredServiceName]bool) ) - getService := func(service structs.ServiceName) *ServiceSummary { - serv, ok := summary[service] + getService := func(psn structs.PeeredServiceName) *ServiceSummary { + serv, ok := summary[psn] if !ok { serv = &ServiceSummary{ - Name: service.Name, - EnterpriseMeta: service.EnterpriseMeta, + Name: psn.ServiceName.Name, + EnterpriseMeta: psn.ServiceName.EnterpriseMeta, // the other code will increment this unconditionally so we // shouldn't initialize it to 1 InstanceCount: 0, + PeerName: psn.Peer, } - summary[service] = serv + summary[psn] = serv } return serv } for _, csn := range dump { + var peerName string + // all entities will have the same peer name so it is safe to use the node's peer name + if csn.Node == nil { + // this can happen for gateway dumps that call this summarize func + peerName = structs.DefaultPeerKeyword + } else { + peerName = csn.Node.PeerName + } + if cfg != nil && csn.GatewayService != nil { gwsvc := csn.GatewayService - sum := getService(gwsvc.Service) + + psn := structs.PeeredServiceName{Peer: peerName, ServiceName: gwsvc.Service} + sum := getService(psn) modifySummaryForGatewayService(cfg, dc, sum, gwsvc) } @@ -421,8 +453,10 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s if csn.Service == nil { continue } + sn := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta) - sum := getService(sn) + psn := structs.PeeredServiceName{Peer: peerName, ServiceName: sn} + sum := getService(psn) svc := csn.Service sum.Nodes = append(sum.Nodes, csn.Node.Node) @@ -432,9 +466,10 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s sum.ConnectNative = svc.Connect.Native if svc.Kind == structs.ServiceKindConnectProxy { sn := structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta) - hasProxy[sn] = true + psn := structs.PeeredServiceName{Peer: peerName, ServiceName: sn} + hasProxy[psn] = true - destination := getService(sn) + destination := getService(psn) for _, check := range csn.Checks { cid := structs.NewCheckID(check.CheckID, &check.EnterpriseMeta) uid := structs.UniqueID(csn.Node.Node, cid.String()) @@ -496,7 +531,7 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s return summary, hasProxy } -func prepSummaryOutput(summaries map[structs.ServiceName]*ServiceSummary, excludeSidecars bool) []*ServiceSummary { +func prepSummaryOutput(summaries map[structs.PeeredServiceName]*ServiceSummary, excludeSidecars bool) []*ServiceSummary { var resp []*ServiceSummary // Ensure at least a zero length slice resp = make([]*ServiceSummary, 0) diff --git a/agent/ui_endpoint_test.go b/agent/ui_endpoint_test.go index f2a17f73e..0ddb180ac 100644 --- a/agent/ui_endpoint_test.go +++ b/agent/ui_endpoint_test.go @@ -2,6 +2,7 @@ package agent import ( "bytes" + "context" "fmt" "io" "io/ioutil" @@ -11,6 +12,7 @@ import ( "path/filepath" "sync/atomic" "testing" + "time" cleanhttp "github.com/hashicorp/go-cleanhttp" "github.com/stretchr/testify/assert" @@ -19,12 +21,14 @@ import ( "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" + "github.com/hashicorp/consul/types" ) -func TestUiIndex(t *testing.T) { +func TestUIIndex(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } @@ -74,7 +78,7 @@ func TestUiIndex(t *testing.T) { } } -func TestUiNodes(t *testing.T) { +func TestUINodes(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } @@ -84,15 +88,42 @@ func TestUiNodes(t *testing.T) { defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "test", - Address: "127.0.0.1", + args := []*structs.RegisterRequest{ + { + Datacenter: "dc1", + Node: "test", + Address: "127.0.0.1", + }, + { + Datacenter: "dc1", + Node: "foo-peer", + Address: "127.0.0.3", + PeerName: "peer1", + }, } - var out struct{} - if err := a.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + for _, reg := range args { + var out struct{} + err := a.RPC("Catalog.Register", reg, &out) + require.NoError(t, err) + } + + // establish "peer1" + { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + peerOne := &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + Name: "peer1", + State: pbpeering.PeeringState_INITIAL, + PeerCAPems: nil, + PeerServerName: "fooservername", + PeerServerAddresses: []string{"addr1"}, + }, + } + _, err := a.rpcClientPeering.PeeringWrite(ctx, peerOne) + require.NoError(t, err) } req, _ := http.NewRequest("GET", "/v1/internal/ui/nodes/dc1", nil) @@ -103,20 +134,32 @@ func TestUiNodes(t *testing.T) { } assertIndex(t, resp) - // Should be 2 nodes, and all the empty lists should be non-nil + // Should be 3 nodes, and all the empty lists should be non-nil nodes := obj.(structs.NodeDump) - if len(nodes) != 2 || - nodes[0].Node != a.Config.NodeName || - nodes[0].Services == nil || len(nodes[0].Services) != 1 || - nodes[0].Checks == nil || len(nodes[0].Checks) != 1 || - nodes[1].Node != "test" || - nodes[1].Services == nil || len(nodes[1].Services) != 0 || - nodes[1].Checks == nil || len(nodes[1].Checks) != 0 { - t.Fatalf("bad: %v", obj) - } + require.Len(t, nodes, 3) + + // check local nodes, services and checks + require.Equal(t, a.Config.NodeName, nodes[0].Node) + require.NotNil(t, nodes[0].Services) + require.Len(t, nodes[0].Services, 1) + require.NotNil(t, nodes[0].Checks) + require.Len(t, nodes[0].Checks, 1) + require.Equal(t, "test", nodes[1].Node) + require.NotNil(t, nodes[1].Services) + require.Len(t, nodes[1].Services, 0) + require.NotNil(t, nodes[1].Checks) + require.Len(t, nodes[1].Checks, 0) + + // peered node + require.Equal(t, "foo-peer", nodes[2].Node) + require.Equal(t, "peer1", nodes[2].PeerName) + require.NotNil(t, nodes[2].Services) + require.Len(t, nodes[2].Services, 0) + require.NotNil(t, nodes[1].Checks) + require.Len(t, nodes[2].Services, 0) } -func TestUiNodes_Filter(t *testing.T) { +func TestUINodes_Filter(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } @@ -162,7 +205,7 @@ func TestUiNodes_Filter(t *testing.T) { require.Empty(t, nodes[0].Checks) } -func TestUiNodeInfo(t *testing.T) { +func TestUINodeInfo(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } @@ -214,7 +257,7 @@ func TestUiNodeInfo(t *testing.T) { } } -func TestUiServices(t *testing.T) { +func TestUIServices(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } @@ -318,6 +361,30 @@ func TestUiServices(t *testing.T) { Tags: []string{}, }, }, + // register peer node foo with peer service + { + Datacenter: "dc1", + Node: "foo", + ID: types.NodeID("e0155642-135d-4739-9853-a1ee6c9f945b"), + Address: "127.0.0.2", + TaggedAddresses: map[string]string{ + "lan": "127.0.0.2", + "wan": "198.18.0.2", + }, + NodeMeta: map[string]string{ + "env": "production", + "os": "linux", + }, + PeerName: "peer1", + Service: &structs.NodeService{ + Kind: structs.ServiceKindTypical, + ID: "serviceID", + Service: "service", + Port: 1235, + Address: "198.18.1.2", + PeerName: "peer1", + }, + }, } for _, args := range requests { @@ -325,6 +392,24 @@ func TestUiServices(t *testing.T) { require.NoError(t, a.RPC("Catalog.Register", args, &out)) } + // establish "peer1" + { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + peerOne := &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + Name: "peer1", + State: pbpeering.PeeringState_INITIAL, + PeerCAPems: nil, + PeerServerName: "fooservername", + PeerServerAddresses: []string{"addr1"}, + }, + } + _, err := a.rpcClientPeering.PeeringWrite(ctx, peerOne) + require.NoError(t, err) + } + // Register a terminating gateway associated with api and cache { arg := structs.RegisterRequest{ @@ -393,7 +478,7 @@ func TestUiServices(t *testing.T) { // Should be 2 nodes, and all the empty lists should be non-nil summary := obj.([]*ServiceListingSummary) - require.Len(t, summary, 6) + require.Len(t, summary, 7) // internal accounting that users don't see can be blown away for _, sum := range summary { @@ -493,6 +578,21 @@ func TestUiServices(t *testing.T) { EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), }, }, + { + ServiceSummary: ServiceSummary{ + Kind: structs.ServiceKindTypical, + Name: "service", + Datacenter: "dc1", + Tags: nil, + Nodes: []string{"foo"}, + InstanceCount: 1, + ChecksPassing: 0, + ChecksWarning: 0, + ChecksCritical: 0, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + PeerName: "peer1", + }, + }, } require.ElementsMatch(t, expected, summary) }) diff --git a/agent/xds/clusters.go b/agent/xds/clusters.go index 2acfa7c10..feb488a63 100644 --- a/agent/xds/clusters.go +++ b/agent/xds/clusters.go @@ -465,9 +465,9 @@ func (s *ResourceGenerator) makeDestinationClusters(cfgSnap *proxycfg.ConfigSnap cluster := s.makeDynamicForwardProxyCluster(cfgSnap, opts) // TODO (dans): might be relevant later for TLS addons like CA validation - //if err := s.injectGatewayServiceAddons(cfgSnap, cluster, svc, loadBalancer); err != nil { + // if err := s.injectGatewayServiceAddons(cfgSnap, cluster, svc, loadBalancer); err != nil { // return nil, err - //} + // } clusters = append(clusters, cluster) } return clusters, nil @@ -695,7 +695,7 @@ func (s *ResourceGenerator) makeUpstreamClusterForPeerService( rootPEMs := cfgSnap.RootPEMs() if uid.Peer != "" { - rootPEMs = cfgSnap.ConnectProxy.PeerTrustBundles[uid.Peer].ConcatenatedRootPEMs() + rootPEMs = cfgSnap.ConnectProxy.UpstreamPeerTrustBundles[uid.Peer].ConcatenatedRootPEMs() } // Enable TLS upstream with the configured client certificate. @@ -999,7 +999,7 @@ func (s *ResourceGenerator) makeUpstreamClustersForDiscoveryChain( rootPEMs := cfgSnap.RootPEMs() if uid.Peer != "" { - rootPEMs = cfgSnap.ConnectProxy.PeerTrustBundles[uid.Peer].ConcatenatedRootPEMs() + rootPEMs = cfgSnap.ConnectProxy.UpstreamPeerTrustBundles[uid.Peer].ConcatenatedRootPEMs() } commonTLSContext := makeCommonTLSContext( cfgSnap.Leaf(), diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index aab41f44b..a364ecce1 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -697,7 +697,8 @@ func (s *ResourceGenerator) injectConnectFilters(cfgSnap *proxycfg.ConfigSnapsho authzFilter, err := makeRBACNetworkFilter( cfgSnap.ConnectProxy.Intentions, cfgSnap.IntentionDefaultAllow, - cfgSnap.ConnectProxy.PeerTrustBundles, + cfgSnap.Roots.TrustDomain, + cfgSnap.ConnectProxy.InboundPeerTrustBundles, ) if err != nil { return err @@ -952,7 +953,8 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot httpAuthzFilter, err := makeRBACHTTPFilter( cfgSnap.ConnectProxy.Intentions, cfgSnap.IntentionDefaultAllow, - cfgSnap.ConnectProxy.PeerTrustBundles, + cfgSnap.Roots.TrustDomain, + cfgSnap.ConnectProxy.InboundPeerTrustBundles, ) if err != nil { return nil, err @@ -1009,7 +1011,8 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot filterOpts.httpAuthzFilter, err = makeRBACHTTPFilter( cfgSnap.ConnectProxy.Intentions, cfgSnap.IntentionDefaultAllow, - cfgSnap.ConnectProxy.PeerTrustBundles, + cfgSnap.Roots.TrustDomain, + cfgSnap.ConnectProxy.InboundPeerTrustBundles, ) if err != nil { return nil, err @@ -1307,6 +1310,7 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg. authFilter, err := makeRBACNetworkFilter( intentions, cfgSnap.IntentionDefaultAllow, + cfgSnap.Roots.TrustDomain, nil, // TODO(peering): verify intentions w peers don't apply to terminatingGateway ) if err != nil { @@ -1344,6 +1348,7 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg. opts.httpAuthzFilter, err = makeRBACHTTPFilter( intentions, cfgSnap.IntentionDefaultAllow, + cfgSnap.Roots.TrustDomain, nil, // TODO(peering): verify intentions w peers don't apply to terminatingGateway ) if err != nil { diff --git a/agent/xds/rbac.go b/agent/xds/rbac.go index 319c648e3..c5a651150 100644 --- a/agent/xds/rbac.go +++ b/agent/xds/rbac.go @@ -21,9 +21,10 @@ import ( func makeRBACNetworkFilter( intentions structs.Intentions, intentionDefaultAllow bool, - peerTrustBundles map[string]*pbpeering.PeeringTrustBundle, + trustDomain string, + peerTrustBundles []*pbpeering.PeeringTrustBundle, ) (*envoy_listener_v3.Filter, error) { - rules, err := makeRBACRules(intentions, intentionDefaultAllow, false, peerTrustBundles) + rules, err := makeRBACRules(intentions, intentionDefaultAllow, trustDomain, false, peerTrustBundles) if err != nil { return nil, err } @@ -38,9 +39,10 @@ func makeRBACNetworkFilter( func makeRBACHTTPFilter( intentions structs.Intentions, intentionDefaultAllow bool, - peerTrustBundles map[string]*pbpeering.PeeringTrustBundle, + trustDomain string, + peerTrustBundles []*pbpeering.PeeringTrustBundle, ) (*envoy_http_v3.HttpFilter, error) { - rules, err := makeRBACRules(intentions, intentionDefaultAllow, true, peerTrustBundles) + rules, err := makeRBACRules(intentions, intentionDefaultAllow, trustDomain, true, peerTrustBundles) if err != nil { return nil, err } @@ -53,6 +55,7 @@ func makeRBACHTTPFilter( func intentionListToIntermediateRBACForm( intentions structs.Intentions, + trustDomain string, isHTTP bool, trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle, ) []*rbacIntention { @@ -72,7 +75,7 @@ func intentionListToIntermediateRBACForm( continue } - rixn := intentionToIntermediateRBACForm(ixn, isHTTP, trustBundle) + rixn := intentionToIntermediateRBACForm(ixn, trustDomain, isHTTP, trustBundle) rbacIxns = append(rbacIxns, rixn) } return rbacIxns @@ -210,11 +213,12 @@ func removePermissionPrecedence(perms []*rbacPermission, intentionDefaultAction return out } -func intentionToIntermediateRBACForm(ixn *structs.Intention, isHTTP bool, bundle *pbpeering.PeeringTrustBundle) *rbacIntention { +func intentionToIntermediateRBACForm(ixn *structs.Intention, trustDomain string, isHTTP bool, bundle *pbpeering.PeeringTrustBundle) *rbacIntention { rixn := &rbacIntention{ Source: rbacService{ ServiceName: ixn.SourceServiceName(), Peer: ixn.SourcePeer, + TrustDomain: trustDomain, }, Precedence: ixn.Precedence, } @@ -426,25 +430,21 @@ func simplifyNotSourceSlice(notSources []rbacService) []rbacService { func makeRBACRules( intentions structs.Intentions, intentionDefaultAllow bool, + trustDomain string, isHTTP bool, - peerTrustBundles map[string]*pbpeering.PeeringTrustBundle, + peerTrustBundles []*pbpeering.PeeringTrustBundle, ) (*envoy_rbac_v3.RBAC, error) { - // Note that we DON'T explicitly validate the trust-domain matches ours. - // - // For now we don't validate the trust domain of the _destination_ at all. - // The RBAC policies below ignore the trust domain and it's implicit that - // the request is for the correct cluster. We might want to reconsider this - // later but plumbing in additional machinery to check the clusterID here - // is not really necessary for now unless the Envoys are badly configured. - // Our threat model _requires_ correctly configured and well behaved - // proxies given that they have ACLs to fetch certs and so can do whatever - // they want including not authorizing traffic at all or routing it do a - // different service than they auth'd against. - // TODO(banks,rb): Implement revocation list checking? + // TODO(peering): mkeeler asked that these maps come from proxycfg instead of + // being constructed in xds to save memory allocation and gc pressure. Low priority. + trustBundlesByPeer := make(map[string]*pbpeering.PeeringTrustBundle, len(peerTrustBundles)) + for _, ptb := range peerTrustBundles { + trustBundlesByPeer[ptb.PeerName] = ptb + } + // First build up just the basic principal matches. - rbacIxns := intentionListToIntermediateRBACForm(intentions, isHTTP, peerTrustBundles) + rbacIxns := intentionListToIntermediateRBACForm(intentions, trustDomain, isHTTP, trustBundlesByPeer) // Normalize: if we are in default-deny then all intentions must be allows and vice versa intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow) @@ -641,7 +641,7 @@ const anyPath = `[^/]+` func makeSpiffePattern(src rbacService) string { var ( - host = anyPath // TODO(peering): We match trust domain on any value but should be defaulting to the local trust domain + host = src.TrustDomain ap = src.PartitionOrDefault() ns = src.NamespaceOrDefault() svc = src.Name diff --git a/agent/xds/rbac_test.go b/agent/xds/rbac_test.go index 358a91347..81fb32ad8 100644 --- a/agent/xds/rbac_test.go +++ b/agent/xds/rbac_test.go @@ -58,10 +58,13 @@ func TestRemoveIntentionPrecedence(t *testing.T) { ExportedPartition: "part1", }, } + testTrustDomain := "test.consul" var ( - nameWild = rbacService{ServiceName: structs.NewServiceName("*", nil)} - nameWeb = rbacService{ServiceName: structs.NewServiceName("web", nil)} + nameWild = rbacService{ServiceName: structs.NewServiceName("*", nil), + TrustDomain: testTrustDomain} + nameWeb = rbacService{ServiceName: structs.NewServiceName("web", nil), + TrustDomain: testTrustDomain} nameWildPeered = rbacService{ServiceName: structs.NewServiceName("*", nil), Peer: "peer1", TrustDomain: "peer1.domain", ExportedPartition: "part1"} nameWebPeered = rbacService{ServiceName: structs.NewServiceName("web", nil), @@ -439,7 +442,7 @@ func TestRemoveIntentionPrecedence(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - rbacIxns := intentionListToIntermediateRBACForm(tt.intentions, tt.http, testPeerTrustBundle) + rbacIxns := intentionListToIntermediateRBACForm(tt.intentions, testTrustDomain, tt.http, testPeerTrustBundle) intentionDefaultAction := intentionActionFromBool(tt.intentionDefaultAllow) rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction) @@ -472,13 +475,14 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { ixn.Permissions = perms return ixn } - testPeerTrustBundle := map[string]*pbpeering.PeeringTrustBundle{ - "peer1": { + testPeerTrustBundle := []*pbpeering.PeeringTrustBundle{ + { PeerName: "peer1", TrustDomain: "peer1.domain", ExportedPartition: "part1", }, } + testTrustDomain := "test.consul" sorted := func(ixns ...*structs.Intention) structs.Intentions { sort.SliceStable(ixns, func(i, j int) bool { return ixns[j].Precedence < ixns[i].Precedence @@ -797,7 +801,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { tt := tt t.Run(name, func(t *testing.T) { t.Run("network filter", func(t *testing.T) { - filter, err := makeRBACNetworkFilter(tt.intentions, tt.intentionDefaultAllow, testPeerTrustBundle) + filter, err := makeRBACNetworkFilter(tt.intentions, tt.intentionDefaultAllow, testTrustDomain, testPeerTrustBundle) require.NoError(t, err) t.Run("current", func(t *testing.T) { @@ -807,7 +811,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { }) }) t.Run("http filter", func(t *testing.T) { - filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow, testPeerTrustBundle) + filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow, testTrustDomain, testPeerTrustBundle) require.NoError(t, err) t.Run("current", func(t *testing.T) { diff --git a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow--httpfilter.golden index b44899f2a..b590b7135 100644 --- a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow--httpfilter.golden @@ -22,7 +22,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -35,7 +35,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow.golden b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow.golden index 092676776..51346b03f 100644 --- a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow.golden +++ b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-allow.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -34,7 +34,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -47,7 +47,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny--httpfilter.golden index 806b19c59..62f4ba97e 100644 --- a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny--httpfilter.golden @@ -22,7 +22,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -35,7 +35,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -64,7 +64,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny.golden b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny.golden index 092676776..51346b03f 100644 --- a/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny.golden +++ b/agent/xds/testdata/rbac/default-allow-deny-all-and-path-deny.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -34,7 +34,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -47,7 +47,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-kitchen-sink--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-kitchen-sink--httpfilter.golden index 714203229..65da4d27f 100644 --- a/agent/xds/testdata/rbac/default-allow-kitchen-sink--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-kitchen-sink--httpfilter.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } @@ -31,7 +31,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -46,7 +46,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -59,7 +59,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -73,7 +73,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/unsafe$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/unsafe$" } } } @@ -87,7 +87,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-kitchen-sink.golden b/agent/xds/testdata/rbac/default-allow-kitchen-sink.golden index a85359a55..21e94ee14 100644 --- a/agent/xds/testdata/rbac/default-allow-kitchen-sink.golden +++ b/agent/xds/testdata/rbac/default-allow-kitchen-sink.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } @@ -31,7 +31,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -46,7 +46,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -59,7 +59,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -73,7 +73,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/unsafe$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/unsafe$" } } } @@ -87,7 +87,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-one-deny--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-one-deny--httpfilter.golden index 72be1fe60..f315c45e0 100644 --- a/agent/xds/testdata/rbac/default-allow-one-deny--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-one-deny--httpfilter.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-one-deny.golden b/agent/xds/testdata/rbac/default-allow-one-deny.golden index 05dca90f8..45b6965e0 100644 --- a/agent/xds/testdata/rbac/default-allow-one-deny.golden +++ b/agent/xds/testdata/rbac/default-allow-one-deny.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-path-allow.golden b/agent/xds/testdata/rbac/default-allow-path-allow.golden index 05dca90f8..45b6965e0 100644 --- a/agent/xds/testdata/rbac/default-allow-path-allow.golden +++ b/agent/xds/testdata/rbac/default-allow-path-allow.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-path-deny--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-path-deny--httpfilter.golden index 4f64c2734..aa06ebe3d 100644 --- a/agent/xds/testdata/rbac/default-allow-path-deny--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-path-deny--httpfilter.golden @@ -23,7 +23,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-path-deny.golden b/agent/xds/testdata/rbac/default-allow-path-deny.golden index 05dca90f8..45b6965e0 100644 --- a/agent/xds/testdata/rbac/default-allow-path-deny.golden +++ b/agent/xds/testdata/rbac/default-allow-path-deny.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-service-wildcard-deny--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-service-wildcard-deny--httpfilter.golden index be70c6725..0c69fa845 100644 --- a/agent/xds/testdata/rbac/default-allow-service-wildcard-deny--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-service-wildcard-deny--httpfilter.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-service-wildcard-deny.golden b/agent/xds/testdata/rbac/default-allow-service-wildcard-deny.golden index 1951818cd..d685342e9 100644 --- a/agent/xds/testdata/rbac/default-allow-service-wildcard-deny.golden +++ b/agent/xds/testdata/rbac/default-allow-service-wildcard-deny.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms--httpfilter.golden index a3945edbb..ba1787d12 100644 --- a/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms--httpfilter.golden @@ -227,7 +227,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms.golden b/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms.golden index 05dca90f8..45b6965e0 100644 --- a/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms.golden +++ b/agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow--httpfilter.golden b/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow--httpfilter.golden index 8eb2021cc..98f414209 100644 --- a/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow--httpfilter.golden @@ -45,7 +45,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow.golden b/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow.golden index 05dca90f8..45b6965e0 100644 --- a/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow.golden +++ b/agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow.golden @@ -19,7 +19,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-allow-deny--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-allow-deny--httpfilter.golden index ee099df84..3e48b7c56 100644 --- a/agent/xds/testdata/rbac/default-deny-allow-deny--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-allow-deny--httpfilter.golden @@ -21,7 +21,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -34,7 +34,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-allow-deny.golden b/agent/xds/testdata/rbac/default-deny-allow-deny.golden index b92c68935..d901cf42c 100644 --- a/agent/xds/testdata/rbac/default-deny-allow-deny.golden +++ b/agent/xds/testdata/rbac/default-deny-allow-deny.golden @@ -21,7 +21,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -34,7 +34,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-deny-all-and-path-allow--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-deny-all-and-path-allow--httpfilter.golden index 323526f48..003fdb24b 100644 --- a/agent/xds/testdata/rbac/default-deny-deny-all-and-path-allow--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-deny-all-and-path-allow--httpfilter.golden @@ -22,7 +22,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-kitchen-sink--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-kitchen-sink--httpfilter.golden index f61f892d4..426404d74 100644 --- a/agent/xds/testdata/rbac/default-deny-kitchen-sink--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-kitchen-sink--httpfilter.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } @@ -30,7 +30,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -45,7 +45,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -58,7 +58,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -72,7 +72,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/unsafe$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/unsafe$" } } } @@ -86,7 +86,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-kitchen-sink.golden b/agent/xds/testdata/rbac/default-deny-kitchen-sink.golden index 30d56485c..912b64d95 100644 --- a/agent/xds/testdata/rbac/default-deny-kitchen-sink.golden +++ b/agent/xds/testdata/rbac/default-deny-kitchen-sink.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } @@ -30,7 +30,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -45,7 +45,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } @@ -58,7 +58,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } @@ -72,7 +72,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/unsafe$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/unsafe$" } } } @@ -86,7 +86,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/cron$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-mixed-precedence--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-mixed-precedence--httpfilter.golden index a37549e5e..c4795f46e 100644 --- a/agent/xds/testdata/rbac/default-deny-mixed-precedence--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-mixed-precedence--httpfilter.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-mixed-precedence.golden b/agent/xds/testdata/rbac/default-deny-mixed-precedence.golden index 4bc3c7378..3fc78b295 100644 --- a/agent/xds/testdata/rbac/default-deny-mixed-precedence.golden +++ b/agent/xds/testdata/rbac/default-deny-mixed-precedence.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-one-allow--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-one-allow--httpfilter.golden index a37549e5e..c4795f46e 100644 --- a/agent/xds/testdata/rbac/default-deny-one-allow--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-one-allow--httpfilter.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-one-allow.golden b/agent/xds/testdata/rbac/default-deny-one-allow.golden index 4bc3c7378..3fc78b295 100644 --- a/agent/xds/testdata/rbac/default-deny-one-allow.golden +++ b/agent/xds/testdata/rbac/default-deny-one-allow.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-path-allow--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-path-allow--httpfilter.golden index 323526f48..003fdb24b 100644 --- a/agent/xds/testdata/rbac/default-deny-path-allow--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-path-allow--httpfilter.golden @@ -22,7 +22,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink--httpfilter.golden index 304e428ef..80d977dff 100644 --- a/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink--httpfilter.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink.golden b/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink.golden index 2d9985759..eb4801594 100644 --- a/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink.golden +++ b/agent/xds/testdata/rbac/default-deny-peered-kitchen-sink.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-service-wildcard-allow--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-service-wildcard-allow--httpfilter.golden index 365f970c5..a65162f24 100644 --- a/agent/xds/testdata/rbac/default-deny-service-wildcard-allow--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-service-wildcard-allow--httpfilter.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-service-wildcard-allow.golden b/agent/xds/testdata/rbac/default-deny-service-wildcard-allow.golden index fd8445265..3780a9079 100644 --- a/agent/xds/testdata/rbac/default-deny-service-wildcard-allow.golden +++ b/agent/xds/testdata/rbac/default-deny-service-wildcard-allow.golden @@ -18,7 +18,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/[^/]+$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms--httpfilter.golden index 694f5858f..f23112071 100644 --- a/agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms--httpfilter.golden @@ -226,7 +226,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow--httpfilter.golden b/agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow--httpfilter.golden index aeadff72b..75534cb10 100644 --- a/agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow--httpfilter.golden +++ b/agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow--httpfilter.golden @@ -46,7 +46,7 @@ "googleRe2": { }, - "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$" + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" } } } diff --git a/command/connect/envoy/bootstrap_config.go b/command/connect/envoy/bootstrap_config.go index 9c94eaac7..f2ea33b7f 100644 --- a/command/connect/envoy/bootstrap_config.go +++ b/command/connect/envoy/bootstrap_config.go @@ -637,6 +637,31 @@ func (c *BootstrapConfig) generateListenerConfig(args *BootstrapTplArgs, bindAdd ] } }` + + // Enable TLS on the prometheus listener if cert/private key are provided. + var tlsConfig string + if args.PrometheusCertFile != "" { + tlsConfig = `, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsCertificateSdsSecretConfigs": [ + { + "name": "prometheus_cert" + } + ], + "validationContextSdsSecretConfig": { + "trustedCa": { + "name": "prometheus_validation_context" + } + } + } + } + }` + } + listenerJSON := `{ "name": "` + name + `_listener", "address": { @@ -694,11 +719,43 @@ func (c *BootstrapConfig) generateListenerConfig(args *BootstrapTplArgs, bindAdd ] } } - ] + ]` + tlsConfig + ` } ] }` + secretsTemplate := `{ + "name": "prometheus_cert", + "tlsCertificate": { + "certificateChain": { + "filename": "%s" + }, + "privateKey": { + "filename": "%s" + } + } + }, + { + "name": "prometheus_validation_context", + "validationContext": { + %s + } + }` + var validationContext string + if args.PrometheusCAPath != "" { + validationContext = fmt.Sprintf(`"watchedDirectory": { + "path": "%s" + }`, args.PrometheusCAPath) + } else { + validationContext = fmt.Sprintf(`"trustedCa": { + "filename": "%s" + }`, args.PrometheusCAFile) + } + var secretsJSON string + if args.PrometheusCertFile != "" { + secretsJSON = fmt.Sprintf(secretsTemplate, args.PrometheusCertFile, args.PrometheusKeyFile, validationContext) + } + // Make sure we do not append the same cluster multiple times, as that will // cause envoy startup to fail. selfAdminClusterExists, err := containsSelfAdminCluster(args.StaticClustersJSON) @@ -716,6 +773,12 @@ func (c *BootstrapConfig) generateListenerConfig(args *BootstrapTplArgs, bindAdd listenerJSON = ",\n" + listenerJSON } args.StaticListenersJSON += listenerJSON + + if args.StaticSecretsJSON != "" { + secretsJSON = ",\n" + secretsJSON + } + args.StaticSecretsJSON += secretsJSON + return nil } diff --git a/command/connect/envoy/bootstrap_config_test.go b/command/connect/envoy/bootstrap_config_test.go index f02ae04c5..bb471566c 100644 --- a/command/connect/envoy/bootstrap_config_test.go +++ b/command/connect/envoy/bootstrap_config_test.go @@ -273,6 +273,126 @@ const ( } ] }` + expectedPromListenerWithBackendAndTLS = `{ + "name": "envoy_prometheus_metrics_listener", + "address": { + "socket_address": { + "address": "0.0.0.0", + "port_value": 9000 + } + }, + "filter_chains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "envoy_prometheus_metrics", + "codec_type": "HTTP1", + "route_config": { + "name": "self_admin_route", + "virtual_hosts": [ + { + "name": "self_admin", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "path": "/metrics" + }, + "route": { + "cluster": "prometheus_backend", + "prefix_rewrite": "/stats/prometheus" + } + }, + { + "match": { + "prefix": "/" + }, + "direct_response": { + "status": 404 + } + } + ] + } + ] + }, + "http_filters": [ + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ] + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsCertificateSdsSecretConfigs": [ + { + "name": "prometheus_cert" + } + ], + "validationContextSdsSecretConfig": { + "trustedCa": { + "name": "prometheus_validation_context" + } + } + } + } + } + } + ] + }` + + expectedPromSecretsWithBackendAndTLS = `{ + "name": "prometheus_cert", + "tlsCertificate": { + "certificateChain": { + "filename": "test-cert-file" + }, + "privateKey": { + "filename": "test-key-file" + } + } + }, + { + "name": "prometheus_validation_context", + "validationContext": { + "trustedCa": { + "filename": "test-ca-file" + } + } + }` + + expectedPromSecretsWithBackendAndTLSCAPath = `{ + "name": "prometheus_cert", + "tlsCertificate": { + "certificateChain": { + "filename": "test-cert-file" + }, + "privateKey": { + "filename": "test-key-file" + } + } + }, + { + "name": "prometheus_validation_context", + "validationContext": { + "watchedDirectory": { + "path": "test-ca-directory" + } + } + }` + expectedStatsListener = `{ "name": "envoy_metrics_listener", "address": { @@ -760,6 +880,68 @@ func TestBootstrapConfig_ConfigureArgs(t *testing.T) { }, wantErr: false, }, + { + name: "prometheus-bind-addr-with-backend-and-tls", + input: BootstrapConfig{ + PrometheusBindAddr: "0.0.0.0:9000", + }, + baseArgs: BootstrapTplArgs{ + AdminBindAddress: "127.0.0.1", + AdminBindPort: "19000", + PrometheusBackendPort: "20100", + PrometheusScrapePath: "/metrics", + PrometheusCAFile: "test-ca-file", + PrometheusCertFile: "test-cert-file", + PrometheusKeyFile: "test-key-file", + }, + wantArgs: BootstrapTplArgs{ + AdminBindAddress: "127.0.0.1", + AdminBindPort: "19000", + // Should use the "prometheus_backend" cluster instead, which + // uses the PrometheusBackendPort rather than Envoy admin port + StaticClustersJSON: expectedPrometheusBackendCluster, + StaticListenersJSON: expectedPromListenerWithBackendAndTLS, + StaticSecretsJSON: expectedPromSecretsWithBackendAndTLS, + StatsConfigJSON: defaultStatsConfigJSON, + PrometheusBackendPort: "20100", + PrometheusScrapePath: "/metrics", + PrometheusCAFile: "test-ca-file", + PrometheusCertFile: "test-cert-file", + PrometheusKeyFile: "test-key-file", + }, + wantErr: false, + }, + { + name: "prometheus-bind-addr-with-backend-and-tls-ca-path", + input: BootstrapConfig{ + PrometheusBindAddr: "0.0.0.0:9000", + }, + baseArgs: BootstrapTplArgs{ + AdminBindAddress: "127.0.0.1", + AdminBindPort: "19000", + PrometheusBackendPort: "20100", + PrometheusScrapePath: "/metrics", + PrometheusCAPath: "test-ca-directory", + PrometheusCertFile: "test-cert-file", + PrometheusKeyFile: "test-key-file", + }, + wantArgs: BootstrapTplArgs{ + AdminBindAddress: "127.0.0.1", + AdminBindPort: "19000", + // Should use the "prometheus_backend" cluster instead, which + // uses the PrometheusBackendPort rather than Envoy admin port + StaticClustersJSON: expectedPrometheusBackendCluster, + StaticListenersJSON: expectedPromListenerWithBackendAndTLS, + StaticSecretsJSON: expectedPromSecretsWithBackendAndTLSCAPath, + StatsConfigJSON: defaultStatsConfigJSON, + PrometheusBackendPort: "20100", + PrometheusScrapePath: "/metrics", + PrometheusCAPath: "test-ca-directory", + PrometheusCertFile: "test-cert-file", + PrometheusKeyFile: "test-key-file", + }, + wantErr: false, + }, { name: "stats-bind-addr", input: BootstrapConfig{ diff --git a/command/connect/envoy/bootstrap_tpl.go b/command/connect/envoy/bootstrap_tpl.go index 6cb238d49..e6b71f548 100644 --- a/command/connect/envoy/bootstrap_tpl.go +++ b/command/connect/envoy/bootstrap_tpl.go @@ -76,6 +76,10 @@ type BootstrapTplArgs struct { // https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/metrics/v2/stats.proto#envoy-api-msg-config-metrics-v2-statsconfig. StatsConfigJSON string + // StaticSecretsJSON is a JSON string containing zero or more Secret definitions. + // See https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/secret.proto#envoy-v3-api-msg-extensions-transport-sockets-tls-v3-secret + StaticSecretsJSON string + // StatsFlushInterval is the time duration between Envoy stats flushes. It is // in proto3 "duration" string format for example "1.12s" See // https://developers.google.com/protocol-buffers/docs/proto3#json and @@ -106,6 +110,11 @@ type BootstrapTplArgs struct { // PrometheusScrapePath will configure the path where metrics are exposed on // the envoy_prometheus_bind_addr listener. PrometheusScrapePath string + + PrometheusCAFile string + PrometheusCAPath string + PrometheusCertFile string + PrometheusKeyFile string } // GRPC settings used in the bootstrap template. @@ -209,6 +218,12 @@ const bootstrapTemplate = `{ {{ .StaticListenersJSON }} ] {{- end }} + {{- if .StaticSecretsJSON -}} + , + "secrets": [ + {{ .StaticSecretsJSON }} + ] + {{- end }} }, {{- if .StatsSinksJSON }} "stats_sinks": {{ .StatsSinksJSON }}, diff --git a/command/connect/envoy/envoy.go b/command/connect/envoy/envoy.go index a78ae54d5..342357dfa 100644 --- a/command/connect/envoy/envoy.go +++ b/command/connect/envoy/envoy.go @@ -52,6 +52,10 @@ type cmd struct { envoyVersion string prometheusBackendPort string prometheusScrapePath string + prometheusCAFile string + prometheusCAPath string + prometheusCertFile string + prometheusKeyFile string // mesh gateway registration information register bool @@ -174,6 +178,19 @@ func (c *cmd) init() { "0.0.0.0:20200/scrape-metrics. "+ "Only applicable when envoy_prometheus_bind_addr is set in proxy config.") + c.flags.StringVar(&c.prometheusCAFile, "prometheus-ca-file", "", + "Path to a CA file for Envoy to use when serving TLS on the Prometheus metrics endpoint. "+ + "Only applicable when envoy_prometheus_bind_addr is set in proxy config.") + c.flags.StringVar(&c.prometheusCAPath, "prometheus-ca-path", "", + "Path to a directory of CA certificates for Envoy to use when serving the Prometheus metrics endpoint. "+ + "Only applicable when envoy_prometheus_bind_addr is set in proxy config.") + c.flags.StringVar(&c.prometheusCertFile, "prometheus-cert-file", "", + "Path to a certificate file for Envoy to use when serving TLS on the Prometheus metrics endpoint. "+ + "Only applicable when envoy_prometheus_bind_addr is set in proxy config.") + c.flags.StringVar(&c.prometheusKeyFile, "prometheus-key-file", "", + "Path to a private key file for Envoy to use when serving TLS on the Prometheus metrics endpoint. "+ + "Only applicable when envoy_prometheus_bind_addr is set in proxy config.") + c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.MultiTenancyFlags()) @@ -306,6 +323,15 @@ func (c *cmd) run(args []string) int { return 1 } + // If any of CA/Cert/Key are specified, make sure they are all present. + if c.prometheusKeyFile != "" || c.prometheusCertFile != "" || (c.prometheusCAFile != "" || c.prometheusCAPath != "") { + if c.prometheusKeyFile == "" || c.prometheusCertFile == "" || (c.prometheusCAFile == "" && c.prometheusCAPath == "") { + c.UI.Error("Must provide a CA (-prometheus-ca-file or -prometheus-ca-path) as well as " + + "-prometheus-cert-file and -prometheus-key-file to enable TLS for prometheus metrics") + return 1 + } + } + if c.register { if c.nodeName != "" { c.UI.Error("'-register' cannot be used with '-node-name'") @@ -505,6 +531,10 @@ func (c *cmd) templateArgs() (*BootstrapTplArgs, error) { Datacenter: httpCfg.Datacenter, PrometheusBackendPort: c.prometheusBackendPort, PrometheusScrapePath: c.prometheusScrapePath, + PrometheusCAFile: c.prometheusCAFile, + PrometheusCAPath: c.prometheusCAPath, + PrometheusCertFile: c.prometheusCertFile, + PrometheusKeyFile: c.prometheusKeyFile, }, nil } diff --git a/command/connect/envoy/envoy_test.go b/command/connect/envoy/envoy_test.go index 0e29a6934..4eedd16d4 100644 --- a/command/connect/envoy/envoy_test.go +++ b/command/connect/envoy/envoy_test.go @@ -211,6 +211,72 @@ func TestGenerateConfig(t *testing.T) { PrometheusScrapePath: "/scrape-path", }, }, + { + Name: "prometheus-metrics-tls-ca-file", + Flags: []string{"-proxy-id", "test-proxy", + "-prometheus-backend-port", "20100", "-prometheus-scrape-path", "/scrape-path", + "-prometheus-ca-file", "../../../test/key/ourdomain.cer", "-prometheus-cert-file", "../../../test/key/ourdomain_server.cer", + "-prometheus-key-file", "../../../test/key/ourdomain_server.key"}, + ProxyConfig: map[string]interface{}{ + // When envoy_prometheus_bind_addr is set, if + // PrometheusBackendPort is set, there will be a + // "prometheus_backend" cluster in the Envoy configuration. + "envoy_prometheus_bind_addr": "0.0.0.0:9000", + }, + WantArgs: BootstrapTplArgs{ + ProxyCluster: "test-proxy", + ProxyID: "test-proxy", + // We don't know this til after the lookup so it will be empty in the + // initial args call we are testing here. + ProxySourceService: "", + GRPC: GRPC{ + AgentAddress: "127.0.0.1", + AgentPort: "8502", // Note this is the gRPC port + }, + AdminAccessLogPath: "/dev/null", + AdminBindAddress: "127.0.0.1", + AdminBindPort: "19000", + LocalAgentClusterName: xds.LocalAgentClusterName, + PrometheusBackendPort: "20100", + PrometheusScrapePath: "/scrape-path", + PrometheusCAFile: "../../../test/key/ourdomain.cer", + PrometheusCertFile: "../../../test/key/ourdomain_server.cer", + PrometheusKeyFile: "../../../test/key/ourdomain_server.key", + }, + }, + { + Name: "prometheus-metrics-tls-ca-path", + Flags: []string{"-proxy-id", "test-proxy", + "-prometheus-backend-port", "20100", "-prometheus-scrape-path", "/scrape-path", + "-prometheus-ca-path", "../../../test/ca_path", "-prometheus-cert-file", "../../../test/key/ourdomain_server.cer", + "-prometheus-key-file", "../../../test/key/ourdomain_server.key"}, + ProxyConfig: map[string]interface{}{ + // When envoy_prometheus_bind_addr is set, if + // PrometheusBackendPort is set, there will be a + // "prometheus_backend" cluster in the Envoy configuration. + "envoy_prometheus_bind_addr": "0.0.0.0:9000", + }, + WantArgs: BootstrapTplArgs{ + ProxyCluster: "test-proxy", + ProxyID: "test-proxy", + // We don't know this til after the lookup so it will be empty in the + // initial args call we are testing here. + ProxySourceService: "", + GRPC: GRPC{ + AgentAddress: "127.0.0.1", + AgentPort: "8502", // Note this is the gRPC port + }, + AdminAccessLogPath: "/dev/null", + AdminBindAddress: "127.0.0.1", + AdminBindPort: "19000", + LocalAgentClusterName: xds.LocalAgentClusterName, + PrometheusBackendPort: "20100", + PrometheusScrapePath: "/scrape-path", + PrometheusCAPath: "../../../test/ca_path", + PrometheusCertFile: "../../../test/key/ourdomain_server.cer", + PrometheusKeyFile: "../../../test/key/ourdomain_server.key", + }, + }, { Name: "token-arg", Flags: []string{"-proxy-id", "test-proxy", diff --git a/command/connect/envoy/testdata/prometheus-metrics-tls-ca-file.golden b/command/connect/envoy/testdata/prometheus-metrics-tls-ca-file.golden new file mode 100644 index 000000000..239250c72 --- /dev/null +++ b/command/connect/envoy/testdata/prometheus-metrics-tls-ca-file.golden @@ -0,0 +1,320 @@ +{ + "admin": { + "access_log_path": "/dev/null", + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 19000 + } + } + }, + "node": { + "cluster": "test", + "id": "test-proxy", + "metadata": { + "namespace": "default", + "partition": "default" + } + }, + "static_resources": { + "clusters": [ + { + "name": "local_agent", + "ignore_health_on_host_removal": false, + "connect_timeout": "1s", + "type": "STATIC", + "http2_protocol_options": {}, + "loadAssignment": { + "clusterName": "local_agent", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 8502 + } + } + } + } + ] + } + ] + } + }, + { + "name": "prometheus_backend", + "ignore_health_on_host_removal": false, + "connect_timeout": "5s", + "type": "STATIC", + "http_protocol_options": {}, + "loadAssignment": { + "clusterName": "prometheus_backend", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 20100 + } + } + } + } + ] + } + ] + } + } + ], + "listeners": [ + { + "name": "envoy_prometheus_metrics_listener", + "address": { + "socket_address": { + "address": "0.0.0.0", + "port_value": 9000 + } + }, + "filter_chains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "envoy_prometheus_metrics", + "codec_type": "HTTP1", + "route_config": { + "name": "self_admin_route", + "virtual_hosts": [ + { + "name": "self_admin", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "path": "/scrape-path" + }, + "route": { + "cluster": "prometheus_backend", + "prefix_rewrite": "/stats/prometheus" + } + }, + { + "match": { + "prefix": "/" + }, + "direct_response": { + "status": 404 + } + } + ] + } + ] + }, + "http_filters": [ + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ] + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsCertificateSdsSecretConfigs": [ + { + "name": "prometheus_cert" + } + ], + "validationContextSdsSecretConfig": { + "trustedCa": { + "name": "prometheus_validation_context" + } + } + } + } + } + } + ] + } + ], + "secrets": [ + { + "name": "prometheus_cert", + "tlsCertificate": { + "certificateChain": { + "filename": "../../../test/key/ourdomain_server.cer" + }, + "privateKey": { + "filename": "../../../test/key/ourdomain_server.key" + } + } + }, + { + "name": "prometheus_validation_context", + "validationContext": { + "trustedCa": { + "filename": "../../../test/key/ourdomain.cer" + } + } + } + ] + }, + "stats_config": { + "stats_tags": [ + { + "regex": "^cluster\\.(?:passthrough~)?((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.custom_hash" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.service_subset" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.service" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.namespace" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.partition" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.datacenter" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.routing_type" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)", + "tag_name": "consul.destination.trust_domain" + }, + { + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.target" + }, + { + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)", + "tag_name": "consul.destination.full_target" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.(([^.]+)(?:\\.[^.]+)?(?:\\.[^.]+)?\\.[^.]+\\.)", + "tag_name": "consul.upstream.service" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.[^.]+)?\\.([^.]+)\\.)", + "tag_name": "consul.upstream.datacenter" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.([^.]+))?(?:\\.[^.]+)?\\.[^.]+\\.)", + "tag_name": "consul.upstream.namespace" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.([^.]+))?\\.[^.]+\\.)", + "tag_name": "consul.upstream.partition" + }, + { + "regex": "^cluster\\.((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.custom_hash" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.service_subset" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.service" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.namespace" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.datacenter" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)", + "tag_name": "consul.routing_type" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)", + "tag_name": "consul.trust_domain" + }, + { + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.target" + }, + { + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)", + "tag_name": "consul.full_target" + }, + { + "tag_name": "local_cluster", + "fixed_value": "test" + }, + { + "tag_name": "consul.source.service", + "fixed_value": "test" + }, + { + "tag_name": "consul.source.namespace", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.partition", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.datacenter", + "fixed_value": "dc1" + } + ], + "use_all_default_tags": true + }, + "dynamic_resources": { + "lds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "cds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "ads_config": { + "api_type": "DELTA_GRPC", + "transport_api_version": "V3", + "grpc_services": { + "initial_metadata": [ + { + "key": "x-consul-token", + "value": "" + } + ], + "envoy_grpc": { + "cluster_name": "local_agent" + } + } + } + } +} + diff --git a/command/connect/envoy/testdata/prometheus-metrics-tls-ca-path.golden b/command/connect/envoy/testdata/prometheus-metrics-tls-ca-path.golden new file mode 100644 index 000000000..c00fa54ed --- /dev/null +++ b/command/connect/envoy/testdata/prometheus-metrics-tls-ca-path.golden @@ -0,0 +1,320 @@ +{ + "admin": { + "access_log_path": "/dev/null", + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 19000 + } + } + }, + "node": { + "cluster": "test", + "id": "test-proxy", + "metadata": { + "namespace": "default", + "partition": "default" + } + }, + "static_resources": { + "clusters": [ + { + "name": "local_agent", + "ignore_health_on_host_removal": false, + "connect_timeout": "1s", + "type": "STATIC", + "http2_protocol_options": {}, + "loadAssignment": { + "clusterName": "local_agent", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 8502 + } + } + } + } + ] + } + ] + } + }, + { + "name": "prometheus_backend", + "ignore_health_on_host_removal": false, + "connect_timeout": "5s", + "type": "STATIC", + "http_protocol_options": {}, + "loadAssignment": { + "clusterName": "prometheus_backend", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 20100 + } + } + } + } + ] + } + ] + } + } + ], + "listeners": [ + { + "name": "envoy_prometheus_metrics_listener", + "address": { + "socket_address": { + "address": "0.0.0.0", + "port_value": 9000 + } + }, + "filter_chains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "envoy_prometheus_metrics", + "codec_type": "HTTP1", + "route_config": { + "name": "self_admin_route", + "virtual_hosts": [ + { + "name": "self_admin", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "path": "/scrape-path" + }, + "route": { + "cluster": "prometheus_backend", + "prefix_rewrite": "/stats/prometheus" + } + }, + { + "match": { + "prefix": "/" + }, + "direct_response": { + "status": 404 + } + } + ] + } + ] + }, + "http_filters": [ + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ] + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsCertificateSdsSecretConfigs": [ + { + "name": "prometheus_cert" + } + ], + "validationContextSdsSecretConfig": { + "trustedCa": { + "name": "prometheus_validation_context" + } + } + } + } + } + } + ] + } + ], + "secrets": [ + { + "name": "prometheus_cert", + "tlsCertificate": { + "certificateChain": { + "filename": "../../../test/key/ourdomain_server.cer" + }, + "privateKey": { + "filename": "../../../test/key/ourdomain_server.key" + } + } + }, + { + "name": "prometheus_validation_context", + "validationContext": { + "watchedDirectory": { + "path": "../../../test/ca_path" + } + } + } + ] + }, + "stats_config": { + "stats_tags": [ + { + "regex": "^cluster\\.(?:passthrough~)?((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.custom_hash" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.service_subset" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.service" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.namespace" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.partition" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.datacenter" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.routing_type" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)", + "tag_name": "consul.destination.trust_domain" + }, + { + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.target" + }, + { + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)", + "tag_name": "consul.destination.full_target" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.(([^.]+)(?:\\.[^.]+)?(?:\\.[^.]+)?\\.[^.]+\\.)", + "tag_name": "consul.upstream.service" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.[^.]+)?\\.([^.]+)\\.)", + "tag_name": "consul.upstream.datacenter" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.([^.]+))?(?:\\.[^.]+)?\\.[^.]+\\.)", + "tag_name": "consul.upstream.namespace" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.([^.]+))?\\.[^.]+\\.)", + "tag_name": "consul.upstream.partition" + }, + { + "regex": "^cluster\\.((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.custom_hash" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.service_subset" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.service" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.namespace" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.datacenter" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)", + "tag_name": "consul.routing_type" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)", + "tag_name": "consul.trust_domain" + }, + { + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.target" + }, + { + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)", + "tag_name": "consul.full_target" + }, + { + "tag_name": "local_cluster", + "fixed_value": "test" + }, + { + "tag_name": "consul.source.service", + "fixed_value": "test" + }, + { + "tag_name": "consul.source.namespace", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.partition", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.datacenter", + "fixed_value": "dc1" + } + ], + "use_all_default_tags": true + }, + "dynamic_resources": { + "lds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "cds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "ads_config": { + "api_type": "DELTA_GRPC", + "transport_api_version": "V3", + "grpc_services": { + "initial_metadata": [ + { + "key": "x-consul-token", + "value": "" + } + ], + "envoy_grpc": { + "cluster_name": "local_agent" + } + } + } + } +} + diff --git a/test/integration/consul-container/go.mod b/test/integration/consul-container/go.mod index b5a7b1738..182ff2e3a 100644 --- a/test/integration/consul-container/go.mod +++ b/test/integration/consul-container/go.mod @@ -7,6 +7,7 @@ require ( github.com/hashicorp/consul/api v1.11.0 github.com/hashicorp/consul/sdk v0.8.0 github.com/hashicorp/go-uuid v1.0.2 + github.com/hashicorp/hcl v1.0.0 github.com/stretchr/testify v1.7.0 github.com/testcontainers/testcontainers-go v0.13.0 ) @@ -14,11 +15,11 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.4.17 // indirect - github.com/Microsoft/hcsshim v0.8.23 // indirect + github.com/Microsoft/hcsshim v0.8.24 // indirect github.com/armon/go-metrics v0.3.10 // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect - github.com/containerd/cgroups v1.0.1 // indirect - github.com/containerd/containerd v1.5.9 // indirect + github.com/containerd/cgroups v1.0.3 // indirect + github.com/containerd/containerd v1.5.13 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect diff --git a/test/integration/consul-container/go.sum b/test/integration/consul-container/go.sum index 7d67cf3ca..1ec317a43 100644 --- a/test/integration/consul-container/go.sum +++ b/test/integration/consul-container/go.sum @@ -55,8 +55,9 @@ github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg3 github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.23 h1:47MSwtKGXet80aIn+7h4YI6fwPmwIghAnsx2aOUrG2M= github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= +github.com/Microsoft/hcsshim v0.8.24 h1:jP+GMeRXIR1sH1kG4lJr9ShmSjVrua5jmFZDtfYGkn4= +github.com/Microsoft/hcsshim v0.8.24/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -130,8 +131,9 @@ github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1 github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= +github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= @@ -150,8 +152,9 @@ github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7 github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.9 h1:rs6Xg1gtIxaeyG+Smsb/0xaSDu1VgFhOCKBXxMxbsF4= github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ= +github.com/containerd/containerd v1.5.13 h1:XqvKw9i4P7/mFrC3TSM7yV5cwFZ9avXe6M3YANKnzEE= +github.com/containerd/containerd v1.5.13/go.mod h1:3AlCrzKROjIuP3JALsY14n8YtntaUDBu7vek+rPN5Vc= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -424,6 +427,7 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= @@ -699,6 +703,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= @@ -715,6 +720,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -761,6 +767,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -797,6 +804,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= @@ -886,6 +894,7 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -946,6 +955,7 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/test/integration/consul-container/libs/node/consul-container.go b/test/integration/consul-container/libs/node/consul-container.go index 598724170..caa8a7409 100644 --- a/test/integration/consul-container/libs/node/consul-container.go +++ b/test/integration/consul-container/libs/node/consul-container.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/pkg/ioutils" "github.com/hashicorp/consul/api" + "github.com/hashicorp/hcl" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -48,7 +49,6 @@ func newConsulContainerWithReq(ctx context.Context, req testcontainers.Container // NewConsulContainer starts a Consul node in a container with the given config. func NewConsulContainer(ctx context.Context, config Config) (Node, error) { - license, err := readLicense() if err != nil { return nil, err @@ -64,30 +64,29 @@ func NewConsulContainer(ctx context.Context, config Config) (Node, error) { return nil, err } + pc, err := readSomeConfigFileFields(config.HCL) + if err != nil { + return nil, err + } + configFile, err := createConfigFile(config.HCL) if err != nil { return nil, err } - skipReaper := isRYUKDisabled() - req := testcontainers.ContainerRequest{ - Image: consulImage + ":" + config.Version, - ExposedPorts: []string{"8500/tcp"}, - WaitingFor: wait.ForLog(bootLogLine).WithStartupTimeout(10 * time.Second), - AutoRemove: false, - Name: name, - Mounts: testcontainers.ContainerMounts{ - testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: configFile}, Target: "/consul/config/config.hcl"}, - testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: tmpDirData}, Target: "/consul/data"}, - }, - Cmd: config.Cmd, - SkipReaper: skipReaper, - Env: map[string]string{"CONSUL_LICENSE": license}, - } + + req := newContainerRequest(config, name, configFile, tmpDirData, license) container, err := newConsulContainerWithReq(ctx, req) if err != nil { return nil, err } + if err := container.StartLogProducer(ctx); err != nil { + return nil, err + } + container.FollowOutput(&NodeLogConsumer{ + Prefix: pc.NodeName, + }) + localIP, err := container.Host(ctx) if err != nil { return nil, err @@ -104,21 +103,42 @@ func NewConsulContainer(ctx context.Context, config Config) (Node, error) { } uri := fmt.Sprintf("http://%s:%s", localIP, mappedPort.Port()) - c := new(consulContainerNode) - c.config = config - c.container = container - c.ip = ip - c.port = mappedPort.Int() apiConfig := api.DefaultConfig() apiConfig.Address = uri - c.client, err = api.NewClient(apiConfig) - c.ctx = ctx - c.req = req - c.dataDir = tmpDirData + apiClient, err := api.NewClient(apiConfig) if err != nil { return nil, err } - return c, nil + + return &consulContainerNode{ + config: config, + container: container, + ip: ip, + port: mappedPort.Int(), + client: apiClient, + ctx: ctx, + req: req, + dataDir: tmpDirData, + }, nil +} + +func newContainerRequest(config Config, name, configFile, dataDir, license string) testcontainers.ContainerRequest { + skipReaper := isRYUKDisabled() + + return testcontainers.ContainerRequest{ + Image: consulImage + ":" + config.Version, + ExposedPorts: []string{"8500/tcp"}, + WaitingFor: wait.ForLog(bootLogLine).WithStartupTimeout(10 * time.Second), + AutoRemove: false, + Name: name, + Mounts: []testcontainers.ContainerMount{ + {Source: testcontainers.DockerBindMountSource{HostPath: configFile}, Target: "/consul/config/config.hcl"}, + {Source: testcontainers.DockerBindMountSource{HostPath: dataDir}, Target: "/consul/data"}, + }, + Cmd: config.Cmd, + SkipReaper: skipReaper, + Env: map[string]string{"CONSUL_LICENSE": license}, + } } // GetClient returns an API client that can be used to communicate with the Node. @@ -132,25 +152,44 @@ func (c *consulContainerNode) GetAddr() (string, int) { } func (c *consulContainerNode) Upgrade(ctx context.Context, config Config) error { + pc, err := readSomeConfigFileFields(config.HCL) + if err != nil { + return err + } + file, err := createConfigFile(config.HCL) if err != nil { return err } - c.req.Cmd = config.Cmd - c.req.Mounts = testcontainers.ContainerMounts{ - testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: file}, Target: "/consul/config/config.hcl"}, - testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: c.dataDir}, Target: "/consul/data"}, - } - c.req.Image = consulImage + ":" + config.Version - err = c.container.Terminate(ctx) - if err != nil { + + req2 := newContainerRequest( + config, + c.req.Name, + file, + c.dataDir, + "", + ) + req2.Env = c.req.Env // copy license + + _ = c.container.StopLogProducer() + if err := c.container.Terminate(ctx); err != nil { return err } + + c.req = req2 + container, err := newConsulContainerWithReq(ctx, c.req) if err != nil { return err } + if err := container.StartLogProducer(ctx); err != nil { + return err + } + container.FollowOutput(&NodeLogConsumer{ + Prefix: pc.NodeName, + }) + c.container = container localIP, err := container.Host(ctx) @@ -185,7 +224,19 @@ func (c *consulContainerNode) Upgrade(ctx context.Context, config Config) error // Terminate attempts to terminate the container. On failure, an error will be // returned and the reaper process (RYUK) will handle cleanup. func (c *consulContainerNode) Terminate() error { - return c.container.Terminate(c.ctx) + if c.container == nil { + return nil + } + + err := c.container.StopLogProducer() + + if err1 := c.container.Terminate(c.ctx); err == nil { + err = err1 + } + + c.container = nil + + return err } // isRYUKDisabled returns whether the reaper process (RYUK) has been disabled @@ -236,3 +287,15 @@ func createConfigFile(HCL string) (string, error) { } return configFile, nil } + +type parsedConfig struct { + NodeName string `hcl:"node_name"` +} + +func readSomeConfigFileFields(HCL string) (parsedConfig, error) { + var pc parsedConfig + if err := hcl.Decode(&pc, HCL); err != nil { + return pc, fmt.Errorf("Failed to parse config file: %w", err) + } + return pc, nil +} diff --git a/test/integration/consul-container/libs/node/log.go b/test/integration/consul-container/libs/node/log.go new file mode 100644 index 000000000..58b63f2ea --- /dev/null +++ b/test/integration/consul-container/libs/node/log.go @@ -0,0 +1,23 @@ +package node + +import ( + "fmt" + "os" + + "github.com/testcontainers/testcontainers-go" +) + +type NodeLogConsumer struct { + Prefix string +} + +var _ testcontainers.LogConsumer = (*NodeLogConsumer)(nil) + +func (c *NodeLogConsumer) Accept(log testcontainers.Log) { + switch log.LogType { + case "STDOUT": + fmt.Fprint(os.Stdout, c.Prefix+" ~~ "+string(log.Content)) + case "STDERR": + fmt.Fprint(os.Stderr, c.Prefix+" ~~ "+string(log.Content)) + } +} diff --git a/test/integration/consul-container/libs/node/node.go b/test/integration/consul-container/libs/node/node.go index 69e5c7e50..6f02a446c 100644 --- a/test/integration/consul-container/libs/node/node.go +++ b/test/integration/consul-container/libs/node/node.go @@ -2,19 +2,18 @@ package node import ( "context" + "github.com/hashicorp/consul/api" ) -type ( - // Node represent a Consul node abstraction - Node interface { - Terminate() error - GetClient() *api.Client - GetAddr() (string, int) - GetConfig() Config - Upgrade(ctx context.Context, config Config) error - } -) +// Node represent a Consul node abstraction +type Node interface { + Terminate() error + GetClient() *api.Client + GetAddr() (string, int) + GetConfig() Config + Upgrade(ctx context.Context, config Config) error +} // Config is a set of configurations required to create a Node type Config struct { diff --git a/test/integration/consul-container/metrics/leader_test.go b/test/integration/consul-container/metrics/leader_test.go index 9bea12592..08cf0e392 100644 --- a/test/integration/consul-container/metrics/leader_test.go +++ b/test/integration/consul-container/metrics/leader_test.go @@ -23,7 +23,7 @@ func TestLeadershipMetrics(t *testing.T) { configs = append(configs, node.Config{ HCL: `node_name="` + utils.RandName("consul-server") + `" - log_level="TRACE" + log_level="DEBUG" server=true telemetry { statsite_address = "127.0.0.1:2180" @@ -37,7 +37,7 @@ func TestLeadershipMetrics(t *testing.T) { configs = append(configs, node.Config{ HCL: `node_name="` + utils.RandName("consul-server") + `" - log_level="TRACE" + log_level="DEBUG" bootstrap_expect=3 server=true`, Cmd: []string{"agent", "-client=0.0.0.0"}, diff --git a/test/integration/consul-container/upgrade/healthcheck_test.go b/test/integration/consul-container/upgrade/healthcheck_test.go index 73c8a807b..7cc88ad6e 100644 --- a/test/integration/consul-container/upgrade/healthcheck_test.go +++ b/test/integration/consul-container/upgrade/healthcheck_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/hashicorp/consul/api" + libcluster "github.com/hashicorp/consul/integration/consul-container/libs/cluster" "github.com/hashicorp/consul/integration/consul-container/libs/node" "github.com/hashicorp/consul/integration/consul-container/libs/utils" @@ -74,7 +75,7 @@ func TestMixedServersMajorityLatestGAClient(t *testing.T) { configs = append(configs, node.Config{ HCL: `node_name="` + utils.RandName("consul-server") + `" - log_level="TRACE" + log_level="DEBUG" server=true`, Cmd: []string{"agent", "-client=0.0.0.0"}, Version: *utils.TargetImage, @@ -84,7 +85,7 @@ func TestMixedServersMajorityLatestGAClient(t *testing.T) { configs = append(configs, node.Config{ HCL: `node_name="` + utils.RandName("consul-server") + `" - log_level="TRACE" + log_level="DEBUG" bootstrap_expect=3 server=true`, Cmd: []string{"agent", "-client=0.0.0.0"}, @@ -151,7 +152,7 @@ func TestMixedServersMajorityTargetGAClient(t *testing.T) { configs = append(configs, node.Config{ HCL: `node_name="` + utils.RandName("consul-server") + `" - log_level="TRACE" + log_level="DEBUG" bootstrap_expect=3 server=true`, Cmd: []string{"agent", "-client=0.0.0.0"}, @@ -162,7 +163,7 @@ func TestMixedServersMajorityTargetGAClient(t *testing.T) { configs = append(configs, node.Config{ HCL: `node_name="` + utils.RandName("consul-server") + `" - log_level="TRACE" + log_level="DEBUG" server=true`, Cmd: []string{"agent", "-client=0.0.0.0"}, Version: *utils.LatestImage, @@ -227,7 +228,7 @@ func clientsCreate(t *testing.T, numClients int, version string, serfKey string) node.Config{ HCL: fmt.Sprintf(` node_name = %q - log_level = "TRACE" + log_level = "DEBUG" encrypt = %q`, utils.RandName("consul-client"), serfKey), Cmd: []string{"agent", "-client=0.0.0.0"}, Version: version, @@ -255,7 +256,7 @@ func serversCluster(t *testing.T, numServers int, version string) *libcluster.Cl for i := 0; i < numServers; i++ { configs = append(configs, node.Config{ HCL: `node_name="` + utils.RandName("consul-server") + `" - log_level="TRACE" + log_level="DEBUG" bootstrap_expect=3 server=true`, Cmd: []string{"agent", "-client=0.0.0.0"}, diff --git a/ui/packages/consul-ui/app/adapters/intention.js b/ui/packages/consul-ui/app/adapters/intention.js index f81b3645b..c28193d0f 100644 --- a/ui/packages/consul-ui/app/adapters/intention.js +++ b/ui/packages/consul-ui/app/adapters/intention.js @@ -36,16 +36,40 @@ export default class IntentionAdapter extends Adapter { // get the information we need from the id, which has been previously // encoded - const [ - SourcePartition, - SourceNS, - SourceName, - DestinationPartition, - DestinationNS, - DestinationName, - ] = id.split(':').map(decodeURIComponent); + if (id.match(/^peer:/)) { + // id indicates we are dealing with a peered intention - handle this with + // different query-param for source - we need to prepend the peer + const [ + peerIdentifier, + SourcePeer, + SourceNS, + SourceName, + DestinationPartition, + DestinationNS, + DestinationName, + ] = id.split(':').map(decodeURIComponent); - return request` + return request` + GET /v1/connect/intentions/exact?${{ + source: `${peerIdentifier}:${SourcePeer}/${SourceNS}/${SourceName}`, + destination: `${DestinationPartition}/${DestinationNS}/${DestinationName}`, + dc: dc, + }} + Cache-Control: no-store + + ${{ index }} + `; + } else { + const [ + SourcePartition, + SourceNS, + SourceName, + DestinationPartition, + DestinationNS, + DestinationName, + ] = id.split(':').map(decodeURIComponent); + + return request` GET /v1/connect/intentions/exact?${{ source: `${SourcePartition}/${SourceNS}/${SourceName}`, destination: `${DestinationPartition}/${DestinationNS}/${DestinationName}`, @@ -55,6 +79,7 @@ export default class IntentionAdapter extends Adapter { ${{ index }} `; + } } requestForCreateRecord(request, serialized, data) { diff --git a/ui/packages/consul-ui/app/adapters/node.js b/ui/packages/consul-ui/app/adapters/node.js index 3d8065c52..e9520ba5d 100644 --- a/ui/packages/consul-ui/app/adapters/node.js +++ b/ui/packages/consul-ui/app/adapters/node.js @@ -1,4 +1,5 @@ import Adapter from './application'; +import { inject as service } from '@ember/service'; // TODO: Update to use this.formatDatacenter() @@ -10,6 +11,18 @@ import Adapter from './application'; // to the node. export default class NodeAdapter extends Adapter { + @service features; + + get peeringQuery() { + const query = {}; + + if (this.features.isEnabled('peering')) { + query['with-peers'] = true; + } + + return query; + } + requestForQuery(request, { dc, ns, partition, index, id, uri }) { return request` GET /v1/internal/ui/nodes?${{ dc }} @@ -19,23 +32,32 @@ export default class NodeAdapter extends Adapter { ns, partition, index, + ...this.peeringQuery, }} `; } - requestForQueryRecord(request, { dc, ns, partition, index, id, uri }) { + requestForQueryRecord(request, { dc, ns, partition, index, id, uri, peer }) { if (typeof id === 'undefined') { throw new Error('You must specify an id'); } + let options = { + ns, + partition, + index, + }; + + if (peer) { + options = { + ...options, + peer, + }; + } return request` GET /v1/internal/ui/node/${id}?${{ dc }} X-Request-ID: ${uri} - ${{ - ns, - partition, - index, - }} + ${options} `; } diff --git a/ui/packages/consul-ui/app/adapters/peer.js b/ui/packages/consul-ui/app/adapters/peer.js new file mode 100644 index 000000000..096ed0eeb --- /dev/null +++ b/ui/packages/consul-ui/app/adapters/peer.js @@ -0,0 +1,9 @@ +import JSONAPIAdapter from '@ember-data/adapter/json-api'; + +export default class PeerAdapter extends JSONAPIAdapter { + namespace = 'v1'; + + pathForType(_modelName) { + return 'peerings'; + } +} diff --git a/ui/packages/consul-ui/app/adapters/service-instance.js b/ui/packages/consul-ui/app/adapters/service-instance.js index 009944e79..093d1f791 100644 --- a/ui/packages/consul-ui/app/adapters/service-instance.js +++ b/ui/packages/consul-ui/app/adapters/service-instance.js @@ -2,20 +2,30 @@ import Adapter from './application'; // TODO: Update to use this.formatDatacenter() export default class ServiceInstanceAdapter extends Adapter { - requestForQuery(request, { dc, ns, partition, index, id, uri }) { + requestForQuery(request, { dc, ns, partition, index, id, uri, peer }) { if (typeof id === 'undefined') { throw new Error('You must specify an id'); } + + let options = { + ns, + partition, + index, + }; + + if (peer) { + options = { + ...options, + peer, + }; + } + return request` GET /v1/health/service/${id}?${{ dc }} X-Request-ID: ${uri} X-Range: ${id} - ${{ - ns, - partition, - index, - }} + ${options} `; } diff --git a/ui/packages/consul-ui/app/adapters/service.js b/ui/packages/consul-ui/app/adapters/service.js index ea69f0927..a80427034 100644 --- a/ui/packages/consul-ui/app/adapters/service.js +++ b/ui/packages/consul-ui/app/adapters/service.js @@ -1,6 +1,19 @@ import Adapter from './application'; +import { inject as service } from '@ember/service'; export default class ServiceAdapter extends Adapter { + @service features; + + get peeringQuery() { + const query = {}; + + if (this.features.isEnabled('peering')) { + query['with-peers'] = true; + } + + return query; + } + requestForQuery(request, { dc, ns, partition, index, gateway, uri }) { if (typeof gateway !== 'undefined') { return request` @@ -23,6 +36,7 @@ export default class ServiceAdapter extends Adapter { ns, partition, index, + ...this.peeringQuery, }} `; } diff --git a/ui/packages/consul-ui/app/components/consul/bucket/list/README.mdx b/ui/packages/consul-ui/app/components/consul/bucket/list/README.mdx index 0d9d7ccc1..d6bdc01c6 100644 --- a/ui/packages/consul-ui/app/components/consul/bucket/list/README.mdx +++ b/ui/packages/consul-ui/app/components/consul/bucket/list/README.mdx @@ -22,6 +22,20 @@ At the time of writing, this is not currently used across the entire UI ```hbs preview-template
Show everything
+ +
+
+
Show everything without peer
+
+
Show only peer-info
+ +
+ +
+
Don't surface anything - no relevant info to show
+ +
+ ``` ## Arguments diff --git a/ui/packages/consul-ui/app/components/consul/bucket/list/index.hbs b/ui/packages/consul-ui/app/components/consul/bucket/list/index.hbs index d8bc488d0..12751479a 100644 --- a/ui/packages/consul-ui/app/components/consul/bucket/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/bucket/list/index.hbs @@ -1,60 +1,12 @@ -{{#if (and @partition (can 'use partitions'))}} - {{#if (not-eq @item.Partition @partition)}} -
-
- Admin Partition +{{#if this.itemsToDisplay.length}} +
+ {{#each this.itemsToDisplay as |item|}} +
+ {{item.label}}
-
- {{@item.Partition}} -
-
- Namespace -
-
- {{@item.Namespace}} -
- -{{#if (and @service @item.Service)}} -
- Service -
-
- {{@item.Service}} +
+ {{item.item}}
-{{/if}} - -
- {{/if}} -{{else if (and @nspace (can 'use nspace'))}} - {{#if (not-eq @item.Namespace @nspace)}} -
-
- Namespace -
-
- {{@item.Namespace}} -
-{{#if (and @service @item.Service)}} -
- Service -
-
- {{@item.Service}} -
-{{/if}} -
- {{/if}} + {{/each}} +
{{/if}} diff --git a/ui/packages/consul-ui/app/components/consul/bucket/list/index.js b/ui/packages/consul-ui/app/components/consul/bucket/list/index.js new file mode 100644 index 000000000..9c48f1bd9 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/bucket/list/index.js @@ -0,0 +1,88 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +export default class ConsulBucketList extends Component { + @service abilities; + + get itemsToDisplay() { + const { item, partition, nspace } = this.args; + const { abilities } = this; + + let items = []; + + if (partition && abilities.can('use partitions')) { + if (item.Partition !== partition) { + this._addPeer(items); + this._addPartition(items); + this._addNamespace(items); + this._addService(items); + } else { + this._addPeerInfo(items); + } + } else if (nspace && abilities.can('use nspace')) { + if (item.Namespace !== nspace) { + this._addPeerInfo(items); + this._addService(items); + } else { + this._addPeerInfo(items); + } + } else { + this._addPeerInfo(items); + } + + return items; + } + + _addPeerInfo(items) { + const { item } = this.args; + + if (item.PeerName) { + this._addPeer(items); + this._addNamespace(items); + } + } + + _addPartition(items) { + const { item } = this.args; + + items.push({ + type: 'partition', + label: 'Admin Partition', + item: item.Partition, + }); + } + + _addNamespace(items) { + const { item } = this.args; + + items.push({ + type: 'nspace', + label: 'Namespace', + item: item.Namespace, + }); + } + + _addService(items) { + const { service, item } = this.args; + + if (service && item.Service) { + items.push({ + type: 'service', + label: 'Service', + item: item.Service, + }); + } + } + + _addPeer(items) { + const { item } = this.args; + + if (item?.PeerName) { + items.push({ + type: 'peer', + label: 'Peer', + item: item.PeerName, + }); + } + } +} diff --git a/ui/packages/consul-ui/app/components/consul/bucket/list/index.scss b/ui/packages/consul-ui/app/components/consul/bucket/list/index.scss index 1a78063ed..c49c5907b 100644 --- a/ui/packages/consul-ui/app/components/consul/bucket/list/index.scss +++ b/ui/packages/consul-ui/app/components/consul/bucket/list/index.scss @@ -11,6 +11,9 @@ .service { @extend %visually-hidden; } + .peer::before { + @extend %with-network-alt-mask, %as-pseudo; + } .service + dd { font-weight: var(--typo-weight-semibold); } diff --git a/ui/packages/consul-ui/app/components/consul/intention/list/table/index.hbs b/ui/packages/consul-ui/app/components/consul/intention/list/table/index.hbs index beeb91cd6..4bb110a89 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/list/table/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/intention/list/table/index.hbs @@ -26,10 +26,37 @@ as |item index|> {{/if}} {{#if (or (can 'use nspaces') (can 'use partitions'))}} {{! TODO: slugify }} - + + {{#if item.SourcePeer}} + + + + + + + + + {{item.SourcePeer}} + + {{else}} + + {{or item.SourcePartition 'default'}} + + {{/if}} + / {{or item.SourcePartition 'default'}} / {{or item.SourceNS 'default'}} diff --git a/ui/packages/consul-ui/app/components/consul/intention/list/table/index.scss b/ui/packages/consul-ui/app/components/consul/intention/list/table/index.scss new file mode 100644 index 000000000..dc1da5692 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/intention/list/table/index.scss @@ -0,0 +1,8 @@ +.consul-intention-list-table__meta-info { + display: flex; + + .consul-intention-list-table__meta-info__peer { + display: flex; + align-items: center; + } +} diff --git a/ui/packages/consul-ui/app/components/consul/intention/view/index.hbs b/ui/packages/consul-ui/app/components/consul/intention/view/index.hbs index e77f07c50..22b493b0b 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/view/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/intention/view/index.hbs @@ -25,6 +25,7 @@ Namespace=item.SourceNS Partition=item.SourcePartition Service=item.SourceName + PeerName=item.SourcePeer }} @nspace="-" @partition="-" diff --git a/ui/packages/consul-ui/app/components/consul/node/list/index.hbs b/ui/packages/consul-ui/app/components/consul/node/list/index.hbs index 2fd449b12..728104814 100644 --- a/ui/packages/consul-ui/app/components/consul/node/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/node/list/index.hbs @@ -21,14 +21,15 @@ as |item index|> - + {{item.Node}} - {{#if (eq item.Address @leader.Address)}} - Leader - {{/if}} + + {{#if (eq item.Address @leader.Address)}} + Leader + {{/if}} {{format-number item.MeshServiceInstances.length}} {{pluralize item.MeshServiceInstances.length 'Service' without-count=true}} diff --git a/ui/packages/consul-ui/app/components/consul/node/peer-info/index.hbs b/ui/packages/consul-ui/app/components/consul/node/peer-info/index.hbs new file mode 100644 index 000000000..720314209 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/node/peer-info/index.hbs @@ -0,0 +1,22 @@ +{{#if @item.PeerName}} + + + + + + + + + {{@item.PeerName}} + +{{/if}} diff --git a/ui/packages/consul-ui/app/components/consul/node/peer-info/index.scss b/ui/packages/consul-ui/app/components/consul/node/peer-info/index.scss new file mode 100644 index 000000000..b272a2030 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/node/peer-info/index.scss @@ -0,0 +1,8 @@ +.consul-node-peer-info { + display: flex; + align-items: center; + + .consul-node-peer-info__name { + margin-left: 4px; + } +} diff --git a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs index f7643af0f..551cf027c 100644 --- a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs @@ -32,8 +32,11 @@ (hash partition=item.Partition nspace=item.Namespace + peer=item.PeerName + ) + (hash + peer=item.PeerName ) - (hash) ) }} > diff --git a/ui/packages/consul-ui/app/components/consul/service/peer-info/index.hbs b/ui/packages/consul-ui/app/components/consul/service/peer-info/index.hbs new file mode 100644 index 000000000..5ed3fd4e8 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/service/peer-info/index.hbs @@ -0,0 +1,23 @@ +{{#if @service.PeerName}} +
+ + + + + + + + + Imported from {{@service.PeerName}} +
+{{/if}} diff --git a/ui/packages/consul-ui/app/components/consul/service/peer-info/index.scss b/ui/packages/consul-ui/app/components/consul/service/peer-info/index.scss new file mode 100644 index 000000000..88be476fa --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/service/peer-info/index.scss @@ -0,0 +1,13 @@ +.consul-service-peer-info { + background: rgb(var(--gray-100)); + color: rgb(var(--gray-600)); + padding: 0px 8px; + border-radius: 2px; + + display: flex; + align-items: center; + + .consul-service-peer-info__description { + margin-left: 4px; + } +} diff --git a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs index 08feee5b1..2a98a7685 100644 --- a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs +++ b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs @@ -81,7 +81,7 @@ <:home-nav> @@ -115,7 +115,7 @@ }} > Overview @@ -123,22 +123,22 @@ {{/if}} {{#if (can "read services")}}
  • - Services + Services
  • {{/if}} {{#if (can "read nodes")}}
  • - Nodes + Nodes
  • {{/if}} {{#if (can "read kv")}}
  • - Key/Value + Key/Value
  • {{/if}} {{#if (can "read intentions")}}
  • - Intentions + Intentions
  • {{/if}} + {{#if (feature-flag "peering")}} +
  • + Organization +
  • +
  • + Peers +
  • + {{/if}} diff --git a/ui/packages/consul-ui/app/components/peerings/badge/icon/index.hbs b/ui/packages/consul-ui/app/components/peerings/badge/icon/index.hbs new file mode 100644 index 000000000..16153d91f --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/badge/icon/index.hbs @@ -0,0 +1,86 @@ +{{#if (or (eq @state "PENDING") (eq @state "ESTABLISHING"))}} + + + + +{{/if}} +{{#if (eq @state "ACTIVE")}} + + + +{{/if}} +{{#if (eq @state "FAILING")}} + + + +{{/if}} + +{{#if (eq @state "TERMINATED")}} + + + +{{/if}} +{{#if (eq @state "UNDEFINED")}} + + + + + +{{/if}} + +{{#if (eq @state "DELETING")}} + + + + +{{/if}} diff --git a/ui/packages/consul-ui/app/components/peerings/badge/index.hbs b/ui/packages/consul-ui/app/components/peerings/badge/index.hbs new file mode 100644 index 000000000..d90d1cbf0 --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/badge/index.hbs @@ -0,0 +1,6 @@ +{{#if @peering.State}} +
    + + {{capitalize (lowercase @peering.State)}} +
    +{{/if}} diff --git a/ui/packages/consul-ui/app/components/peerings/badge/index.js b/ui/packages/consul-ui/app/components/peerings/badge/index.js new file mode 100644 index 000000000..7fd211d4d --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/badge/index.js @@ -0,0 +1,37 @@ +import Component from '@glimmer/component'; + +const BADGE_LOOKUP = { + ACTIVE: { + tooltip: 'This peer connection is currently active.', + }, + PENDING: { + tooltip: 'This peering connection has not been established yet.', + }, + ESTABLISHING: { + tooltip: 'This peering connection is in the process of being established.', + }, + FAILING: { + tooltip: + 'This peering connection has some intermittent errors (usually network related). It will continue to retry. ', + }, + DELETING: { + tooltip: 'This peer is in the process of being deleted.', + }, + TERMINATED: { + tooltip: 'Someone in the other peer may have deleted this peering connection.', + }, + UNDEFINED: {}, +}; +export default class PeeingsBadge extends Component { + get styles() { + const { + peering: { State }, + } = this.args; + + return BADGE_LOOKUP[State]; + } + + get tooltip() { + return this.styles.tooltip; + } +} diff --git a/ui/packages/consul-ui/app/components/peerings/badge/index.scss b/ui/packages/consul-ui/app/components/peerings/badge/index.scss new file mode 100644 index 000000000..e61a22222 --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/badge/index.scss @@ -0,0 +1,42 @@ +.peerings-badge { + display: flex; + align-items: center; + justify-content: center; + padding: 2px 8px; + border-radius: 5px; + gap: 4px; + + &.active { + background: rgb(var(--tone-green-050)); + color: rgb(var(--tone-green-600)); + } + &.pending { + background: rgb(var(--tone-strawberry-050)); + color: rgb(var(--tone-strawberry-500)); + } + &.establishing { + background: rgb(var(--tone-blue-050)); + color: rgb(var(--tone-blue-500)); + } + &.failing { + background: rgb(var(--tone-red-050)); + color: rgb(var(--tone-red-500)); + } + &.deleting { + background: rgb(var(--tone-yellow-050)); + color: rgb(var(--tone-yellow-800)); + } + &.terminated { + background: rgb(var(--tone-gray-150)); + color: rgb(var(--tone-gray-800)); + } + &.undefined { + background: rgb(var(--tone-gray-150)); + color: rgb(var(--tone-gray-800)); + } + + .peerings-badge__text { + font-weight: 500; + font-size: 13px; + } +} diff --git a/ui/packages/consul-ui/app/components/peerings/search/index.hbs b/ui/packages/consul-ui/app/components/peerings/search/index.hbs new file mode 100644 index 000000000..31326928b --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/search/index.hbs @@ -0,0 +1,28 @@ + diff --git a/ui/packages/consul-ui/app/components/peerings/search/index.scss b/ui/packages/consul-ui/app/components/peerings/search/index.scss new file mode 100644 index 000000000..b5dc270fc --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/search/index.scss @@ -0,0 +1,36 @@ +.peerings-search { + display: flex; + padding: 4px 8px; + background: rgb(var(--gray-010)); + + .peerings-search__input { + position: relative; + border-width: 1px; + border-radius: 0.125rem; + } + + .peerings-search__input__label { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + width: 32px; + height: 100%; + } + + .peerings-search__input__input { + padding: 8px 32px; + border-radius: 2px; + border: 1px solid rgb(var(--gray-300)); + } + + .peerings-search__input__clear-button { + position: absolute; + right: 4px; + top: 0px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/ui/packages/consul-ui/app/components/peerings/service-count/index.hbs b/ui/packages/consul-ui/app/components/peerings/service-count/index.hbs new file mode 100644 index 000000000..1c81bfe21 --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/service-count/index.hbs @@ -0,0 +1,5 @@ +{{#if this.count}} +
    + {{this.text}} +
    +{{/if}} diff --git a/ui/packages/consul-ui/app/components/peerings/service-count/index.js b/ui/packages/consul-ui/app/components/peerings/service-count/index.js new file mode 100644 index 000000000..4b455cb86 --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/service-count/index.js @@ -0,0 +1,29 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +export default class PeeringsServiceCount extends Component { + @service intl; + + get count() { + const { peering, kind } = this.args; + + return peering[`${kind.capitalize()}ServiceCount`]; + } + + get text() { + const { kind } = this.args; + const { intl, count } = this; + + return intl.t(`routes.dc.peers.index.detail.${kind}.count`, { count }); + } + + get tooltipText() { + const { + kind, + peering: { name }, + } = this.args; + const { intl } = this; + + return intl.t(`routes.dc.peers.index.detail.${kind}.tooltip`, { name }); + } +} diff --git a/ui/packages/consul-ui/app/components/topology-metrics/index.hbs b/ui/packages/consul-ui/app/components/topology-metrics/index.hbs index 79b112ac4..23dcce62c 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/index.hbs +++ b/ui/packages/consul-ui/app/components/topology-metrics/index.hbs @@ -92,7 +92,7 @@ {{#if (gt this.upstreams.length 0)}}
    - {{#each-in (group-by "Datacenter" this.upstreams) as |dc upstreams|}} + {{#each-in (group-by "PeerOrDatacenter" this.upstreams) as |dc upstreams|}}
    { + u.PeerOrDatacenter = u.PeerName || u.Datacenter; + }); const items = [...upstreams]; const defaultACLPolicy = get(this.args.dc, 'DefaultACLPolicy'); const wildcardIntention = get(this.args.topology, 'wildcardIntention'); @@ -108,18 +111,21 @@ export default class TopologyMetrics extends Component { items.push({ Name: 'Upstreams unknown.', Datacenter: '', + PeerOrDatacenter: '', Namespace: '', }); } else if (defaultACLPolicy === 'allow' || wildcardIntention) { items.push({ Name: '* (All Services)', Datacenter: '', + PeerOrDatacenter: '', Namespace: '', }); } else if (upstreams.length === 0) { items.push({ Name: 'No upstreams.', Datacenter: '', + PeerOrDatacenter: '', Namespace: '', }); } diff --git a/ui/packages/consul-ui/app/components/watcher/index.hbs b/ui/packages/consul-ui/app/components/watcher/index.hbs new file mode 100644 index 000000000..f8ec72bb1 --- /dev/null +++ b/ui/packages/consul-ui/app/components/watcher/index.hbs @@ -0,0 +1,6 @@ +{{yield (hash + fns=(hash + start=this.start + stop=this.stop + ) +)}} diff --git a/ui/packages/consul-ui/app/components/watcher/index.js b/ui/packages/consul-ui/app/components/watcher/index.js new file mode 100644 index 000000000..bcb8a2d48 --- /dev/null +++ b/ui/packages/consul-ui/app/components/watcher/index.js @@ -0,0 +1,61 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { later, cancel as _cancel } from '@ember/runloop'; +import { inject as service } from '@ember/service'; + +const DEFAULT_TIMEOUT = 10000; +const TESTING_TIMEOUT = 300; + +export default class Watcher extends Component { + @service env; + + @tracked _isPolling = false; + @tracked cancel = null; + + get timeout() { + if (this.isTesting) { + return TESTING_TIMEOUT; + } else { + return this.args.timeout || DEFAULT_TIMEOUT; + } + } + + get isTesting() { + return this.env.var('environment') === 'testing'; + } + + get isPolling() { + const { isTesting, _isPolling: isPolling } = this; + + return !isTesting && isPolling; + } + + @action start() { + this._isPolling = true; + + this.watchTask(); + } + + @action stop() { + this._isPolling = false; + + _cancel(this.cancel); + } + + watchTask() { + const cancel = later( + this, + () => { + this.args.watch?.(); + + if (this.isPolling) { + this.watchTask(); + } + }, + this.timeout + ); + + this.cancel = cancel; + } +} diff --git a/ui/packages/consul-ui/app/controllers/dc/peers/index.js b/ui/packages/consul-ui/app/controllers/dc/peers/index.js new file mode 100644 index 000000000..8ea1ae96d --- /dev/null +++ b/ui/packages/consul-ui/app/controllers/dc/peers/index.js @@ -0,0 +1,29 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +export default class PeersController extends Controller { + queryParams = ['filter']; + + @tracked filter = ''; + + get peers() { + return this.model.peers; + } + + get filteredPeers() { + const { peers, filter } = this; + + if (filter) { + const filterRegex = new RegExp(`${filter}`, 'gi'); + + return peers.filter(peer => peer.Name.match(filterRegex)); + } + + return peers; + } + + @action handleSearchChanged(newSearchTerm) { + this.filter = newSearchTerm; + } +} diff --git a/ui/packages/consul-ui/app/helpers/feature-flag.js b/ui/packages/consul-ui/app/helpers/feature-flag.js new file mode 100644 index 000000000..bf5c27318 --- /dev/null +++ b/ui/packages/consul-ui/app/helpers/feature-flag.js @@ -0,0 +1,10 @@ +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; + +export default class extends Helper { + @service features; + + compute([feature]) { + return this.features.isEnabled(feature); + } +} diff --git a/ui/packages/consul-ui/app/locations/fsm-with-optional.js b/ui/packages/consul-ui/app/locations/fsm-with-optional.js index 1e4feb839..5b43868e9 100644 --- a/ui/packages/consul-ui/app/locations/fsm-with-optional.js +++ b/ui/packages/consul-ui/app/locations/fsm-with-optional.js @@ -8,6 +8,8 @@ if (env('CONSUL_NSPACES_ENABLED')) { OPTIONAL.nspace = /^~([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/; } +OPTIONAL.peer = /^:([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/; + const trailingSlashRe = /\/$/; // see below re: ember double slashes @@ -165,7 +167,7 @@ export default class FSMWithOptionalLocation { optionalParams() { let optional = this.optional || {}; - return ['partition', 'nspace'].reduce((prev, item) => { + return ['partition', 'nspace', 'peer'].reduce((prev, item) => { let value = ''; if (typeof optional[item] !== 'undefined') { value = optional[item].match; @@ -196,6 +198,10 @@ export default class FSMWithOptionalLocation { if (typeof hash.partition !== 'undefined') { hash.partition = `_${hash.partition}`; } + if (typeof hash.peer !== 'undefined') { + hash.peer = `:${hash.peer}`; + } + if (typeof this.router === 'undefined') { this.router = this.container.lookup('router:main'); } diff --git a/ui/packages/consul-ui/app/models/intention.js b/ui/packages/consul-ui/app/models/intention.js index 4f19696d2..b074425ea 100644 --- a/ui/packages/consul-ui/app/models/intention.js +++ b/ui/packages/consul-ui/app/models/intention.js @@ -13,6 +13,7 @@ export default class Intention extends Model { @attr('string') Datacenter; @attr('string') Description; + @attr('string') SourcePeer; @attr('string', { defaultValue: () => '*' }) SourceName; @attr('string', { defaultValue: () => '*' }) DestinationName; @attr('string', { defaultValue: () => 'default' }) SourceNS; diff --git a/ui/packages/consul-ui/app/models/node.js b/ui/packages/consul-ui/app/models/node.js index 1d5787ea7..27563fbd1 100644 --- a/ui/packages/consul-ui/app/models/node.js +++ b/ui/packages/consul-ui/app/models/node.js @@ -11,6 +11,7 @@ export default class Node extends Model { @attr('string') ID; @attr('string') Datacenter; + @attr('string') PeerName; @attr('string') Partition; @attr('string') Address; @attr('string') Node; diff --git a/ui/packages/consul-ui/app/models/peer.js b/ui/packages/consul-ui/app/models/peer.js new file mode 100644 index 000000000..221f26970 --- /dev/null +++ b/ui/packages/consul-ui/app/models/peer.js @@ -0,0 +1,10 @@ +import Model, { attr } from '@ember-data/model'; + +export default class Peer extends Model { + @attr('string') Name; + @attr('string') State; + @attr('string') CreateIndex; + @attr('string') ModifyIndex; + @attr('number') ImportedServiceCount; + @attr('number') ExportedServiceCount; +} diff --git a/ui/packages/consul-ui/app/models/service.js b/ui/packages/consul-ui/app/models/service.js index 31f0e2fc0..406b96b10 100644 --- a/ui/packages/consul-ui/app/models/service.js +++ b/ui/packages/consul-ui/app/models/service.js @@ -2,7 +2,7 @@ import Model, { attr } from '@ember-data/model'; import { computed } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import { fragment } from 'ember-data-model-fragments/attributes'; -import { nullValue } from 'consul-ui/decorators/replace'; +import replace, { nullValue } from 'consul-ui/decorators/replace'; export const PRIMARY_KEY = 'uid'; export const SLUG_KEY = 'Name'; @@ -36,6 +36,7 @@ export default class Service extends Model { @attr('string') Namespace; @attr('string') Partition; @attr('string') Kind; + @replace('', undefined) @attr('string') PeerName; @attr('number') ChecksPassing; @attr('number') ChecksCritical; @attr('number') ChecksWarning; diff --git a/ui/packages/consul-ui/app/modifiers/tooltip.js b/ui/packages/consul-ui/app/modifiers/tooltip.js index cefa8435e..336222278 100644 --- a/ui/packages/consul-ui/app/modifiers/tooltip.js +++ b/ui/packages/consul-ui/app/modifiers/tooltip.js @@ -10,64 +10,66 @@ import tippy, { followCursor } from 'tippy.js'; export default modifier(($element, [content], hash = {}) => { const options = hash.options || {}; - let $anchor = $element; + if (!options.hideTooltip) { + let $anchor = $element; - // make it easy to specify the modified element as the actual tooltip - if (typeof options.triggerTarget === 'string') { - const $el = $anchor; - switch (options.triggerTarget) { - case 'parentNode': - $anchor = $anchor.parentNode; - break; - default: - $anchor = $anchor.querySelectorAll(options.triggerTarget); + // make it easy to specify the modified element as the actual tooltip + if (typeof options.triggerTarget === 'string') { + const $el = $anchor; + switch (options.triggerTarget) { + case 'parentNode': + $anchor = $anchor.parentNode; + break; + default: + $anchor = $anchor.querySelectorAll(options.triggerTarget); + } + content = $anchor.cloneNode(true); + $el.remove(); + hash.options.triggerTarget = undefined; } - content = $anchor.cloneNode(true); - $el.remove(); - hash.options.triggerTarget = undefined; - } - // {{tooltip}} will just use the HTML content - if (typeof content === 'undefined') { - content = $anchor.innerHTML; - $anchor.innerHTML = ''; - } - let interval; - if (options.trigger === 'manual') { - // if we are manually triggering, a out delay means only show for the - // amount of time specified by the delay - const delay = options.delay || []; - if (typeof delay[1] !== 'undefined') { - hash.options.onShown = tooltip => { - clearInterval(interval); - interval = setTimeout(() => { - tooltip.hide(); - }, delay[1]); - }; + // {{tooltip}} will just use the HTML content + if (typeof content === 'undefined') { + content = $anchor.innerHTML; + $anchor.innerHTML = ''; } - } - let $trigger = $anchor; - let needsTabIndex = false; - if (!$trigger.hasAttribute('tabindex')) { - needsTabIndex = true; - $trigger.setAttribute('tabindex', '0'); - } - const tooltip = tippy($anchor, { - theme: 'tooltip', - triggerTarget: $trigger, - content: $anchor => content, - // showOnCreate: true, - // hideOnClick: false, - plugins: [typeof options.followCursor !== 'undefined' ? followCursor : undefined].filter(item => - Boolean(item) - ), - ...hash.options, - }); + let interval; + if (options.trigger === 'manual') { + // if we are manually triggering, a out delay means only show for the + // amount of time specified by the delay + const delay = options.delay || []; + if (typeof delay[1] !== 'undefined') { + hash.options.onShown = tooltip => { + clearInterval(interval); + interval = setTimeout(() => { + tooltip.hide(); + }, delay[1]); + }; + } + } + let $trigger = $anchor; + let needsTabIndex = false; + if (!$trigger.hasAttribute('tabindex')) { + needsTabIndex = true; + $trigger.setAttribute('tabindex', '0'); + } + const tooltip = tippy($anchor, { + theme: 'tooltip', + triggerTarget: $trigger, + content: $anchor => content, + // showOnCreate: true, + // hideOnClick: false, + plugins: [ + typeof options.followCursor !== 'undefined' ? followCursor : undefined, + ].filter(item => Boolean(item)), + ...hash.options, + }); - return () => { - if (needsTabIndex) { - $trigger.removeAttribute('tabindex'); - } - clearInterval(interval); - tooltip.destroy(); - }; + return () => { + if (needsTabIndex) { + $trigger.removeAttribute('tabindex'); + } + clearInterval(interval); + tooltip.destroy(); + }; + } }); diff --git a/ui/packages/consul-ui/app/routes/dc/peers.js b/ui/packages/consul-ui/app/routes/dc/peers.js new file mode 100644 index 000000000..195d1e2a1 --- /dev/null +++ b/ui/packages/consul-ui/app/routes/dc/peers.js @@ -0,0 +1,12 @@ +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; + +export default class PeersRoute extends Route { + @service features; + + beforeModel() { + if (!this.features.isEnabled('peering')) { + this.transitionTo('dc.services.index'); + } + } +} diff --git a/ui/packages/consul-ui/app/routes/dc/peers/index.js b/ui/packages/consul-ui/app/routes/dc/peers/index.js new file mode 100644 index 000000000..19eb41801 --- /dev/null +++ b/ui/packages/consul-ui/app/routes/dc/peers/index.js @@ -0,0 +1,17 @@ +import Route from '@ember/routing/route'; +import { action } from '@ember/object'; + +export default class PeersRoute extends Route { + model() { + return this.store.findAll('peer').then(peers => { + return { + peers, + loadPeers: this.loadPeers, + }; + }); + } + + @action loadPeers() { + return this.store.findAll('peer'); + } +} diff --git a/ui/packages/consul-ui/app/search/predicates/service.js b/ui/packages/consul-ui/app/search/predicates/service.js index 7080801a3..f46529769 100644 --- a/ui/packages/consul-ui/app/search/predicates/service.js +++ b/ui/packages/consul-ui/app/search/predicates/service.js @@ -1,4 +1,5 @@ export default { Name: item => item.Name, Tags: item => item.Tags || [], + PeerName: item => item.PeerName, }; diff --git a/ui/packages/consul-ui/app/serializers/intention.js b/ui/packages/consul-ui/app/serializers/intention.js index f2345c574..6fbd35ad9 100644 --- a/ui/packages/consul-ui/app/serializers/intention.js +++ b/ui/packages/consul-ui/app/serializers/intention.js @@ -21,8 +21,15 @@ export default class IntentionSerializer extends Serializer { item.Legacy = true; item.LegacyID = item.ID; } - item.ID = this - .uri`${item.SourcePartition}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}`; + + if (item.SourcePeer) { + item.ID = this + .uri`peer:${item.SourcePeer}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}`; + } else { + item.ID = this + .uri`${item.SourcePartition}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}`; + } + return item; } diff --git a/ui/packages/consul-ui/app/serializers/node.js b/ui/packages/consul-ui/app/serializers/node.js index 6e8f32a0e..5b74d4601 100644 --- a/ui/packages/consul-ui/app/serializers/node.js +++ b/ui/packages/consul-ui/app/serializers/node.js @@ -37,6 +37,9 @@ export default class NodeSerializer extends Serializer.extend(EmbeddedRecordsMix } checks[item.ServiceID].push(item); }); + if (item.PeerName === '') { + item.PeerName = undefined; + } serializer = this.store.serializerFor(relationship.type); item.Services = item.Services.map(service => serializer.transformHasManyResponseFromNode(item, service, checks) diff --git a/ui/packages/consul-ui/app/serializers/peer.js b/ui/packages/consul-ui/app/serializers/peer.js new file mode 100644 index 000000000..4d6740def --- /dev/null +++ b/ui/packages/consul-ui/app/serializers/peer.js @@ -0,0 +1,21 @@ +import JSONAPISerializer from '@ember-data/serializer/json-api'; + +export default class PeerSerializer extends JSONAPISerializer { + keyForAttribute(key) { + return key.capitalize(); + } + + normalizeFindAllResponse(store, primaryModelClass, payload, id, requestType) { + const data = payload.map(peering => { + return { + type: 'peer', + id: peering.ID, + attributes: { + ...peering, + }, + }; + }); + + return super.normalizeFindAllResponse(store, primaryModelClass, { data }, id, requestType); + } +} diff --git a/ui/packages/consul-ui/app/serializers/service.js b/ui/packages/consul-ui/app/serializers/service.js index ef535ba4c..3d1c92901 100644 --- a/ui/packages/consul-ui/app/serializers/service.js +++ b/ui/packages/consul-ui/app/serializers/service.js @@ -36,6 +36,7 @@ export default class ServiceSerializer extends Serializer { }); } }); + return cb(headers, body); }), query diff --git a/ui/packages/consul-ui/app/services/features.js b/ui/packages/consul-ui/app/services/features.js new file mode 100644 index 000000000..bbfc53748 --- /dev/null +++ b/ui/packages/consul-ui/app/services/features.js @@ -0,0 +1,13 @@ +import Service, { inject as service } from '@ember/service'; + +export default class FeatureService extends Service { + @service env; + + get features() { + return this.env.var('features'); + } + + isEnabled(featureName) { + return !!this.features?.[featureName]; + } +} diff --git a/ui/packages/consul-ui/app/services/repository/node.js b/ui/packages/consul-ui/app/services/repository/node.js index 11fdb3f8a..9b2cb0d7b 100644 --- a/ui/packages/consul-ui/app/services/repository/node.js +++ b/ui/packages/consul-ui/app/services/repository/node.js @@ -12,7 +12,7 @@ export default class NodeService extends RepositoryService { return super.findAllByDatacenter(...arguments); } - @dataSource('/:partition/:ns/:dc/node/:id') + @dataSource('/:partition/:ns/:dc/node/:id/:peer') async findBySlug() { return super.findBySlug(...arguments); } diff --git a/ui/packages/consul-ui/app/services/repository/service-instance.js b/ui/packages/consul-ui/app/services/repository/service-instance.js index 749262abc..93fd3f64d 100644 --- a/ui/packages/consul-ui/app/services/repository/service-instance.js +++ b/ui/packages/consul-ui/app/services/repository/service-instance.js @@ -13,14 +13,14 @@ export default class ServiceInstanceService extends RepositoryService { return super.shouldReconcile(...arguments) && item.Service.Service === params.id; } - @dataSource('/:partition/:ns/:dc/service-instances/for-service/:id') + @dataSource('/:partition/:ns/:dc/service-instances/for-service/:id/:peer') async findByService(params, configuration = {}) { if (typeof configuration.cursor !== 'undefined') { params.index = configuration.cursor; params.uri = configuration.uri; } return this.authorizeBySlug( - async (resources) => { + async resources => { const instances = await this.query(params); set(instances, 'firstObject.Service.Resources', resources); return instances; @@ -30,7 +30,7 @@ export default class ServiceInstanceService extends RepositoryService { ); } - @dataSource('/:partition/:ns/:dc/service-instance/:serviceId/:node/:id') + @dataSource('/:partition/:ns/:dc/service-instance/:serviceId/:node/:id/:peer') async findBySlug(params, configuration = {}) { return super.findBySlug(...arguments); } diff --git a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss index 064b8a4df..4ba418ad6 100644 --- a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss +++ b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss @@ -1,4 +1,3 @@ - // @import './alert-circle-fill/index.scss'; @import './alert-circle-outline/index.scss'; @import './alert-triangle/index.scss'; @@ -453,7 +452,7 @@ // @import './navigation/index.scss'; // @import './navigation-alt/index.scss'; // @import './network/index.scss'; -// @import './network-alt/index.scss'; +@import './network-alt/index.scss'; // @import './newspaper/index.scss'; // @import './node/index.scss'; // @import './nomad/index.scss'; diff --git a/ui/packages/consul-ui/app/styles/components.scss b/ui/packages/consul-ui/app/styles/components.scss index 875e3a142..9b4f3bba6 100644 --- a/ui/packages/consul-ui/app/styles/components.scss +++ b/ui/packages/consul-ui/app/styles/components.scss @@ -104,3 +104,8 @@ @import 'consul-ui/components/topology-metrics/series'; @import 'consul-ui/components/topology-metrics/stats'; @import 'consul-ui/components/topology-metrics/status'; +@import 'consul-ui/components/peerings/badge'; +@import 'consul-ui/components/peerings/search'; +@import 'consul-ui/components/consul/node/peer-info'; +@import 'consul-ui/components/consul/intention/list/table'; +@import 'consul-ui/components/consul/service/peer-info'; diff --git a/ui/packages/consul-ui/app/styles/routes.scss b/ui/packages/consul-ui/app/styles/routes.scss index 624ff4570..0493242c3 100644 --- a/ui/packages/consul-ui/app/styles/routes.scss +++ b/ui/packages/consul-ui/app/styles/routes.scss @@ -5,3 +5,4 @@ @import 'routes/dc/intentions/index'; @import 'routes/dc/overview/serverstatus'; @import 'routes/dc/overview/license'; +@import 'routes/dc/peers'; diff --git a/ui/packages/consul-ui/app/styles/routes/dc/peers/index.scss b/ui/packages/consul-ui/app/styles/routes/dc/peers/index.scss new file mode 100644 index 000000000..1380b3ab1 --- /dev/null +++ b/ui/packages/consul-ui/app/styles/routes/dc/peers/index.scss @@ -0,0 +1,6 @@ +.peers__list__peer-detail { + display: flex; + align-content: center; + overflow-x: scroll; + gap: 18px; +} diff --git a/ui/packages/consul-ui/app/templates/dc/nodes/show.hbs b/ui/packages/consul-ui/app/templates/dc/nodes/show.hbs index 236dc3e04..47459c399 100644 --- a/ui/packages/consul-ui/app/templates/dc/nodes/show.hbs +++ b/ui/packages/consul-ui/app/templates/dc/nodes/show.hbs @@ -10,12 +10,13 @@ as |route|> ) }} as |tomography|> @@ -94,7 +95,7 @@ as |item tomography|}}
      -
    1. All Nodes
    2. +
    3. All Nodes
    diff --git a/ui/packages/consul-ui/app/templates/dc/peers/index.hbs b/ui/packages/consul-ui/app/templates/dc/peers/index.hbs new file mode 100644 index 000000000..e2e039c8f --- /dev/null +++ b/ui/packages/consul-ui/app/templates/dc/peers/index.hbs @@ -0,0 +1,61 @@ + + + {{did-insert w.fns.start}} + {{will-destroy w.fns.stop}} + + + +

    + +

    +
    + + + + + {{#if this.filteredPeers.length}} + + +

    {{item.Name}}

    +
    + +
    + + + +
    +
    +
    + {{else}} + {{!-- TODO: do we need to check permissions here or will we receive an error automatically? --}} + + +

    Welcome to Peers

    +
    + +

    + Peering allows an admin partition in one datacenter to communicate with a partition in a different + datacenter. There don't seem to be any peers for this admin partition, or you may not have + peering:read permissions to + access this view. +

    +
    + + + + +
    + {{/if}} +
    + +
    +
    diff --git a/ui/packages/consul-ui/app/templates/dc/services/index.hbs b/ui/packages/consul-ui/app/templates/dc/services/index.hbs index 433211d0c..4d6ae6581 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/index.hbs @@ -3,7 +3,7 @@ as |route|> id=route.params.id node=route.params.node name=route.params.name + peer=route.params.peer ) }} as |loader|> @@ -108,7 +109,7 @@ as |item|}} {{! and this second request get the info for that instance and saves }} {{! it into the `proxy` variable }}
      -
    1. All Services
    2. +
    3. All Services
    4. Service ({{item.Service.Service}})
    @@ -151,6 +153,12 @@ as |item|}}
    Node Name
    {{item.Node.Node}}
    + {{#if item.Service.PeerName}} +
    +
    Peer Name
    +
    {{item.Service.PeerName}}
    +
    + {{/if}}
    {{#let (or item.Service.Address item.Node.Address) as |address|}} diff --git a/ui/packages/consul-ui/app/templates/dc/services/show.hbs b/ui/packages/consul-ui/app/templates/dc/services/show.hbs index 6f0e827bf..7fad2d700 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show.hbs @@ -2,12 +2,13 @@ @name={{routeName}} as |route|> @@ -135,7 +136,7 @@ as |items item dc|}}
      -
    1. All Services
    2. +
    3. All Services
    @@ -144,6 +145,7 @@ as |items item dc|}} + {{#if (not-eq item.Service.Kind 'mesh-gateway')}} diff --git a/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs b/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs index 7ee9df362..cde2e5ed9 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs @@ -46,12 +46,13 @@ as |sort filters items proxyMeta|}} {{/if}} {{#if proxyMeta.ServiceName}} { const legacy = ID.indexOf('%3A') === -1; const source = location.search.source.split('/'); const destination = location.search.destination.split('/'); + const sourceIsPeered = !!source[0].match(/^peer:/)?.length + + const sourcePeerString = `"SourcePeer": "${source[0].split(':')[1]}",` + const sourcePartitionString = `"SourcePartition": "${source[0]}",` return ` { "ID": "${legacy ? ID : ''}" @@ -12,7 +16,7 @@ ${ http.method !== "PUT" ? ` "DestinationName": "${destination[2]}", "SourceNS": "${source[1]}", "DestinationNS": "${destination[1]}", - "SourcePartition": "${source[0]}", + ${sourceIsPeered ? sourcePeerString : sourcePartitionString} "DestinationPartition": "${destination[0]}", "SourceType": "${fake.helpers.randomize(['consul', 'externaluri'])}", ${legacy ? ` diff --git a/ui/packages/consul-ui/mock-api/v1/health/service/_ b/ui/packages/consul-ui/mock-api/v1/health/service/_ index 3a33f731e..0b0daa392 100644 --- a/ui/packages/consul-ui/mock-api/v1/health/service/_ +++ b/ui/packages/consul-ui/mock-api/v1/health/service/_ @@ -43,11 +43,17 @@ "Datacenter":"dc1", "TaggedAddresses":{"lan":"${ip}","wan":"${ip}"}, "Meta":{"${service}-network-segment":""}, +${typeof location.search.peer !== 'undefined' ? ` + "PeerName": "${location.search.peer}", +` : ``} "CreateIndex":5, "ModifyIndex":6 }, "Service":{ "ID": "${ i === 0 ? id : fake.helpers.randomize([service, service + '-ID'])}", +${typeof location.search.peer !== 'undefined' ? ` + "PeerName": "${location.search.peer}", +` : ``} "Service":"${service}", ${typeof location.search.ns !== 'undefined' ? ` "Namespace": "${location.search.ns}", diff --git a/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ b/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ index a89e38293..200e066a0 100644 --- a/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ +++ b/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ @@ -19,9 +19,12 @@ ${[1].map(() => { } ); + +const peerNameString = location.search.peer ? `"PeerName": "${location.search.peer}",`: '' return ` { "ID":"${node = location.pathname.get(4)}", + ${peerNameString} "Node":"${node}", "Address":"${ip = fake.internet.ip()}", "TaggedAddresses":{"lan":"${ip}","wan":"${ip}"}, diff --git a/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes b/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes index a867b6db0..30557b643 100644 --- a/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes +++ b/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes @@ -12,10 +12,12 @@ ).map( function(item, i) { + const peerNameString = i === 0 ? '"PeerName": "billing",' : '"PeerName": "",' return ` { "ID":"${fake.random.uuid()}", "Node":"node-${i}", + ${location.search["with-peers"] ? peerNameString : ''} "Address":"${fake.internet.ip()}", "TaggedAddresses":{ "lan":"${fake.internet.ip()}", diff --git a/ui/packages/consul-ui/mock-api/v1/internal/ui/service-topology/_ b/ui/packages/consul-ui/mock-api/v1/internal/ui/service-topology/_ index 422288cca..f2bbb523f 100644 --- a/ui/packages/consul-ui/mock-api/v1/internal/ui/service-topology/_ +++ b/ui/packages/consul-ui/mock-api/v1/internal/ui/service-topology/_ @@ -64,8 +64,10 @@ ${ ${ upstreams.map((item, i) => { const hasPerms = fake.random.boolean(); + const isPeered = fake.random.boolean(); // if hasPerms is true allowed is always false as some restrictions apply const allowed = hasPerms ? false : fake.random.boolean(); + const peerString = isPeered ? `"PeerName": "${fake.random.word()}",` : ''; return ` { ${(Math.random(1) > 0.3) ? ` @@ -79,6 +81,7 @@ ${(Math.random(1) > 0.3) ? ` "ChecksWarning":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "ChecksCritical":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "Source": "${fake.helpers.randomize(['routing-config', 'proxy-registration', 'default-allow', 'wildcard-intention'])}", + ${peerString} "TransparentProxy": ${fake.random.boolean()}, "Intention": { "Allowed": ${allowed}, diff --git a/ui/packages/consul-ui/mock-api/v1/internal/ui/services b/ui/packages/consul-ui/mock-api/v1/internal/ui/services index 9fb437fbc..a9d0c98cc 100644 --- a/ui/packages/consul-ui/mock-api/v1/internal/ui/services +++ b/ui/packages/consul-ui/mock-api/v1/internal/ui/services @@ -19,9 +19,11 @@ ${[0].map( function(item, i) { let kind; + let peerName; switch(i) { case 0: kind = ''; + peerName = 'billing' break; case 1: kind = 'connect-proxy'; @@ -40,6 +42,8 @@ ${[0].map( } else { name = `service-${i}${ kind !== '' ? `-${kind.replace('connect-', '')}` : '' }`; } + + const peerNameString = `"PeerName": "${peerName || ''}",` return ` { "Name":"${name}", @@ -49,6 +53,7 @@ ${typeof location.search.ns !== 'undefined' ? ` ${typeof location.search.partition !== 'undefined' ? ` "Partition": "${fake.helpers.randomize([env('CONSUL_PARTITION_EXPORTER', location.search.partition), location.search.partition])}", ` : ``} + ${location.search['with-peers'] ? peerNameString : ''} "Tags": [ ${ range( diff --git a/ui/packages/consul-ui/mock-api/v1/peerings b/ui/packages/consul-ui/mock-api/v1/peerings new file mode 100644 index 000000000..1c88a98d8 --- /dev/null +++ b/ui/packages/consul-ui/mock-api/v1/peerings @@ -0,0 +1,65 @@ +[ + { + "ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b94", + "Name": "web", + "State": "ACTIVE", + "ImportedServiceCount": 10, + "ExportedServiceCount": 3, + "CreateIndex": 18, + "ModifyIndex": 18 + }, + { + "ID": "a25cdcc4-9e09-5276-bcd7-e2e4743ca687", + "Name": "billing", + "State": "PENDING", + "ImportedServiceCount": 5, + "ExportedServiceCount": 2, + "CreateIndex": 16, + "ModifyIndex": 16 + }, + { + "ID": "a25cdcc4-9e09-5276-bcd7-e2e4743ca688", + "Name": "peer-1", + "State": "ESTABLISHING", + "ImportedServiceCount": 2, + "ExportedServiceCount": 4, + "CreateIndex": 16, + "ModifyIndex": 16 + }, + { + "ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b95", + "Name": "db", + "State": "FAILING", + "ImportedServiceCount": 4, + "ExportedServiceCount": 3, + "CreateIndex": 19, + "ModifyIndex": 19 + }, + { + "ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b98", + "Name": "legacy deleted", + "State": "DELETING", + "ImportedServiceCount": 2, + "ExportedServiceCount": 4, + "CreateIndex": 20, + "ModifyIndex": 20 + }, + { + "ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b96", + "Name": "legacy", + "State": "TERMINATED", + "ImportedServiceCount": 0, + "ExportedServiceCount": 0, + "CreateIndex": 20, + "ModifyIndex": 20 + }, + { + "ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b97", + "Name": "legacy undefined", + "ImportedServiceCount": 0, + "ExportedServiceCount": 0, + "State": "UNDEFINED", + "CreateIndex": 20, + "ModifyIndex": 20 + } +] diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json index e40b6fe28..3870e0e9a 100644 --- a/ui/packages/consul-ui/package.json +++ b/ui/packages/consul-ui/package.json @@ -123,6 +123,7 @@ "ember-in-viewport": "^3.8.1", "ember-inflector": "^4.0.1", "ember-intl": "^5.5.1", + "ember-keyboard": "^7.0.1", "ember-load-initializers": "^2.1.1", "ember-math-helpers": "^2.4.0", "ember-maybe-import-regenerator": "^0.1.6", diff --git a/ui/packages/consul-ui/tests/acceptance/dc/intentions/navigation.feature b/ui/packages/consul-ui/tests/acceptance/dc/intentions/navigation.feature index 4b1bd2318..8c749634d 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/intentions/navigation.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/intentions/navigation.feature @@ -27,7 +27,7 @@ Feature: dc / intentions / navigation ID: 755b72bd-f5ab-4c92-90cc-bed0e7d8e9f0 --- When I click intention on the intentionList.intentions component - Then a GET request was made to "/v1/internal/ui/services?dc=dc-1&ns=*" + Then a GET request was made to "/v1/internal/ui/services?dc=dc-1&with-peers=true&ns=*" And I click "[data-test-back]" Then the url should be /dc-1/intentions Scenario: Clicking the create button and back again diff --git a/ui/packages/consul-ui/tests/acceptance/dc/nodes/index.feature b/ui/packages/consul-ui/tests/acceptance/dc/nodes/index.feature index 8bcd61f2e..8a3a52d1d 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/nodes/index.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/nodes/index.feature @@ -45,6 +45,7 @@ Feature: dc / nodes / index --- Then the url should be /dc-1/nodes And the title should be "Nodes - Consul" + And a GET request was made to "/v1/internal/ui/nodes?dc=dc-1&with-peers=true" Then I see 3 node models Scenario: Seeing the leader in node listing Given 3 node models from yaml diff --git a/ui/packages/consul-ui/tests/acceptance/dc/services/list.feature b/ui/packages/consul-ui/tests/acceptance/dc/services/list.feature index 3fb025137..3052fcf97 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/services/list.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/services/list.feature @@ -1,20 +1,38 @@ -@setupApplicationTest -Feature: dc / services / list - Scenario: Listing service - Given 1 datacenter model with the value "dc-1" - And 3 service models from yaml - --- - - Name: Service-0 - Kind: ~ - - Name: Service-1 - Kind: ~ - - Name: Service-2 - Kind: ~ - --- - When I visit the services page for yaml - --- - dc: dc-1 - --- - Then the url should be /dc-1/services - - Then I see 3 service models \ No newline at end of file +@setupApplicationTest +Feature: dc / services / list + Scenario: Listing service + Given 1 datacenter model with the value "dc-1" + And 3 service models from yaml + --- + - Name: Service-0 + Kind: ~ + - Name: Service-1 + Kind: ~ + - Name: Service-2 + Kind: ~ + --- + When I visit the services page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/services + And a GET request was made to "/v1/internal/ui/services?dc=dc-1&with-peers=true" + + Then I see 3 service models + + + Scenario: Listing peered service + Given 1 datacenter model with the value "dc-1" + And 1 service models from yaml + --- + - Name: Service-0 + Kind: ~ + PeerName: billing-app + --- + When I visit the services page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/services + + Then I see 1 service model with the peer "billing-app" diff --git a/ui/packages/consul-ui/tests/acceptance/dc/services/navigation.feature b/ui/packages/consul-ui/tests/acceptance/dc/services/navigation.feature index 3944655b2..356370f2a 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/services/navigation.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/services/navigation.feature @@ -14,3 +14,15 @@ Feature: dc / services / navigation And I click "[data-test-back]" Then the url should be /dc-1/services + Scenario: Clicking a peered service in the listing and back again + Given 1 datacenter model with the value "dc-1" + And 1 service model + When I visit the services page for yaml + --- + dc: dc-1 + --- + When I click service on the services + Then the url should match /:billing/dc-1/services/service-0 + And I click "[data-test-back]" + Then the url should be /dc-1/services + diff --git a/ui/packages/consul-ui/tests/acceptance/dc/services/show-with-slashes.feature b/ui/packages/consul-ui/tests/acceptance/dc/services/show-with-slashes.feature index 3cfde519e..88d8c111c 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/services/show-with-slashes.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/services/show-with-slashes.feature @@ -17,5 +17,5 @@ Feature: dc / services / show-with-slashes: Show Service that has slashes in its Then the url should be /dc1/services Then I see 1 service model And I click service on the services - Then the url should be /dc1/services/hashicorp%2Fservice%2Fservice-0/topology + Then the url should be /:billing/dc1/services/hashicorp%2Fservice%2Fservice-0/topology diff --git a/ui/packages/consul-ui/tests/acceptance/dc/services/show/intentions/index.feature b/ui/packages/consul-ui/tests/acceptance/dc/services/show/intentions/index.feature index 53c5fe54b..05521e23a 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/services/show/intentions/index.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/services/show/intentions/index.feature @@ -41,7 +41,7 @@ Feature: dc / services / show / intentions / index: Intentions per service Scenario: I can see intentions And I see 3 intention models on the intentionList component And I click intention on the intentionList.intentions component - Then the url should be /dc1/services/service-0/intentions/default:default:name:default:default:destination + Then the url should be /dc1/services/service-0/intentions/peer:billing:default:name:default:default:destination Scenario: I can delete intentions And I click actions on the intentionList.intentions component And I click delete on the intentionList.intentions component diff --git a/ui/packages/consul-ui/tests/acceptance/dc/services/show/navigation.feature b/ui/packages/consul-ui/tests/acceptance/dc/services/show/navigation.feature new file mode 100644 index 000000000..75212be25 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/dc/services/show/navigation.feature @@ -0,0 +1,7 @@ +@setupApplicationTest +Feature: dc / services / show / navigation + Scenario: Accessing peered service directly + Given 1 datacenter model with the value "dc-1" + And 1 service models + When I visit the service page with the url /:billing/dc-1/services/service-0 + Then I see peer like "billing" diff --git a/ui/packages/consul-ui/tests/acceptance/page-navigation.feature b/ui/packages/consul-ui/tests/acceptance/page-navigation.feature index 73f57caad..62d259c76 100644 --- a/ui/packages/consul-ui/tests/acceptance/page-navigation.feature +++ b/ui/packages/consul-ui/tests/acceptance/page-navigation.feature @@ -20,14 +20,14 @@ Feature: page-navigation Then the url should be [URL] Then a GET request was made to "[Endpoint]" Where: - ------------------------------------------------------------------------------------- - | Link | URL | Endpoint | - | nodes | /dc1/nodes | /v1/internal/ui/nodes?dc=dc1&ns=@namespace | + --------------------------------------------------------------------------------------------------- + | Link | URL | Endpoint | + | nodes | /dc1/nodes | /v1/internal/ui/nodes?dc=dc1&with-peers=true&ns=@namespace | # FIXME - # | kvs | /dc1/kv | /v1/kv/?keys&dc=dc1&separator=%2F&ns=@namespace | - | tokens | /dc1/acls/tokens | /v1/acl/tokens?dc=dc1&ns=@namespace | - # | settings | /settings | /v1/catalog/datacenters | - ------------------------------------------------------------------------------------- + # | kvs | /dc1/kv | /v1/kv/?keys&dc=dc1&separator=%2F&ns=@namespace | + | tokens | /dc1/acls/tokens | /v1/acl/tokens?dc=dc1&ns=@namespace | + # | settings | /settings | /v1/catalog/datacenters | + --------------------------------------------------------------------------------------------------- # FIXME @ignore Scenario: Clicking a [Item] in the [Model] listing and back again diff --git a/ui/packages/consul-ui/tests/acceptance/steps/dc/services/show/navigation-steps.js b/ui/packages/consul-ui/tests/acceptance/steps/dc/services/show/navigation-steps.js new file mode 100644 index 000000000..3231912b9 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/steps/dc/services/show/navigation-steps.js @@ -0,0 +1,10 @@ +import steps from '../../../steps'; + +// step definitions that are shared between features should be moved to the +// tests/acceptance/steps/steps.js file + +export default function(assert) { + return steps(assert).then('I should find a file', function() { + assert.ok(true, this.step); + }); +} diff --git a/ui/packages/consul-ui/tests/acceptance/token-header.feature b/ui/packages/consul-ui/tests/acceptance/token-header.feature index 4fd73cd62..948611441 100644 --- a/ui/packages/consul-ui/tests/acceptance/token-header.feature +++ b/ui/packages/consul-ui/tests/acceptance/token-header.feature @@ -11,7 +11,7 @@ Feature: token-header dc: dc1 --- Then the url should be /dc1/services - And a GET request was made to "/v1/internal/ui/services?dc=dc1&ns=@namespace" from yaml + And a GET request was made to "/v1/internal/ui/services?dc=dc1&with-peers=true&ns=@namespace" from yaml --- headers: X-Consul-Token: '' @@ -35,7 +35,7 @@ Feature: token-header dc: dc1 --- Then the url should be /dc1/services - And a GET request was made to "/v1/internal/ui/services?dc=dc1&ns=@namespace" from yaml + And a GET request was made to "/v1/internal/ui/services?dc=dc1&with-peers=true&ns=@namespace" from yaml --- headers: X-Consul-Token: [Token] diff --git a/ui/packages/consul-ui/tests/integration/adapters/node-test.js b/ui/packages/consul-ui/tests/integration/adapters/node-test.js index 32f1e8eac..61b92be49 100644 --- a/ui/packages/consul-ui/tests/integration/adapters/node-test.js +++ b/ui/packages/consul-ui/tests/integration/adapters/node-test.js @@ -16,7 +16,7 @@ module('Integration | Adapter | node', function(hooks) { const request = client.requestParams.bind(client); const expected = `GET /v1/internal/ui/nodes?dc=${dc}${ shouldHaveNspace(nspace) ? `&ns=${nspace}` : `` - }`; + }&with-peers=true`; const actual = adapter.requestForQuery(request, { dc: dc, ns: nspace, diff --git a/ui/packages/consul-ui/tests/integration/adapters/service-test.js b/ui/packages/consul-ui/tests/integration/adapters/service-test.js index adf46258d..9c6390f53 100644 --- a/ui/packages/consul-ui/tests/integration/adapters/service-test.js +++ b/ui/packages/consul-ui/tests/integration/adapters/service-test.js @@ -16,7 +16,7 @@ module('Integration | Adapter | service', function(hooks) { const request = client.requestParams.bind(client); const expected = `GET /v1/internal/ui/services?dc=${dc}${ shouldHaveNspace(nspace) ? `&ns=${nspace}` : `` - }`; + }&with-peers=true`; let actual = adapter.requestForQuery(request, { dc: dc, ns: nspace, diff --git a/ui/packages/consul-ui/tests/integration/serializers/intention-test.js b/ui/packages/consul-ui/tests/integration/serializers/intention-test.js index 5e78a07cf..4602db047 100644 --- a/ui/packages/consul-ui/tests/integration/serializers/intention-test.js +++ b/ui/packages/consul-ui/tests/integration/serializers/intention-test.js @@ -19,16 +19,19 @@ module('Integration | Serializer | intention', function(hooks) { url: `/v1/connect/intentions?dc=${dc}`, }; return get(request.url).then(function(payload) { - const expected = payload.map(item => - Object.assign({}, item, { + const expected = payload.map(item => { + if (item.SourcePeer) { + delete item.SourcePeer; + } + return Object.assign({}, item, { Datacenter: dc, // TODO: default isn't required here, once we've // refactored out our Serializer this can go Namespace: nspace, Partition: partition, uid: `["${partition}","${nspace}","${dc}","${item.SourcePartition}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}"]`, - }) - ); + }); + }); const actual = serializer.respondForQuery( function(cb) { const headers = { diff --git a/ui/packages/consul-ui/tests/pages/dc/services/index.js b/ui/packages/consul-ui/tests/pages/dc/services/index.js index 64de68537..82ed32661 100644 --- a/ui/packages/consul-ui/tests/pages/dc/services/index.js +++ b/ui/packages/consul-ui/tests/pages/dc/services/index.js @@ -4,6 +4,7 @@ export default function(visitable, clickable, text, attribute, present, collecti service: clickable('a'), externalSource: attribute('data-test-external-source', '[data-test-external-source]'), kind: attribute('data-test-kind', '[data-test-kind]'), + peer: text('[data-test-bucket-item="peer"]'), mesh: present('[data-test-mesh]'), associatedServiceCount: present('[data-test-associated-service-count]'), }; diff --git a/ui/packages/consul-ui/tests/pages/dc/services/show.js b/ui/packages/consul-ui/tests/pages/dc/services/show.js index 8914eb9dd..f0426a18a 100644 --- a/ui/packages/consul-ui/tests/pages/dc/services/show.js +++ b/ui/packages/consul-ui/tests/pages/dc/services/show.js @@ -19,6 +19,7 @@ export default function( metricsAnchor: { href: attribute('href', '[data-test-metrics-anchor]'), }, + peer: text('[data-test-service-peer-info] [data-test-peer-name]'), tabs: tabs('tab', [ 'topology', 'instances', diff --git a/ui/packages/consul-ui/tests/steps/assertions/dom.js b/ui/packages/consul-ui/tests/steps/assertions/dom.js index 425add01d..3f08c53d1 100644 --- a/ui/packages/consul-ui/tests/steps/assertions/dom.js +++ b/ui/packages/consul-ui/tests/steps/assertions/dom.js @@ -68,6 +68,13 @@ export default function(scenario, assert, pauseUntil, find, currentURL, clipboar assert.strictEqual(actual, expected, `Expected settings to be ${expected} was ${actual}`); }); }) + .then('the url should match $url', function(url) { + const currentUrl = currentURL() || ''; + + const matches = !!currentUrl.match(url); + + assert.ok(matches, `Expected currentURL to match the provided regex: ${url}`); + }) .then('the url should be $url', function(url) { // TODO: nice! $url should be wrapped in "" if (url === "''") { diff --git a/ui/packages/consul-ui/tests/steps/interactions/visit.js b/ui/packages/consul-ui/tests/steps/interactions/visit.js index 54890318f..51324f23b 100644 --- a/ui/packages/consul-ui/tests/steps/interactions/visit.js +++ b/ui/packages/consul-ui/tests/steps/interactions/visit.js @@ -1,3 +1,5 @@ +import { visit } from '@ember/test-helpers'; + export default function(scenario, pages, set, reset) { scenario .when('I visit the $name page', function(name) { @@ -10,6 +12,11 @@ export default function(scenario, pages, set, reset) { [model]: id, }); }) + .when('I visit the $name page with the url $url', function(name, url) { + reset(); + set(pages[name]); + return visit(url); + }) .when( ['I visit the $name page for yaml\n$yaml', 'I visit the $name page for json\n$json'], function(name, data) { diff --git a/ui/packages/consul-ui/translations/common/en-us.yaml b/ui/packages/consul-ui/translations/common/en-us.yaml index 70a1e229d..5d10766df 100644 --- a/ui/packages/consul-ui/translations/common/en-us.yaml +++ b/ui/packages/consul-ui/translations/common/en-us.yaml @@ -46,6 +46,7 @@ consul: failuretolerance: Fault tolerance readreplica: Read replica redundancyzone: Redundancy zone + peername: Peer search: search: Search searchproperty: Search Across diff --git a/ui/packages/consul-ui/translations/routes/en-us.yaml b/ui/packages/consul-ui/translations/routes/en-us.yaml index d0d904521..d2f5ef0ad 100644 --- a/ui/packages/consul-ui/translations/routes/en-us.yaml +++ b/ui/packages/consul-ui/translations/routes/en-us.yaml @@ -105,6 +105,17 @@ dc:

    This node has a failing serf node check. The health statuses shown on this page are the statuses as they were known before the node became unreachable.

    + peers: + index: + detail: + imported: + count: | + {count} imported services + tooltip: The number of services imported from {name} + exported: + count: | + {count} exported services + tooltip: The number of services exported from {name} services: index: empty: diff --git a/ui/packages/consul-ui/vendor/consul-ui/routes.js b/ui/packages/consul-ui/vendor/consul-ui/routes.js index 9191b8726..426fccbf2 100644 --- a/ui/packages/consul-ui/vendor/consul-ui/routes.js +++ b/ui/packages/consul-ui/vendor/consul-ui/routes.js @@ -18,7 +18,7 @@ serverstatus: { _options: { path: '/server-status', - abilities: ['read servers'] + abilities: ['read servers'], }, }, cataloghealth: { @@ -30,7 +30,15 @@ license: { _options: { path: '/license', - abilities: ['read license'] + abilities: ['read license'], + }, + }, + }, + peers: { + _options: { path: '/peers' }, + index: { + _options: { + path: '/', }, }, }, @@ -46,7 +54,7 @@ kind: 'kind', searchproperty: { as: 'searchproperty', - empty: [['Name', 'Tags']], + empty: [['Name', 'Tags', 'PeerName']], }, search: { as: 'filter', @@ -56,7 +64,9 @@ }, }, show: { - _options: { path: '/:name' }, + _options: { + path: '/:name', + }, instances: { _options: { path: '/instances', diff --git a/ui/yarn.lock b/ui/yarn.lock index 39b3974a4..f7b7a6c2a 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2944,6 +2944,13 @@ ansi-styles@~1.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg= +ansi-to-html@^0.6.15: + version "0.6.15" + resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.15.tgz#ac6ad4798a00f6aa045535d7f6a9cb9294eebea7" + integrity sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ== + dependencies: + entities "^2.0.0" + ansi-to-html@^0.6.6: version "0.6.14" resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8" @@ -6647,6 +6654,27 @@ ember-cli-htmlbars@^6.0.0: strip-bom "^4.0.0" walk-sync "^2.2.0" +ember-cli-htmlbars@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-6.0.1.tgz#5487831d477e61682bc867fd138808269e5d2152" + integrity sha512-IDsl9uty+MXtMfp/BUTEc/Q36EmlHYj8ZdPekcoRa8hmdsigHnK4iokfaB7dJFktlf6luruei+imv7JrJrBQPQ== + dependencies: + "@ember/edition-utils" "^1.2.0" + babel-plugin-ember-template-compilation "^1.0.0" + babel-plugin-htmlbars-inline-precompile "^5.3.0" + broccoli-debug "^0.6.5" + broccoli-persistent-filter "^3.1.2" + broccoli-plugin "^4.0.3" + ember-cli-version-checker "^5.1.2" + fs-tree-diff "^2.0.1" + hash-for-dep "^1.5.1" + heimdalljs-logger "^0.1.10" + json-stable-stringify "^1.0.1" + semver "^7.3.4" + silent-error "^1.1.1" + strip-bom "^4.0.0" + walk-sync "^2.2.0" + ember-cli-inject-live-reload@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/ember-cli-inject-live-reload/-/ember-cli-inject-live-reload-2.0.2.tgz#95edb543b386239d35959e5ea9579f5382976ac7" @@ -6845,6 +6873,22 @@ ember-cli-typescript@^4.0.0, ember-cli-typescript@^4.1.0: stagehand "^1.0.0" walk-sync "^2.2.0" +ember-cli-typescript@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-5.1.0.tgz#460eb848564e29d64f2b36b2a75bbe98172b72a4" + integrity sha512-wEZfJPkjqFEZAxOYkiXsDrJ1HY75e/6FoGhQFg8oNFJeGYpIS/3W0tgyl1aRkSEEN1NRlWocDubJ4aZikT+RTA== + dependencies: + ansi-to-html "^0.6.15" + broccoli-stew "^3.0.0" + debug "^4.0.0" + execa "^4.0.0" + fs-extra "^9.0.1" + resolve "^1.5.0" + rsvp "^4.8.1" + semver "^7.3.2" + stagehand "^1.0.0" + walk-sync "^2.2.0" + ember-cli-uglify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ember-cli-uglify/-/ember-cli-uglify-3.0.0.tgz#8819665b2cc5fe70e3ba9fe7a94645209bc42fd6" @@ -7247,6 +7291,16 @@ ember-intl@^5.5.1: mkdirp "^1.0.4" silent-error "^1.1.1" +ember-keyboard@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-7.0.1.tgz#6cf336efd4ea6cb69ec93d20fb0b819bd7241a9d" + integrity sha512-MKK9/3yzn30ekmFAQO7z+okCQa7Z5wCSI5m7lR3EL2dMIeRd/9eeLhbQNCU00Slx+GjwsGyCEWPqIQmekFJxpQ== + dependencies: + ember-cli-babel "^7.26.6" + ember-cli-htmlbars "^6.0.1" + ember-modifier "^2.1.2 || ^3.0.0" + ember-modifier-manager-polyfill "^1.2.0" + ember-load-initializers@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-2.1.2.tgz#8a47a656c1f64f9b10cecdb4e22a9d52ad9c7efa" @@ -7305,6 +7359,17 @@ ember-modifier@^2.1.0, ember-modifier@^2.1.1: ember-destroyable-polyfill "^2.0.2" ember-modifier-manager-polyfill "^1.2.0" +"ember-modifier@^2.1.2 || ^3.0.0": + version "3.2.7" + resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b" + integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA== + dependencies: + ember-cli-babel "^7.26.6" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-string-utils "^1.1.0" + ember-cli-typescript "^5.0.0" + ember-compatibility-helpers "^1.2.5" + ember-named-blocks-polyfill@^0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/ember-named-blocks-polyfill/-/ember-named-blocks-polyfill-0.2.4.tgz#f5f30711ee89244927b55aae7fa9630edaadc974" diff --git a/version/version.go b/version/version.go index 8930b4822..edcdca85b 100644 --- a/version/version.go +++ b/version/version.go @@ -10,7 +10,7 @@ var ( // compiler. GitCommit string - // The main version number that is being run at the moment. + // The next version number that will be released. This will be updated after every release. // // Version must conform to the format expected by github.com/hashicorp/go-version // for tests to work. diff --git a/website/content/api-docs/health.mdx b/website/content/api-docs/health.mdx index fcda3cbca..898c8ffe4 100644 --- a/website/content/api-docs/health.mdx +++ b/website/content/api-docs/health.mdx @@ -235,7 +235,7 @@ The table below shows this endpoint's support for - `near` `(string: "")` - Specifies a node name to sort the node list in ascending order based on the estimated round trip time from that node. Passing `?near=_agent` uses the agent's node for the sort. - **Note** that using `near` will ignore + ~> **Note:** Using `near` will ignore [`use_streaming_backend`](/docs/agent/config/config-files#use_streaming_backend) and always use blocking queries, because the data required to sort the results is not available to the streaming backend. @@ -259,6 +259,8 @@ The table below shows this endpoint's support for - `filter` `(string: "")` - Specifies the expression used to filter the queries results prior to returning the data. +- `peer` `(string: "")` - Specifies the imported service's peer. Applies only to imported services. + - `ns` `(string: "")` - Specifies the namespace of the service. You can also [specify the namespace through other methods](#methods-to-specify-namespace). @@ -431,8 +433,8 @@ gateway](/docs/connect/gateways/ingress-gateway) for a service in a given datace Parameters and response format are the same as [`/health/service/:service`](/api-docs/health#list-nodes-for-service). -**Note** that unlike `/health/connect/:service` and `/health/service/:service` this -endpoint does not support the [streaming backend](/api-docs/features/blocking#streaming-backend). +~> **Note:** Unlike `/health/connect/:service` and `/health/service/:service` this +endpoint does not support the `peer` query parameter and the [streaming backend](/api-docs/features/blocking#streaming-backend). ## List Checks in State diff --git a/website/content/api-docs/peering.mdx b/website/content/api-docs/peering.mdx new file mode 100644 index 000000000..607ecf311 --- /dev/null +++ b/website/content/api-docs/peering.mdx @@ -0,0 +1,319 @@ +--- +layout: api +page_title: Cluster Peering - HTTP API +description: The /peering endpoints allow for managing cluster peerings. +--- + +# Cluster Peering - HTTP API + +~> **Cluster peering is currently in technical preview:** Functionality associated +with cluster peering is subject to change. You should never use the technical +preview release in secure environments or production scenarios. Features in +technical preview may have performance issues, scaling issues, and limited support. + +The functionality described here is available only in +[Consul](https://www.hashicorp.com/products/consul/) version 1.13.0 and later. + +## Generate a Peering Token + +This endpoint generates a peering token. + +| Method | Path | Produces | +| ------- | -------------------- | ------------------ | +| `POST` | `/peering/token` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api-docs/features/blocking), +[consistency modes](/api-docs/features/consistency), +[agent caching](/api-docs/features/caching), and +[required ACLs](/api#authentication). + +| Blocking Queries | Consistency Modes | Agent Caching | ACL Required | +| ---------------- | ----------------- | ------------- | ---------------- | +| `NO` | `none` | `none` | `none` | + +### JSON Request Body Schema + +- `PeerName` `(string: )` - The name assigned to the peer cluster. + The `PeerName` is used to reference the peer cluster in service discovery queries + and configuration entries such as `service-intentions`. This field must be a + valid DNS hostname label. + +- `Partition` `(string: "")` - The admin partition that the + peering token is generated from. Uses `default` when not specified. + +- `Datacenter` `(string: "")` - Specifies the datacenter where the peering token is generated. Defaults to the + agent's datacenter when not specified. + +- `Token` `(string: "")` - Specifies the ACL token to use in the request. Takes precedence + over the token specified in the `token` query parameter, `X-Consul-Token` request header, + and `CONSUL_HTTP_TOKEN` environment variable. + +- `Meta` `(map: )` - Specifies KV metadata to associate with + the peering. This parameter is not required and does not directly impact the cluster + peering process. + +### Sample Payload + +```json +{ + "PeerName": "cluster-02", + "Meta": { + "env": "production" + } +} +``` + +### Sample Request + +```shell-session +$ curl --request POST \ + --header "X-Consul-Token: 5cdcae6c-0cce-4210-86fe-5dff3b984a6e" \ + --data @payload.json \ + http://127.0.0.1:8500/v1/peering/token +``` + +### Sample Response + +```json +{ + "PeeringToken": "eyJDQSI6bnVsbCwiU2V..." +} +``` + +## Establish a Peering Connection + +This endpoint establishes a peering connection with a given peering token. + +| Method | Path | Produces | +| ------- | -------------------- | ------------------ | +| `POST` | `/peering/establish` | `application/json` | + +This endpoint returns no data. Success or failure is indicated by the status +code returned. + +The table below shows this endpoint's support for +[blocking queries](/api-docs/features/blocking), +[consistency modes](/api-docs/features/consistency), +[agent caching](/api-docs/features/caching), and +[required ACLs](/api#authentication). + +| Blocking Queries | Consistency Modes | Agent Caching | ACL Required | +| ---------------- | ----------------- | ------------- | ---------------- | +| `NO` | `none` | `none` | `none` | + +### JSON Request Body Schema + +- `PeerName` `(string: )` - The name assigned to the peer cluster. + The `PeerName` is used to reference the peer cluster in service discovery queries + and configuration entries such as `service-intentions`. This field must be a + valid DNS hostname label. + +- `Partition` `(string: "")` - The admin partition + that peers to the cluster that generated the peering token. Uses `default` + when not specified. + +- `PeeringToken` `(string: )` - The peering token fetched from the + peer cluster. + +- `Datacenter` `(string: "")` - Specifies the datacenter where the peering token is generated. Defaults to the + agent's datacenter when not specified. + +- `Token` `(string: "")` - Specifies the ACL token to use in the request. Takes precedence + over the token specified in the `token` query parameter, `X-Consul-Token` request header, + and `CONSUL_HTTP_TOKEN` environment variable. + +- `Meta` `(map: )` - Specifies KV metadata to associate with + the peering. This parameter is not required and does not directly impact the cluster + peering process. + +### Sample Payload + +```json +{ + "PeerName": "cluster-01", + "PeeringToken": "eyJDQSI6bnVsbCwiU2V...", + "Meta": { + "env": "production" + } +} +``` + +### Sample Request + +```shell-session +$ curl --request POST \ + --header "X-Consul-Token: 5cdcae6c-0cce-4210-86fe-5dff3b984a6e" \ + --data @payload.json \ + http://127.0.0.1:8500/v1/peering/establish +``` + +### Sample Response + +```json +{} +``` + +## Read a Peering Connection + +This endpoint returns information about a peering connection for the specified peer name. + +| Method | Path | Produces | +| ------ | ------------------ | ------------------ | +| `GET` | `/peering/:name` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api-docs/features/blocking), +[consistency modes](/api-docs/features/consistency), +[agent caching](/api-docs/features/caching), and +[required ACLs](/api#authentication). + +| Blocking Queries | Consistency Modes | Agent Caching | ACL Required | +| ---------------- | ----------------- | ------------- | ------------ | +| `NO` | `consistent` | `none` | `none` | + +### Path Parameters + +- `name` `(string: )` - Specifies the peering to read. + +### Query Parameters + +- `partition` `(string: "")` - Specifies the partition of the peering + to read. If not specified will default to `default`. + +### Sample Request + +```shell-session +$ curl --header "X-Consul-Token: b23b3cad-5ea1-4413-919e-c76884b9ad60" \ + http://127.0.0.1:8500/v1/peering/cluster-02 +``` + +### Sample Response + +```json +{ + "ID": "462c45e8-018e-f19d-85eb-1fc1bcc2ef12", + "Name": "cluster-02", + "State": "INITIAL", + "PeerID": "e83a315c-027e-bcb1-7c0c-a46650904a05", + "PeerServerName": "server.dc1.consul", + "PeerServerAddresses": [ + "10.0.0.1:8300" + ], + "CreateIndex": 89, + "ModifyIndex": 89 +} +``` + +## Delete a Peering Connection + +Call this endpoint to delete a peering connection. Consul deletes all data imported from the peer in the background. The peering connection is removed after all associated data has been deleted. +Operators can still read the peering connections while the data is being removed. A `DeletedAt` +field will be populated with the timestamp of when the peering was marked for deletion. + +| Method | Path | Produces | +| -------- | ------------------ | -------- | +| `DELETE` | `/peering/:name` | N/A | + +This endpoint returns no data. Success or failure is indicated by the status +code returned. + +The table below shows this endpoint's support for +[blocking queries](/api-docs/features/blocking), +[consistency modes](/api-docs/features/consistency), +[agent caching](/api-docs/features/caching), and +[required ACLs](/api#authentication). + +| Blocking Queries | Consistency Modes | Agent Caching | ACL Required | +| ---------------- | ----------------- | ------------- | ------------- | +| `NO` | `none` | `none` | `none` | + +### Path Parameters + +- `name` `(string: )` - Specifies the name of the peering to delete. +- `partition` `(string: "")` - Specifies the partition of the peering + to delete. Uses `default` when not specified. + +### Sample Request + +```shell-session +$ curl --request DELETE \ + --header "X-Consul-Token: b23b3cad-5ea1-4413-919e-c76884b9ad60" \ + http://127.0.0.1:8500/v1/peering/cluster-02 +``` + +### Sample Read Output After Deletion Prior to Removal + +```json +{ + "ID": "462c45e8-018e-f19d-85eb-1fc1bcc2ef12", + "Name": "cluster-02", + "State": "TERMINATED", + "PeerID": "e83a315c-027e-bcb1-7c0c-a46650904a05", + "PeerServerName": "server.dc1.consul", + "PeerServerAddresses": [ + "10.0.0.1:8300" + ], + "DeletedAt": "2022-12-14T23:00:00Z", + "CreateIndex": 89, + "ModifyIndex": 89 +} +``` + +## List all Peerings + +This endpoint lists all the peerings. + +@include 'http_api_results_filtered_by_acls.mdx' + +| Method | Path | Produces | +| ------ | ------------- | ------------------ | +| `GET` | `/peerings` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api-docs/features/blocking), +[consistency modes](/api-docs/features/consistency), +[agent caching](/api-docs/features/caching), and +[required ACLs](/api#authentication). + +| Blocking Queries | Consistency Modes | Agent Caching | ACL Required | +| ---------------- | ----------------- | ------------- | ------------- | +| `NO` | `consistent` | `none` | `none` | + +### Sample Request + +```shell-session +$ curl --header "X-Consul-Token: 0137db51-5895-4c25-b6cd-d9ed992f4a52" \ + http://127.0.0.1:8500/v1/peerings +``` + +### Sample Response + +```json +[ + { + "ID": "462c45e8-018e-f19d-85eb-1fc1bcc2ef12", + "Name": "cluster-02", + "State": "ACTIVE", + "Partition": "default", + "PeerID": "e83a315c-027e-bcb1-7c0c-a46650904a05", + "PeerServerName": "server.dc1.consul", + "PeerServerAddresses": [ + "10.0.0.1:8300" + ], + "CreateIndex": 89, + "ModifyIndex": 89 + }, + { + "ID": "1460ada9-26d2-f30d-3359-2968aa7dc47d", + "Name": "cluster-03", + "State": "INITIAL", + "Partition": "default", + "Meta": { + "env": "production" + }, + "CreateIndex": 109, + "ModifyIndex": 119 + }, +] +``` diff --git a/website/content/commands/connect/envoy.mdx b/website/content/commands/connect/envoy.mdx index d453a7cbf..88f7fccb7 100644 --- a/website/content/commands/connect/envoy.mdx +++ b/website/content/commands/connect/envoy.mdx @@ -96,6 +96,22 @@ proxy configuration needed. be scrapable at `0.0.0.0:20200/scrape-metrics`. Only applicable when `envoy_prometheus_bind_addr` is set in proxy config. +- `-prometheus-ca-file` - Path to a CA file for Envoy to use when serving TLS on + the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr` + is set in proxy config. + +- `-prometheus-ca-path` - Path to a directory of CA certificates for Envoy to use when + serving the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr` + is set in proxy config. + +- `-prometheus-cert-file` - Path to a certificate file for Envoy to use when serving + TLS on the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr` + is set in proxy config. + +- `-prometheus-key-file` - Path to a private key file for Envoy to use when serving + TLS on the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr` + is set in proxy config. + - `-- [pass-through options]` - Any options given after a double dash are passed directly through to the `envoy` invocation. See [Envoy's documentation](https://www.envoyproxy.io/docs) for more details. The command diff --git a/website/content/commands/monitor.mdx b/website/content/commands/monitor.mdx index 51899cdc5..58abbeede 100644 --- a/website/content/commands/monitor.mdx +++ b/website/content/commands/monitor.mdx @@ -32,6 +32,6 @@ Usage: `consul monitor [options]` - `-log-level` - The log level of the messages to show. By default this is "info". This log level can be more verbose than what the agent is configured to run at. Available log levels are "trace", "debug", "info", - "warn", and "err". + "warn", and "error". - `-log-json` - Toggles whether the messages are streamed in JSON format. By default this is false. diff --git a/website/content/commands/snapshot/agent.mdx b/website/content/commands/snapshot/agent.mdx index f70525389..136a0f065 100644 --- a/website/content/commands/snapshot/agent.mdx +++ b/website/content/commands/snapshot/agent.mdx @@ -229,7 +229,7 @@ if desired. provided, this will be disabled. Defaults to "72h". - `-log-level` - Controls verbosity of snapshot agent logs. Valid options are - "TRACE", "DEBUG", "INFO", "WARN", "ERR". Defaults to "INFO". + "trace", "debug", "info", "warn", "error". Defaults to "info". - `-service` - The service name to used when registering the agent with Consul. Registering helps monitor running agents and the leader registers an additional diff --git a/website/content/docs/agent/config/cli-flags.mdx b/website/content/docs/agent/config/cli-flags.mdx index b55f44129..caf3b6444 100644 --- a/website/content/docs/agent/config/cli-flags.mdx +++ b/website/content/docs/agent/config/cli-flags.mdx @@ -468,7 +468,7 @@ information. - `-log-level` ((#\_log_level)) - The level of logging to show after the Consul agent has started. This defaults to "info". The available log levels are - "trace", "debug", "info", "warn", and "err". You can always connect to an agent + "trace", "debug", "info", "warn", and "error". You can always connect to an agent via [`consul monitor`](/commands/monitor) and use any log level. Also, the log level can be changed during a config reload. diff --git a/website/content/docs/agent/telemetry.mdx b/website/content/docs/agent/telemetry.mdx index 01adb30a1..bc4f27aa4 100644 --- a/website/content/docs/agent/telemetry.mdx +++ b/website/content/docs/agent/telemetry.mdx @@ -349,199 +349,199 @@ populated free list structure. This is a full list of metrics emitted by Consul. -| Metric | Description | Unit | Type | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ------- | -| `consul.acl.blocked.{check,service}.deregistration` | Increments whenever a deregistration fails for an entity (check or service) is blocked by an ACL. | requests | counter | -| `consul.acl.blocked.{check,node,service}.registration` | Increments whenever a registration fails for an entity (check, node or service) is blocked by an ACL. | requests | counter | -| `consul.api.http` | Migrated from consul.http.. this samples how long it takes to service the given HTTP request for the given verb and path. Includes labels for `path` and `method`. `path` does not include details like service or key names, for these an underscore will be present as a placeholder (eg. path=`v1.kv._`) | ms | timer | -| `consul.client.rpc` | Increments whenever a Consul agent in client mode makes an RPC request to a Consul server. This gives a measure of how much a given agent is loading the Consul servers. Currently, this is only generated by agents in client mode, not Consul servers. | requests | counter | -| `consul.client.rpc.exceeded` | Increments whenever a Consul agent in client mode makes an RPC request to a Consul server gets rate limited by that agent's [`limits`](/docs/agent/config/config-files#limits) configuration. This gives an indication that there's an abusive application making too many requests on the agent, or that the rate limit needs to be increased. Currently, this only applies to agents in client mode, not Consul servers. | rejected requests | counter | -| `consul.client.rpc.failed` | Increments whenever a Consul agent in client mode makes an RPC request to a Consul server and fails. | requests | counter | -| `consul.client.api.catalog_register.` | Increments whenever a Consul agent receives a catalog register request. | requests | counter | -| `consul.client.api.success.catalog_register.` | Increments whenever a Consul agent successfully responds to a catalog register request. | requests | counter | -| `consul.client.rpc.error.catalog_register.` | Increments whenever a Consul agent receives an RPC error for a catalog register request. | errors | counter | -| `consul.client.api.catalog_deregister.` | Increments whenever a Consul agent receives a catalog deregister request. | requests | counter | -| `consul.client.api.success.catalog_deregister.` | Increments whenever a Consul agent successfully responds to a catalog deregister request. | requests | counter | -| `consul.client.rpc.error.catalog_deregister.` | Increments whenever a Consul agent receives an RPC error for a catalog deregister request. | errors | counter | -| `consul.client.api.catalog_datacenters.` | Increments whenever a Consul agent receives a request to list datacenters in the catalog. | requests | counter | -| `consul.client.api.success.catalog_datacenters.` | Increments whenever a Consul agent successfully responds to a request to list datacenters. | requests | counter | -| `consul.client.rpc.error.catalog_datacenters.` | Increments whenever a Consul agent receives an RPC error for a request to list datacenters. | errors | counter | -| `consul.client.api.catalog_nodes.` | Increments whenever a Consul agent receives a request to list nodes from the catalog. | requests | counter | -| `consul.client.api.success.catalog_nodes.` | Increments whenever a Consul agent successfully responds to a request to list nodes. | requests | counter | -| `consul.client.rpc.error.catalog_nodes.` | Increments whenever a Consul agent receives an RPC error for a request to list nodes. | errors | counter | -| `consul.client.api.catalog_services.` | Increments whenever a Consul agent receives a request to list services from the catalog. | requests | counter | -| `consul.client.api.success.catalog_services.` | Increments whenever a Consul agent successfully responds to a request to list services. | requests | counter | -| `consul.client.rpc.error.catalog_services.` | Increments whenever a Consul agent receives an RPC error for a request to list services. | errors | counter | -| `consul.client.api.catalog_service_nodes.` | Increments whenever a Consul agent receives a request to list nodes offering a service. | requests | counter | -| `consul.client.api.success.catalog_service_nodes.` | Increments whenever a Consul agent successfully responds to a request to list nodes offering a service. | requests | counter | -| `consul.client.api.error.catalog_service_nodes.` | Increments whenever a Consul agent receives an RPC error for request to list nodes offering a service. | requests | counter | -| `consul.client.rpc.error.catalog_service_nodes.` | Increments whenever a Consul agent receives an RPC error for a request to list nodes offering a service.   | errors | counter | -| `consul.client.api.catalog_node_services.` | Increments whenever a Consul agent receives a request to list services registered in a node.   | requests | counter | -| `consul.client.api.success.catalog_node_services.` | Increments whenever a Consul agent successfully responds to a request to list services in a node.   | requests | counter | -| `consul.client.rpc.error.catalog_node_services.` | Increments whenever a Consul agent receives an RPC error for a request to list services in a node.   | errors | counter | -| `consul.client.api.catalog_node_service_list` | Increments whenever a Consul agent receives a request to list a node's registered services. | requests | counter | -| `consul.client.rpc.error.catalog_node_service_list` | Increments whenever a Consul agent receives an RPC error for request to list a node's registered services. | errors | counter | -| `consul.client.api.success.catalog_node_service_list` | Increments whenever a Consul agent successfully responds to a request to list a node's registered services. | requests | counter | -| `consul.client.api.catalog_gateway_services.` | Increments whenever a Consul agent receives a request to list services associated with a gateway. | requests | counter | -| `consul.client.api.success.catalog_gateway_services.` | Increments whenever a Consul agent successfully responds to a request to list services associated with a gateway. | requests | counter | -| `consul.client.rpc.error.catalog_gateway_services.` | Increments whenever a Consul agent receives an RPC error for a request to list services associated with a gateway. | errors | counter | -| `consul.runtime.num_goroutines` | Tracks the number of running goroutines and is a general load pressure indicator. This may burst from time to time but should return to a steady state value. | number of goroutines | gauge | -| `consul.runtime.alloc_bytes` | Measures the number of bytes allocated by the Consul process. This may burst from time to time but should return to a steady state value. | bytes | gauge | -| `consul.runtime.heap_objects` | Measures the number of objects allocated on the heap and is a general memory pressure indicator. This may burst from time to time but should return to a steady state value. | number of objects | gauge | -| `consul.state.nodes` | Measures the current number of nodes registered with Consul. It is only emitted by Consul servers. Added in v1.9.0. | number of objects | gauge | -| `consul.state.services` | Measures the current number of unique services registered with Consul, based on service name. It is only emitted by Consul servers. Added in v1.9.0. | number of objects | gauge | -| `consul.state.service_instances` | Measures the current number of unique service instances registered with Consul. It is only emitted by Consul servers. Added in v1.9.0. | number of objects | gauge | -| `consul.state.kv_entries` | Measures the current number of entries in the Consul KV store. It is only emitted by Consul servers. Added in v1.10.3. | number of objects | gauge | -| `consul.state.connect_instances` | Measures the current number of unique connect service instances registered with Consul labeled by Kind (e.g. connect-proxy, connect-native, etc). Added in v1.10.4 | number of objects | gauge | -| `consul.state.config_entries` | Measures the current number of configuration entries registered with Consul labeled by Kind (e.g. service-defaults, proxy-defaults, etc). See [Configuration Entries](/docs/connect/config-entries) for more information. Added in v1.10.4 | number of objects | gauge | -| `consul.members.clients` | Measures the current number of client agents registered with Consul. It is only emitted by Consul servers. Added in v1.9.6. | number of clients | gauge | -| `consul.members.servers` | Measures the current number of server agents registered with Consul. It is only emitted by Consul servers. Added in v1.9.6. | number of servers | gauge | -| `consul.dns.stale_queries` | Increments when an agent serves a query within the allowed stale threshold. | queries | counter | -| `consul.dns.ptr_query.` | Measures the time spent handling a reverse DNS query for the given node. | ms | timer | -| `consul.dns.domain_query.` | Measures the time spent handling a domain query for the given node. | ms | timer | -| `consul.http...` | DEPRECATED IN 1.9: Tracks how long it takes to service the given HTTP request for the given verb and path. Paths do not include details like service or key names, for these an underscore will be present as a placeholder (eg. `consul.http.GET.v1.kv._`) | ms | timer | -| `consul.system.licenseExpiration` | This measures the number of hours remaining on the agents license. | hours | gauge | -| `consul.version` | Represents the Consul version. | agents | gauge | +| Metric | Description | Unit | Type | +|--------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------|---------| +| `consul.acl.blocked.{check,service}.deregistration` | Increments whenever a deregistration fails for an entity (check or service) is blocked by an ACL. | requests | counter | +| `consul.acl.blocked.{check,node,service}.registration` | Increments whenever a registration fails for an entity (check, node or service) is blocked by an ACL. | requests | counter | +| `consul.api.http` | Migrated from `consul.http..`. Samples how long it takes to service the given HTTP request for the given verb and path. Includes labels for `path` and `method`. `path` does not include details like service or key names, for these an underscore will be present as a placeholder (eg. path=`v1.kv._`) | ms | timer | +| `consul.client.rpc` | Increments whenever a Consul agent in client mode makes an RPC request to a Consul server. This gives a measure of how much a given agent is loading the Consul servers. Currently, this is only generated by agents in client mode, not Consul servers. | requests | counter | +| `consul.client.rpc.exceeded` | Increments whenever a Consul agent in client mode makes an RPC request to a Consul server gets rate limited by that agent's [`limits`](/docs/agent/config/config-files#limits) configuration. This gives an indication that there's an abusive application making too many requests on the agent, or that the rate limit needs to be increased. Currently, this only applies to agents in client mode, not Consul servers. | rejected requests | counter | +| `consul.client.rpc.failed` | Increments whenever a Consul agent in client mode makes an RPC request to a Consul server and fails. | requests | counter | +| `consul.client.api.catalog_register` | Increments whenever a Consul agent receives a catalog register request. | requests | counter | +| `consul.client.api.success.catalog_register` | Increments whenever a Consul agent successfully responds to a catalog register request. | requests | counter | +| `consul.client.rpc.error.catalog_register` | Increments whenever a Consul agent receives an RPC error for a catalog register request. | errors | counter | +| `consul.client.api.catalog_deregister` | Increments whenever a Consul agent receives a catalog deregister request. | requests | counter | +| `consul.client.api.success.catalog_deregister` | Increments whenever a Consul agent successfully responds to a catalog deregister request. | requests | counter | +| `consul.client.rpc.error.catalog_deregister` | Increments whenever a Consul agent receives an RPC error for a catalog deregister request. | errors | counter | +| `consul.client.api.catalog_datacenters` | Increments whenever a Consul agent receives a request to list datacenters in the catalog. | requests | counter | +| `consul.client.api.success.catalog_datacenters` | Increments whenever a Consul agent successfully responds to a request to list datacenters. | requests | counter | +| `consul.client.rpc.error.catalog_datacenters` | Increments whenever a Consul agent receives an RPC error for a request to list datacenters. | errors | counter | +| `consul.client.api.catalog_nodes` | Increments whenever a Consul agent receives a request to list nodes from the catalog. | requests | counter | +| `consul.client.api.success.catalog_nodes` | Increments whenever a Consul agent successfully responds to a request to list nodes. | requests | counter | +| `consul.client.rpc.error.catalog_nodes` | Increments whenever a Consul agent receives an RPC error for a request to list nodes. | errors | counter | +| `consul.client.api.catalog_services` | Increments whenever a Consul agent receives a request to list services from the catalog. | requests | counter | +| `consul.client.api.success.catalog_services` | Increments whenever a Consul agent successfully responds to a request to list services. | requests | counter | +| `consul.client.rpc.error.catalog_services` | Increments whenever a Consul agent receives an RPC error for a request to list services. | errors | counter | +| `consul.client.api.catalog_service_nodes` | Increments whenever a Consul agent receives a request to list nodes offering a service. | requests | counter | +| `consul.client.api.success.catalog_service_nodes` | Increments whenever a Consul agent successfully responds to a request to list nodes offering a service. | requests | counter | +| `consul.client.api.error.catalog_service_nodes` | Increments whenever a Consul agent receives an RPC error for request to list nodes offering a service. | requests | counter | +| `consul.client.rpc.error.catalog_service_nodes` | Increments whenever a Consul agent receives an RPC error for a request to list nodes offering a service. | errors | counter | +| `consul.client.api.catalog_node_services` | Increments whenever a Consul agent receives a request to list services registered in a node. | requests | counter | +| `consul.client.api.success.catalog_node_services` | Increments whenever a Consul agent successfully responds to a request to list services in a node. | requests | counter | +| `consul.client.rpc.error.catalog_node_services` | Increments whenever a Consul agent receives an RPC error for a request to list services in a node. | errors | counter | +| `consul.client.api.catalog_node_service_list` | Increments whenever a Consul agent receives a request to list a node's registered services. | requests | counter | +| `consul.client.rpc.error.catalog_node_service_list` | Increments whenever a Consul agent receives an RPC error for request to list a node's registered services. | errors | counter | +| `consul.client.api.success.catalog_node_service_list` | Increments whenever a Consul agent successfully responds to a request to list a node's registered services. | requests | counter | +| `consul.client.api.catalog_gateway_services` | Increments whenever a Consul agent receives a request to list services associated with a gateway. | requests | counter | +| `consul.client.api.success.catalog_gateway_services` | Increments whenever a Consul agent successfully responds to a request to list services associated with a gateway. | requests | counter | +| `consul.client.rpc.error.catalog_gateway_services` | Increments whenever a Consul agent receives an RPC error for a request to list services associated with a gateway. | errors | counter | +| `consul.runtime.num_goroutines` | Tracks the number of running goroutines and is a general load pressure indicator. This may burst from time to time but should return to a steady state value. | number of goroutines | gauge | +| `consul.runtime.alloc_bytes` | Measures the number of bytes allocated by the Consul process. This may burst from time to time but should return to a steady state value. | bytes | gauge | +| `consul.runtime.heap_objects` | Measures the number of objects allocated on the heap and is a general memory pressure indicator. This may burst from time to time but should return to a steady state value. | number of objects | gauge | +| `consul.state.nodes` | Measures the current number of nodes registered with Consul. It is only emitted by Consul servers. Added in v1.9.0. | number of objects | gauge | +| `consul.state.services` | Measures the current number of unique services registered with Consul, based on service name. It is only emitted by Consul servers. Added in v1.9.0. | number of objects | gauge | +| `consul.state.service_instances` | Measures the current number of unique service instances registered with Consul. It is only emitted by Consul servers. Added in v1.9.0. | number of objects | gauge | +| `consul.state.kv_entries` | Measures the current number of entries in the Consul KV store. It is only emitted by Consul servers. Added in v1.10.3. | number of objects | gauge | +| `consul.state.connect_instances` | Measures the current number of unique connect service instances registered with Consul labeled by Kind (e.g. connect-proxy, connect-native, etc). Added in v1.10.4 | number of objects | gauge | +| `consul.state.config_entries` | Measures the current number of configuration entries registered with Consul labeled by Kind (e.g. service-defaults, proxy-defaults, etc). See [Configuration Entries](/docs/connect/config-entries) for more information. Added in v1.10.4 | number of objects | gauge | +| `consul.members.clients` | Measures the current number of client agents registered with Consul. It is only emitted by Consul servers. Added in v1.9.6. | number of clients | gauge | +| `consul.members.servers` | Measures the current number of server agents registered with Consul. It is only emitted by Consul servers. Added in v1.9.6. | number of servers | gauge | +| `consul.dns.stale_queries` | Increments when an agent serves a query within the allowed stale threshold. | queries | counter | +| `consul.dns.ptr_query` | Measures the time spent handling a reverse DNS query for the given node. | ms | timer | +| `consul.dns.domain_query` | Measures the time spent handling a domain query for the given node. | ms | timer | +| `consul.http..` | DEPRECATED IN 1.9: Tracks how long it takes to service the given HTTP request for the given verb and path. Paths do not include details like service or key names, for these an underscore will be present as a placeholder (eg. `consul.http.GET.v1.kv._`) | ms | timer | +| `consul.system.licenseExpiration` | This measures the number of hours remaining on the agents license. | hours | gauge | +| `consul.version` | Represents the Consul version. | agents | gauge | ## Server Health These metrics are used to monitor the health of the Consul servers. -| Metric | Description | Unit | Type | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- | -| `consul.acl.ResolveToken` | Measures the time it takes to resolve an ACL token. | ms | timer | -| `consul.acl.ResolveTokenToIdentity` | Measures the time it takes to resolve an ACL token to an Identity. This metric was removed in Consul 1.12. The time will now be reflected in `consul.acl.ResolveToken`. | ms | timer | -| `consul.acl.token.cache_hit` | Increments if Consul is able to resolve a token's identity, or a legacy token, from the cache. | cache read op | counter | -| `consul.acl.token.cache_miss` | Increments if Consul cannot resolve a token's identity, or a legacy token, from the cache. | cache read op | counter | -| `consul.cache.bypass` | Counts how many times a request bypassed the cache because no cache-key was provided. | counter | counter | -| `consul.cache.fetch_success` | Counts the number of successful fetches by the cache. | counter | counter | -| `consul.cache.fetch_error` | Counts the number of failed fetches by the cache. | counter | counter | -| `consul.cache.evict_expired` | Counts the number of expired entries that are evicted. | counter | counter | -| `consul.raft.applied_index` | Represents the raft applied index. | index | gauge | -| `consul.raft.apply` | Counts the number of Raft transactions occurring over the interval, which is a general indicator of the write load on the Consul servers. | raft transactions / interval | counter | -| `consul.raft.barrier` | Counts the number of times the agent has started the barrier i.e the number of times it has issued a blocking call, to ensure that the agent has all the pending operations that were queued, to be applied to the agent's FSM. | blocks / interval | counter | -| `consul.raft.boltdb.freelistBytes` | Represents the number of bytes necessary to encode the freelist metadata. When [`raft_boltdb.NoFreelistSync`](/docs/agent/options#NoFreelistSync) is set to `false` these metadata bytes must also be written to disk for each committed log. | bytes | gauge | -| `consul.raft.boltdb.freePageBytes` | Represents the number of bytes of free space within the raft.db file. | bytes | gauge | -| `consul.raft.boltdb.getLog` | Measures the amount of time spent reading logs from the db. | ms | timer | -| `consul.raft.boltdb.logBatchSize` | Measures the total size in bytes of logs being written to the db in a single batch. | bytes | sample | -| `consul.raft.boltdb.logsPerBatch` | Measures the number of logs being written per batch to the db. | logs | sample | -| `consul.raft.boltdb.logSize` | Measures the size of logs being written to the db. | bytes | sample | -| `consul.raft.boltdb.numFreePages` | Represents the number of free pages within the raft.db file. | pages | gauge | -| `consul.raft.boltdb.numPendingPages` | Represents the number of pending pages within the raft.db that will soon become free. | pages | gauge | -| `consul.raft.boltdb.openReadTxn` | Represents the number of open read transactions against the db | transactions | gauge | -| `consul.raft.boltdb.totalReadTxn` | Represents the total number of started read transactions against the db | transactions | gauge | -| `consul.raft.boltdb.storeLogs` | Measures the amount of time spent writing logs to the db. | ms | timer | -| `consul.raft.boltdb.txstats.cursorCount` | Counts the number of cursors created since Consul was started. | cursors | counter | -| `consul.raft.boltdb.txstats.nodeCount` | Counts the number of node allocations within the db since Consul was started. | allocations | counter | -| `consul.raft.boltdb.txstats.nodeDeref` | Counts the number of node dereferences in the db since Consul was started. | dereferences | counter | -| `consul.raft.boltdb.txstats.pageAlloc` | Represents the number of bytes allocated within the db since Consul was started. Note that this does not take into account space having been freed and reused. In that case, the value of this metric will still increase. | bytes | gauge | -| `consul.raft.boltdb.txstats.pageCount` | Represents the number of pages allocated since Consul was started. Note that this does not take into account space having been freed and reused. In that case, the value of this metric will still increase. | pages | gauge | -| `consul.raft.boltdb.txstats.rebalance` | Counts the number of node rebalances performed in the db since Consul was started. | rebalances | counter | -| `consul.raft.boltdb.txstats.rebalanceTime` | Measures the time spent rebalancing nodes in the db. | ms | timer | -| `consul.raft.boltdb.txstats.spill` | Counts the number of nodes spilled in the db since Consul was started. | spills | counter | -| `consul.raft.boltdb.txstats.spillTime` | Measures the time spent spilling nodes in the db. | ms | timer | -| `consul.raft.boltdb.txstats.split` | Counts the number of nodes split in the db since Consul was started. | splits | counter | -| `consul.raft.boltdb.txstats.write` | Counts the number of writes to the db since Consul was started. | writes | counter | -| `consul.raft.boltdb.txstats.writeTime` | Measures the amount of time spent performing writes to the db. | ms | timer | -| `consul.raft.boltdb.writeCapacity` | Theoretical write capacity in terms of the number of logs that can be written per second. Each sample outputs what the capacity would be if future batched log write operations were similar to this one. This similarity encompasses 4 things: batch size, byte size, disk performance and boltdb performance. While none of these will be static and its highly likely individual samples of this metric will vary, aggregating this metric over a larger time window should provide a decent picture into how this BoltDB store can perform | logs/second | sample | -| `consul.raft.commitNumLogs` | Measures the count of logs processed for application to the FSM in a single batch. | logs | gauge | -| `consul.raft.commitTime` | Measures the time it takes to commit a new entry to the Raft log on the leader. | ms | timer | -| `consul.raft.fsm.lastRestoreDuration` | Measures the time taken to restore the FSM from a snapshot on an agent restart or from the leader calling installSnapshot. This is a gauge that holds it's value since most servers only restore during restarts which are typically infrequent. | ms | gauge | -| `consul.raft.fsm.snapshot` | Measures the time taken by the FSM to record the current state for the snapshot. | ms | timer | -| `consul.raft.fsm.apply` | Measures the time to apply a log to the FSM. | ms | timer | -| `consul.raft.fsm.enqueue` | Measures the amount of time to enqueue a batch of logs for the FSM to apply. | ms | timer | -| `consul.raft.fsm.restore` | Measures the time taken by the FSM to restore its state from a snapshot. | ms | timer | -| `consul.raft.last_index` | Represents the raft applied index. | index | gauge | -| `consul.raft.leader.dispatchLog` | Measures the time it takes for the leader to write log entries to disk. | ms | timer | -| `consul.raft.leader.dispatchNumLogs` | Measures the number of logs committed to disk in a batch. | logs | gauge | -| `consul.raft.leader.lastContact` | Measures the time since the leader was last able to contact the follower nodes when checking its leader lease. It can be used as a measure for how stable the Raft timing is and how close the leader is to timing out its lease.The lease timeout is 500 ms times the [`raft_multiplier` configuration](/docs/agent/config/config-files#raft_multiplier), so this telemetry value should not be getting close to that configured value, otherwise the Raft timing is marginal and might need to be tuned, or more powerful servers might be needed. See the [Server Performance](/docs/install/performance) guide for more details. | ms | timer | +| Metric | Description | Unit | Type | +|-----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|---------| +| `consul.acl.ResolveToken` | Measures the time it takes to resolve an ACL token. | ms | timer | +| `consul.acl.ResolveTokenToIdentity` | Measures the time it takes to resolve an ACL token to an Identity. This metric was removed in Consul 1.12. The time will now be reflected in `consul.acl.ResolveToken`. | ms | timer | +| `consul.acl.token.cache_hit` | Increments if Consul is able to resolve a token's identity, or a legacy token, from the cache. | cache read op | counter | +| `consul.acl.token.cache_miss` | Increments if Consul cannot resolve a token's identity, or a legacy token, from the cache. | cache read op | counter | +| `consul.cache.bypass` | Counts how many times a request bypassed the cache because no cache-key was provided. | counter | counter | +| `consul.cache.fetch_success` | Counts the number of successful fetches by the cache. | counter | counter | +| `consul.cache.fetch_error` | Counts the number of failed fetches by the cache. | counter | counter | +| `consul.cache.evict_expired` | Counts the number of expired entries that are evicted. | counter | counter | +| `consul.raft.applied_index` | Represents the raft applied index. | index | gauge | +| `consul.raft.apply` | Counts the number of Raft transactions occurring over the interval, which is a general indicator of the write load on the Consul servers. | raft transactions / interval | counter | +| `consul.raft.barrier` | Counts the number of times the agent has started the barrier i.e the number of times it has issued a blocking call, to ensure that the agent has all the pending operations that were queued, to be applied to the agent's FSM. | blocks / interval | counter | +| `consul.raft.boltdb.freelistBytes` | Represents the number of bytes necessary to encode the freelist metadata. When [`raft_boltdb.NoFreelistSync`](/docs/agent/options#NoFreelistSync) is set to `false` these metadata bytes must also be written to disk for each committed log. | bytes | gauge | +| `consul.raft.boltdb.freePageBytes` | Represents the number of bytes of free space within the raft.db file. | bytes | gauge | +| `consul.raft.boltdb.getLog` | Measures the amount of time spent reading logs from the db. | ms | timer | +| `consul.raft.boltdb.logBatchSize` | Measures the total size in bytes of logs being written to the db in a single batch. | bytes | sample | +| `consul.raft.boltdb.logsPerBatch` | Measures the number of logs being written per batch to the db. | logs | sample | +| `consul.raft.boltdb.logSize` | Measures the size of logs being written to the db. | bytes | sample | +| `consul.raft.boltdb.numFreePages` | Represents the number of free pages within the raft.db file. | pages | gauge | +| `consul.raft.boltdb.numPendingPages` | Represents the number of pending pages within the raft.db that will soon become free. | pages | gauge | +| `consul.raft.boltdb.openReadTxn` | Represents the number of open read transactions against the db | transactions | gauge | +| `consul.raft.boltdb.totalReadTxn` | Represents the total number of started read transactions against the db | transactions | gauge | +| `consul.raft.boltdb.storeLogs` | Measures the amount of time spent writing logs to the db. | ms | timer | +| `consul.raft.boltdb.txstats.cursorCount` | Counts the number of cursors created since Consul was started. | cursors | counter | +| `consul.raft.boltdb.txstats.nodeCount` | Counts the number of node allocations within the db since Consul was started. | allocations | counter | +| `consul.raft.boltdb.txstats.nodeDeref` | Counts the number of node dereferences in the db since Consul was started. | dereferences | counter | +| `consul.raft.boltdb.txstats.pageAlloc` | Represents the number of bytes allocated within the db since Consul was started. Note that this does not take into account space having been freed and reused. In that case, the value of this metric will still increase. | bytes | gauge | +| `consul.raft.boltdb.txstats.pageCount` | Represents the number of pages allocated since Consul was started. Note that this does not take into account space having been freed and reused. In that case, the value of this metric will still increase. | pages | gauge | +| `consul.raft.boltdb.txstats.rebalance` | Counts the number of node rebalances performed in the db since Consul was started. | rebalances | counter | +| `consul.raft.boltdb.txstats.rebalanceTime` | Measures the time spent rebalancing nodes in the db. | ms | timer | +| `consul.raft.boltdb.txstats.spill` | Counts the number of nodes spilled in the db since Consul was started. | spills | counter | +| `consul.raft.boltdb.txstats.spillTime` | Measures the time spent spilling nodes in the db. | ms | timer | +| `consul.raft.boltdb.txstats.split` | Counts the number of nodes split in the db since Consul was started. | splits | counter | +| `consul.raft.boltdb.txstats.write` | Counts the number of writes to the db since Consul was started. | writes | counter | +| `consul.raft.boltdb.txstats.writeTime` | Measures the amount of time spent performing writes to the db. | ms | timer | +| `consul.raft.boltdb.writeCapacity` | Theoretical write capacity in terms of the number of logs that can be written per second. Each sample outputs what the capacity would be if future batched log write operations were similar to this one. This similarity encompasses 4 things: batch size, byte size, disk performance and boltdb performance. While none of these will be static and its highly likely individual samples of this metric will vary, aggregating this metric over a larger time window should provide a decent picture into how this BoltDB store can perform | logs/second | sample | +| `consul.raft.commitNumLogs` | Measures the count of logs processed for application to the FSM in a single batch. | logs | gauge | +| `consul.raft.commitTime` | Measures the time it takes to commit a new entry to the Raft log on the leader. | ms | timer | +| `consul.raft.fsm.lastRestoreDuration` | Measures the time taken to restore the FSM from a snapshot on an agent restart or from the leader calling installSnapshot. This is a gauge that holds it's value since most servers only restore during restarts which are typically infrequent. | ms | gauge | +| `consul.raft.fsm.snapshot` | Measures the time taken by the FSM to record the current state for the snapshot. | ms | timer | +| `consul.raft.fsm.apply` | Measures the time to apply a log to the FSM. | ms | timer | +| `consul.raft.fsm.enqueue` | Measures the amount of time to enqueue a batch of logs for the FSM to apply. | ms | timer | +| `consul.raft.fsm.restore` | Measures the time taken by the FSM to restore its state from a snapshot. | ms | timer | +| `consul.raft.last_index` | Represents the raft applied index. | index | gauge | +| `consul.raft.leader.dispatchLog` | Measures the time it takes for the leader to write log entries to disk. | ms | timer | +| `consul.raft.leader.dispatchNumLogs` | Measures the number of logs committed to disk in a batch. | logs | gauge | +| `consul.raft.leader.lastContact` | Measures the time since the leader was last able to contact the follower nodes when checking its leader lease. It can be used as a measure for how stable the Raft timing is and how close the leader is to timing out its lease.The lease timeout is 500 ms times the [`raft_multiplier` configuration](/docs/agent/config/config-files#raft_multiplier), so this telemetry value should not be getting close to that configured value, otherwise the Raft timing is marginal and might need to be tuned, or more powerful servers might be needed. See the [Server Performance](/docs/install/performance) guide for more details. | ms | timer | | `consul.raft.leader.oldestLogAge` | The number of milliseconds since the _oldest_ log in the leader's log store was written. This can be important for replication health where write rate is high and the snapshot is large as followers may be unable to recover from a restart if restoring takes longer than the minimum value for the current leader. Compare this with `consul.raft.fsm.lastRestoreDuration` and `consul.raft.rpc.installSnapshot` to monitor. In normal usage this gauge value will grow linearly over time until a snapshot completes on the leader and the log is truncated. Note: this metric won't be emitted until the leader writes a snapshot. After an upgrade to Consul 1.10.0 it won't be emitted until the oldest log was written after the upgrade. | ms | gauge | -| `consul.raft.replication.heartbeat` | Measures the time taken to invoke appendEntries on a peer, so that it doesn’t timeout on a periodic basis. | ms | timer | -| `consul.raft.replication.appendEntries` | Measures the time it takes to replicate log entries to followers. This is a general indicator of the load pressure on the Consul servers, as well as the performance of the communication between the servers. | ms | timer | -| `consul.raft.replication.appendEntries.rpc` | Measures the time taken by the append entries RFC, to replicate the log entries of a leader agent onto its follower agent(s) | ms | timer | -| `consul.raft.replication.appendEntries.logs` | Measures the number of logs replicated to an agent, to bring it up to speed with the leader's logs. | logs appended/ interval | counter | -| `consul.raft.restore` | Counts the number of times the restore operation has been performed by the agent. Here, restore refers to the action of raft consuming an external snapshot to restore its state. | operation invoked / interval | counter | -| `consul.raft.restoreUserSnapshot` | Measures the time taken by the agent to restore the FSM state from a user's snapshot | ms | timer | -| `consul.raft.rpc.appendEntries` | Measures the time taken to process an append entries RPC call from an agent. | ms | timer | -| `consul.raft.rpc.appendEntries.storeLogs` | Measures the time taken to add any outstanding logs for an agent, since the last appendEntries was invoked | ms | timer | -| `consul.raft.rpc.appendEntries.processLogs` | Measures the time taken to process the outstanding log entries of an agent. | ms | timer | -| `consul.raft.rpc.installSnapshot` | Measures the time taken to process the installSnapshot RPC call. This metric should only be seen on agents which are currently in the follower state. | ms | timer | -| `consul.raft.rpc.processHeartBeat` | Measures the time taken to process a heartbeat request. | ms | timer | -| `consul.raft.rpc.requestVote` | Measures the time taken to process the request vote RPC call. | ms | timer | -| `consul.raft.snapshot.create` | Measures the time taken to initialize the snapshot process. | ms | timer | -| `consul.raft.snapshot.persist` | Measures the time taken to dump the current snapshot taken by the Consul agent to the disk. | ms | timer | -| `consul.raft.snapshot.takeSnapshot` | Measures the total time involved in taking the current snapshot (creating one and persisting it) by the Consul agent. | ms | timer | -| `consul.serf.snapshot.appendLine` | Measures the time taken by the Consul agent to append an entry into the existing log. | ms | timer | -| `consul.serf.snapshot.compact` | Measures the time taken by the Consul agent to compact a log. This operation occurs only when the snapshot becomes large enough to justify the compaction . | ms | timer | -| `consul.raft.state.candidate` | Increments whenever a Consul server starts an election. If this increments without a leadership change occurring it could indicate that a single server is overloaded or is experiencing network connectivity issues. | election attempts / interval | counter | -| `consul.raft.state.leader` | Increments whenever a Consul server becomes a leader. If there are frequent leadership changes this may be indication that the servers are overloaded and aren't meeting the soft real-time requirements for Raft, or that there are networking problems between the servers. | leadership transitions / interval | counter | -| `consul.raft.state.follower` | Counts the number of times an agent has entered the follower mode. This happens when a new agent joins the cluster or after the end of a leader election. | follower state entered / interval | counter | -| `consul.raft.transition.heartbeat_timeout` | The number of times an agent has transitioned to the Candidate state, after receive no heartbeat messages from the last known leader. | timeouts / interval | counter | -| `consul.raft.verify_leader` | This metric doesn't have a direct correlation to the leader change. It just counts the number of times an agent checks if it is still the leader or not. For example, during every consistent read, the check is done. Depending on the load in the system, this metric count can be high as it is incremented each time a consistent read is completed. | checks / interval | Counter | -| `consul.rpc.accept_conn` | Increments when a server accepts an RPC connection. | connections | counter | -| `consul.catalog.register` | Measures the time it takes to complete a catalog register operation. | ms | timer | -| `consul.catalog.deregister` | Measures the time it takes to complete a catalog deregister operation. | ms | timer | -| `consul.server.isLeader` | Track if a server is a leader(1) or not(0) | 1 or 0 | gauge | -| `consul.fsm.register` | Measures the time it takes to apply a catalog register operation to the FSM. | ms | timer | -| `consul.fsm.deregister` | Measures the time it takes to apply a catalog deregister operation to the FSM. | ms | timer | -| `consul.fsm.session.` | Measures the time it takes to apply the given session operation to the FSM. | ms | timer | -| `consul.fsm.kvs.` | Measures the time it takes to apply the given KV operation to the FSM. | ms | timer | -| `consul.fsm.tombstone.` | Measures the time it takes to apply the given tombstone operation to the FSM. | ms | timer | -| `consul.fsm.coordinate.batch-update` | Measures the time it takes to apply the given batch coordinate update to the FSM. | ms | timer | -| `consul.fsm.prepared-query.` | Measures the time it takes to apply the given prepared query update operation to the FSM. | ms | timer | -| `consul.fsm.txn` | Measures the time it takes to apply the given transaction update to the FSM. | ms | timer | -| `consul.fsm.autopilot` | Measures the time it takes to apply the given autopilot update to the FSM. | ms | timer | -| `consul.fsm.persist` | Measures the time it takes to persist the FSM to a raft snapshot. | ms | timer | -| `consul.fsm.intention` | Measures the time it takes to apply an intention operation to the state store. | ms | timer | -| `consul.fsm.ca` | Measures the time it takes to apply CA configuration operations to the FSM. | ms | timer | -| `consul.fsm.ca.leaf` | Measures the time it takes to apply an operation while signing a leaf certificate. | ms | timer | -| `consul.fsm.acl.token` | Measures the time it takes to apply an ACL token operation to the FSM. | ms | timer | -| `consul.fsm.acl.policy` | Measures the time it takes to apply an ACL policy operation to the FSM. | ms | timer | -| `consul.fsm.acl.bindingrule` | Measures the time it takes to apply an ACL binding rule operation to the FSM. | ms | timer | -| `consul.fsm.acl.authmethod` | Measures the time it takes to apply an ACL authmethod operation to the FSM. | ms | timer | -| `consul.fsm.system_metadata` | Measures the time it takes to apply a system metadata operation to the FSM. | ms | timer | -| `consul.kvs.apply` | Measures the time it takes to complete an update to the KV store. | ms | timer | -| `consul.leader.barrier` | Measures the time spent waiting for the raft barrier upon gaining leadership. | ms | timer | -| `consul.leader.reconcile` | Measures the time spent updating the raft store from the serf member information. | ms | timer | -| `consul.leader.reconcileMember` | Measures the time spent updating the raft store for a single serf member's information. | ms | timer | -| `consul.leader.reapTombstones` | Measures the time spent clearing tombstones. | ms | timer | -| `consul.leader.replication.acl-policies.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of ACL policy replication was successful or 0 if there was an error. | healthy | gauge | -| `consul.leader.replication.acl-policies.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of ACL policies in the primary datacenter that have been successfully replicated. | index | gauge | -| `consul.leader.replication.acl-roles.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of ACL role replication was successful or 0 if there was an error. | healthy | gauge | -| `consul.leader.replication.acl-roles.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of ACL roles in the primary datacenter that have been successfully replicated. | index | gauge | -| `consul.leader.replication.acl-tokens.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of ACL token replication was successful or 0 if there was an error. | healthy | gauge | -| `consul.leader.replication.acl-tokens.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of ACL tokens in the primary datacenter that have been successfully replicated. | index | gauge | -| `consul.leader.replication.config-entries.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of config entry replication was successful or 0 if there was an error. | healthy | gauge | -| `consul.leader.replication.config-entries.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of config entries in the primary datacenter that have been successfully replicated. | index | gauge | -| `consul.leader.replication.federation-state.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of federation state replication was successful or 0 if there was an error. | healthy | gauge | -| `consul.leader.replication.federation-state.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of federation states in the primary datacenter that have been successfully replicated. | index | gauge | -| `consul.leader.replication.namespaces.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of namespace replication was successful or 0 if there was an error. | healthy | gauge | -| `consul.leader.replication.namespaces.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of namespaces in the primary datacenter that have been successfully replicated. | index | gauge | -| `consul.prepared-query.apply` | Measures the time it takes to apply a prepared query update. | ms | timer | -| `consul.prepared-query.explain` | Measures the time it takes to process a prepared query explain request. | ms | timer | -| `consul.prepared-query.execute` | Measures the time it takes to process a prepared query execute request. | ms | timer | -| `consul.prepared-query.execute_remote` | Measures the time it takes to process a prepared query execute request that was forwarded to another datacenter. | ms | timer | -| `consul.rpc.raft_handoff` | Increments when a server accepts a Raft-related RPC connection. | connections | counter | -| `consul.rpc.request_error` | Increments when a server returns an error from an RPC request. | errors | counter | -| `consul.rpc.request` | Increments when a server receives a Consul-related RPC request. | requests | counter | -| `consul.rpc.query` | Increments when a server receives a read RPC request, indicating the rate of new read queries. See consul.rpc.queries_blocking for the current number of in-flight blocking RPC calls. This metric changed in 1.7.0 to only increment on the the start of a query. The rate of queries will appear lower, but is more accurate. | queries | counter | -| `consul.rpc.queries_blocking` | The current number of in-flight blocking queries the server is handling. | queries | gauge | -| `consul.rpc.cross-dc` | Increments when a server sends a (potentially blocking) cross datacenter RPC query. | queries | counter | -| `consul.rpc.consistentRead` | Measures the time spent confirming that a consistent read can be performed. | ms | timer | -| `consul.session.apply` | Measures the time spent applying a session update. | ms | timer | -| `consul.session.renew` | Measures the time spent renewing a session. | ms | timer | -| `consul.session_ttl.invalidate` | Measures the time spent invalidating an expired session. | ms | timer | -| `consul.txn.apply` | Measures the time spent applying a transaction operation. | ms | timer | -| `consul.txn.read` | Measures the time spent returning a read transaction. | ms | timer | -| `consul.grpc.client.request.count` | Counts the number of gRPC requests made by the client agent to a Consul server. | requests | counter | -| `consul.grpc.client.connection.count` | Counts the number of new gRPC connections opened by the client agent to a Consul server. | connections | counter | -| `consul.grpc.client.connections` | Measures the number of active gRPC connections open from the client agent to any Consul servers. | connections | gauge | -| `consul.grpc.server.request.count` | Counts the number of gRPC requests received by the server. | requests | counter | -| `consul.grpc.server.connection.count` | Counts the number of new gRPC connections received by the server. | connections | counter | -| `consul.grpc.server.connections` | Measures the number of active gRPC connections open on the server. | connections | gauge | -| `consul.grpc.server.stream.count` | Counts the number of new gRPC streams received by the server. | streams | counter | -| `consul.grpc.server.streams` | Measures the number of active gRPC streams handled by the server. | streams | gauge | -| `consul.xds.server.streams` | Measures the number of active xDS streams handled by the server split by protocol version. | streams | gauge | +| `consul.raft.replication.heartbeat` | Measures the time taken to invoke appendEntries on a peer, so that it doesn’t timeout on a periodic basis. | ms | timer | +| `consul.raft.replication.appendEntries` | Measures the time it takes to replicate log entries to followers. This is a general indicator of the load pressure on the Consul servers, as well as the performance of the communication between the servers. | ms | timer | +| `consul.raft.replication.appendEntries.rpc` | Measures the time taken by the append entries RFC, to replicate the log entries of a leader agent onto its follower agent(s) | ms | timer | +| `consul.raft.replication.appendEntries.logs` | Measures the number of logs replicated to an agent, to bring it up to speed with the leader's logs. | logs appended/ interval | counter | +| `consul.raft.restore` | Counts the number of times the restore operation has been performed by the agent. Here, restore refers to the action of raft consuming an external snapshot to restore its state. | operation invoked / interval | counter | +| `consul.raft.restoreUserSnapshot` | Measures the time taken by the agent to restore the FSM state from a user's snapshot | ms | timer | +| `consul.raft.rpc.appendEntries` | Measures the time taken to process an append entries RPC call from an agent. | ms | timer | +| `consul.raft.rpc.appendEntries.storeLogs` | Measures the time taken to add any outstanding logs for an agent, since the last appendEntries was invoked | ms | timer | +| `consul.raft.rpc.appendEntries.processLogs` | Measures the time taken to process the outstanding log entries of an agent. | ms | timer | +| `consul.raft.rpc.installSnapshot` | Measures the time taken to process the installSnapshot RPC call. This metric should only be seen on agents which are currently in the follower state. | ms | timer | +| `consul.raft.rpc.processHeartBeat` | Measures the time taken to process a heartbeat request. | ms | timer | +| `consul.raft.rpc.requestVote` | Measures the time taken to process the request vote RPC call. | ms | timer | +| `consul.raft.snapshot.create` | Measures the time taken to initialize the snapshot process. | ms | timer | +| `consul.raft.snapshot.persist` | Measures the time taken to dump the current snapshot taken by the Consul agent to the disk. | ms | timer | +| `consul.raft.snapshot.takeSnapshot` | Measures the total time involved in taking the current snapshot (creating one and persisting it) by the Consul agent. | ms | timer | +| `consul.serf.snapshot.appendLine` | Measures the time taken by the Consul agent to append an entry into the existing log. | ms | timer | +| `consul.serf.snapshot.compact` | Measures the time taken by the Consul agent to compact a log. This operation occurs only when the snapshot becomes large enough to justify the compaction . | ms | timer | +| `consul.raft.state.candidate` | Increments whenever a Consul server starts an election. If this increments without a leadership change occurring it could indicate that a single server is overloaded or is experiencing network connectivity issues. | election attempts / interval | counter | +| `consul.raft.state.leader` | Increments whenever a Consul server becomes a leader. If there are frequent leadership changes this may be indication that the servers are overloaded and aren't meeting the soft real-time requirements for Raft, or that there are networking problems between the servers. | leadership transitions / interval | counter | +| `consul.raft.state.follower` | Counts the number of times an agent has entered the follower mode. This happens when a new agent joins the cluster or after the end of a leader election. | follower state entered / interval | counter | +| `consul.raft.transition.heartbeat_timeout` | The number of times an agent has transitioned to the Candidate state, after receive no heartbeat messages from the last known leader. | timeouts / interval | counter | +| `consul.raft.verify_leader` | This metric doesn't have a direct correlation to the leader change. It just counts the number of times an agent checks if it is still the leader or not. For example, during every consistent read, the check is done. Depending on the load in the system, this metric count can be high as it is incremented each time a consistent read is completed. | checks / interval | Counter | +| `consul.rpc.accept_conn` | Increments when a server accepts an RPC connection. | connections | counter | +| `consul.catalog.register` | Measures the time it takes to complete a catalog register operation. | ms | timer | +| `consul.catalog.deregister` | Measures the time it takes to complete a catalog deregister operation. | ms | timer | +| `consul.server.isLeader` | Track if a server is a leader(1) or not(0) | 1 or 0 | gauge | +| `consul.fsm.register` | Measures the time it takes to apply a catalog register operation to the FSM. | ms | timer | +| `consul.fsm.deregister` | Measures the time it takes to apply a catalog deregister operation to the FSM. | ms | timer | +| `consul.fsm.session` | Measures the time it takes to apply the given session operation to the FSM. | ms | timer | +| `consul.fsm.kvs` | Measures the time it takes to apply the given KV operation to the FSM. | ms | timer | +| `consul.fsm.tombstone` | Measures the time it takes to apply the given tombstone operation to the FSM. | ms | timer | +| `consul.fsm.coordinate.batch-update` | Measures the time it takes to apply the given batch coordinate update to the FSM. | ms | timer | +| `consul.fsm.prepared-query` | Measures the time it takes to apply the given prepared query update operation to the FSM. | ms | timer | +| `consul.fsm.txn` | Measures the time it takes to apply the given transaction update to the FSM. | ms | timer | +| `consul.fsm.autopilot` | Measures the time it takes to apply the given autopilot update to the FSM. | ms | timer | +| `consul.fsm.persist` | Measures the time it takes to persist the FSM to a raft snapshot. | ms | timer | +| `consul.fsm.intention` | Measures the time it takes to apply an intention operation to the state store. | ms | timer | +| `consul.fsm.ca` | Measures the time it takes to apply CA configuration operations to the FSM. | ms | timer | +| `consul.fsm.ca.leaf` | Measures the time it takes to apply an operation while signing a leaf certificate. | ms | timer | +| `consul.fsm.acl.token` | Measures the time it takes to apply an ACL token operation to the FSM. | ms | timer | +| `consul.fsm.acl.policy` | Measures the time it takes to apply an ACL policy operation to the FSM. | ms | timer | +| `consul.fsm.acl.bindingrule` | Measures the time it takes to apply an ACL binding rule operation to the FSM. | ms | timer | +| `consul.fsm.acl.authmethod` | Measures the time it takes to apply an ACL authmethod operation to the FSM. | ms | timer | +| `consul.fsm.system_metadata` | Measures the time it takes to apply a system metadata operation to the FSM. | ms | timer | +| `consul.kvs.apply` | Measures the time it takes to complete an update to the KV store. | ms | timer | +| `consul.leader.barrier` | Measures the time spent waiting for the raft barrier upon gaining leadership. | ms | timer | +| `consul.leader.reconcile` | Measures the time spent updating the raft store from the serf member information. | ms | timer | +| `consul.leader.reconcileMember` | Measures the time spent updating the raft store for a single serf member's information. | ms | timer | +| `consul.leader.reapTombstones` | Measures the time spent clearing tombstones. | ms | timer | +| `consul.leader.replication.acl-policies.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of ACL policy replication was successful or 0 if there was an error. | healthy | gauge | +| `consul.leader.replication.acl-policies.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of ACL policies in the primary datacenter that have been successfully replicated. | index | gauge | +| `consul.leader.replication.acl-roles.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of ACL role replication was successful or 0 if there was an error. | healthy | gauge | +| `consul.leader.replication.acl-roles.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of ACL roles in the primary datacenter that have been successfully replicated. | index | gauge | +| `consul.leader.replication.acl-tokens.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of ACL token replication was successful or 0 if there was an error. | healthy | gauge | +| `consul.leader.replication.acl-tokens.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of ACL tokens in the primary datacenter that have been successfully replicated. | index | gauge | +| `consul.leader.replication.config-entries.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of config entry replication was successful or 0 if there was an error. | healthy | gauge | +| `consul.leader.replication.config-entries.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of config entries in the primary datacenter that have been successfully replicated. | index | gauge | +| `consul.leader.replication.federation-state.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of federation state replication was successful or 0 if there was an error. | healthy | gauge | +| `consul.leader.replication.federation-state.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of federation states in the primary datacenter that have been successfully replicated. | index | gauge | +| `consul.leader.replication.namespaces.status` | This will only be emitted by the leader in a secondary datacenter. The value will be a 1 if the last round of namespace replication was successful or 0 if there was an error. | healthy | gauge | +| `consul.leader.replication.namespaces.index` | This will only be emitted by the leader in a secondary datacenter. Increments to the index of namespaces in the primary datacenter that have been successfully replicated. | index | gauge | +| `consul.prepared-query.apply` | Measures the time it takes to apply a prepared query update. | ms | timer | +| `consul.prepared-query.explain` | Measures the time it takes to process a prepared query explain request. | ms | timer | +| `consul.prepared-query.execute` | Measures the time it takes to process a prepared query execute request. | ms | timer | +| `consul.prepared-query.execute_remote` | Measures the time it takes to process a prepared query execute request that was forwarded to another datacenter. | ms | timer | +| `consul.rpc.raft_handoff` | Increments when a server accepts a Raft-related RPC connection. | connections | counter | +| `consul.rpc.request_error` | Increments when a server returns an error from an RPC request. | errors | counter | +| `consul.rpc.request` | Increments when a server receives a Consul-related RPC request. | requests | counter | +| `consul.rpc.query` | Increments when a server receives a read RPC request, indicating the rate of new read queries. See consul.rpc.queries_blocking for the current number of in-flight blocking RPC calls. This metric changed in 1.7.0 to only increment on the the start of a query. The rate of queries will appear lower, but is more accurate. | queries | counter | +| `consul.rpc.queries_blocking` | The current number of in-flight blocking queries the server is handling. | queries | gauge | +| `consul.rpc.cross-dc` | Increments when a server sends a (potentially blocking) cross datacenter RPC query. | queries | counter | +| `consul.rpc.consistentRead` | Measures the time spent confirming that a consistent read can be performed. | ms | timer | +| `consul.session.apply` | Measures the time spent applying a session update. | ms | timer | +| `consul.session.renew` | Measures the time spent renewing a session. | ms | timer | +| `consul.session_ttl.invalidate` | Measures the time spent invalidating an expired session. | ms | timer | +| `consul.txn.apply` | Measures the time spent applying a transaction operation. | ms | timer | +| `consul.txn.read` | Measures the time spent returning a read transaction. | ms | timer | +| `consul.grpc.client.request.count` | Counts the number of gRPC requests made by the client agent to a Consul server. | requests | counter | +| `consul.grpc.client.connection.count` | Counts the number of new gRPC connections opened by the client agent to a Consul server. | connections | counter | +| `consul.grpc.client.connections` | Measures the number of active gRPC connections open from the client agent to any Consul servers. | connections | gauge | +| `consul.grpc.server.request.count` | Counts the number of gRPC requests received by the server. | requests | counter | +| `consul.grpc.server.connection.count` | Counts the number of new gRPC connections received by the server. | connections | counter | +| `consul.grpc.server.connections` | Measures the number of active gRPC connections open on the server. | connections | gauge | +| `consul.grpc.server.stream.count` | Counts the number of new gRPC streams received by the server. | streams | counter | +| `consul.grpc.server.streams` | Measures the number of active gRPC streams handled by the server. | streams | gauge | +| `consul.xds.server.streams` | Measures the number of active xDS streams handled by the server split by protocol version. | streams | gauge | ## Server Workload @@ -592,7 +592,7 @@ Here is a Prometheus style example of an RPC metric and its labels: -```json +``` ... consul_rpc_server_call{errored="false",method="Catalog.ListNodes",request_type="read",rpc_type="net/rpc",quantile="0.5"} 255 ... @@ -606,43 +606,44 @@ Any metric in this section can be turned off with the [`prefix_filter`](/docs/ag These metrics give insight into the health of the cluster as a whole. -| Metric | Description | Unit | Type | -| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------- | -| `consul.memberlist.degraded.probe` | Counts the number of times the agent has performed failure detection on another agent at a slower probe rate. The agent uses its own health metric as an indicator to perform this action. (If its health score is low, means that the node is healthy, and vice versa.) | probes / interval | counter | -| `consul.memberlist.degraded.timeout` | Counts the number of times an agent was marked as a dead node, whilst not getting enough confirmations from a randomly selected list of agent nodes in an agent's membership. | occurrence / interval | counter | -| `consul.memberlist.msg.dead` | Counts the number of times an agent has marked another agent to be a dead node. | messages / interval | counter | -| `consul.memberlist.health.score` | Describes a node's perception of its own health based on how well it is meeting the soft real-time requirements of the protocol. This metric ranges from 0 to 8, where 0 indicates "totally healthy". This health score is used to scale the time between outgoing probes, and higher scores translate into longer probing intervals. For more details see section IV of the Lifeguard paper: https://arxiv.org/pdf/1707.00788.pdf | score | gauge | -| `consul.memberlist.msg.suspect` | Increments when an agent suspects another as failed when executing random probes as part of the gossip protocol. These can be an indicator of overloaded agents, network problems, or configuration errors where agents can not connect to each other on the [required ports](/docs/agent/config/config-files#ports). | suspect messages received / interval | counter | -| `consul.memberlist.tcp.accept` | Counts the number of times an agent has accepted an incoming TCP stream connection. | connections accepted / interval | counter | -| `consul.memberlist.udp.sent/received` | Measures the total number of bytes sent/received by an agent through the UDP protocol. | bytes sent or bytes received / interval | counter | -| `consul.memberlist.tcp.connect` | Counts the number of times an agent has initiated a push/pull sync with an other agent. | push/pull initiated / interval | counter | -| `consul.memberlist.tcp.sent` | Measures the total number of bytes sent by an agent through the TCP protocol | bytes sent / interval | counter | -| `consul.memberlist.gossip` | Measures the time taken for gossip messages to be broadcasted to a set of randomly selected nodes. | ms | timer | -| `consul.memberlist.msg_alive` | Counts the number of alive messages, that the agent has processed so far, based on the message information given by the network layer. | messages / Interval | counter | -| `consul.memberlist.msg_dead` | The number of dead messages that the agent has processed so far, based on the message information given by the network layer. | messages / Interval | counter | -| `consul.memberlist.msg_suspect` | The number of suspect messages that the agent has processed so far, based on the message information given by the network layer. | messages / Interval | counter | -| `consul.memberlist.probeNode` | Measures the time taken to perform a single round of failure detection on a select agent. | nodes / Interval | counter | -| `consul.memberlist.pushPullNode` | Measures the number of agents that have exchanged state with this agent. | nodes / Interval | counter | -| `consul.serf.member.failed` | Increments when an agent is marked dead. This can be an indicator of overloaded agents, network problems, or configuration errors where agents cannot connect to each other on the [required ports](/docs/agent/config/config-files#ports). | failures / interval | counter | -| `consul.serf.member.flap` | Available in Consul 0.7 and later, this increments when an agent is marked dead and then recovers within a short time period. This can be an indicator of overloaded agents, network problems, or configuration errors where agents cannot connect to each other on the [required ports](/docs/agent/config/config-files#ports). | flaps / interval | counter | -| `consul.serf.member.join` | Increments when an agent joins the cluster. If an agent flapped or failed this counter also increments when it re-joins. | joins / interval | counter | -| `consul.serf.member.left` | Increments when an agent leaves the cluster. | leaves / interval | counter | -| `consul.serf.events` | Increments when an agent processes an [event](/commands/event). Consul uses events internally so there may be additional events showing in telemetry. There are also a per-event counters emitted as `consul.serf.events.`. | events / interval | counter | -| `consul.serf.msgs.sent` | This metric is sample of the number of bytes of messages broadcast to the cluster. In a given time interval, the sum of this metric is the total number of bytes sent and the count is the number of messages sent. | message bytes / interval | counter | -| `consul.autopilot.failure_tolerance` | Tracks the number of voting servers that the cluster can lose while continuing to function. | servers | gauge | -| `consul.autopilot.healthy` | Tracks the overall health of the local server cluster. If all servers are considered healthy by Autopilot, this will be set to 1. If any are unhealthy, this will be 0. | boolean | gauge | -| `consul.session_ttl.active` | Tracks the active number of sessions being tracked. | sessions | gauge | -| `consul.catalog.service.query.` | Increments for each catalog query for the given service. | queries | counter | -| `consul.catalog.service.query-tag..` | Increments for each catalog query for the given service with the given tag. | queries | counter | -| `consul.catalog.service.query-tags..` | Increments for each catalog query for the given service with the given tags. | queries | counter | -| `consul.catalog.service.not-found.` | Increments for each catalog query where the given service could not be found. | queries | counter | -| `consul.catalog.connect.query.` | Increments for each connect-based catalog query for the given service. | queries | counter | -| `consul.catalog.connect.query-tag..` | Increments for each connect-based catalog query for the given service with the given tag. | queries | counter | -| `consul.catalog.connect.query-tags..` | Increments for each connect-based catalog query for the given service with the given tags. | queries | counter | -| `consul.catalog.connect.not-found.` | Increments for each connect-based catalog query where the given service could not be found. | queries | counter | -| `consul.mesh.active-root-ca.expiry` | The number of seconds until the root CA expires, updated every hour. | seconds | gauge | -| `consul.mesh.active-signing-ca.expiry`| The number of seconds until the signing CA expires, updated every hour. | seconds | gauge | -| `consul.agent.tls.cert.expiry` | The number of seconds until the Agent TLS certificate expires, updated every hour. | seconds | gauge | +| Metric | Description | Unit | Type | +|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------|---------| +| `consul.memberlist.degraded.probe` | Counts the number of times the agent has performed failure detection on another agent at a slower probe rate. The agent uses its own health metric as an indicator to perform this action. (If its health score is low, means that the node is healthy, and vice versa.) | probes / interval | counter | +| `consul.memberlist.degraded.timeout` | Counts the number of times an agent was marked as a dead node, whilst not getting enough confirmations from a randomly selected list of agent nodes in an agent's membership. | occurrence / interval | counter | +| `consul.memberlist.msg.dead` | Counts the number of times an agent has marked another agent to be a dead node. | messages / interval | counter | +| `consul.memberlist.health.score` | Describes a node's perception of its own health based on how well it is meeting the soft real-time requirements of the protocol. This metric ranges from 0 to 8, where 0 indicates "totally healthy". This health score is used to scale the time between outgoing probes, and higher scores translate into longer probing intervals. For more details see section IV of the Lifeguard paper: https://arxiv.org/pdf/1707.00788.pdf | score | gauge | +| `consul.memberlist.msg.suspect` | Increments when an agent suspects another as failed when executing random probes as part of the gossip protocol. These can be an indicator of overloaded agents, network problems, or configuration errors where agents can not connect to each other on the [required ports](/docs/agent/config/config-files#ports). | suspect messages received / interval | counter | +| `consul.memberlist.tcp.accept` | Counts the number of times an agent has accepted an incoming TCP stream connection. | connections accepted / interval | counter | +| `consul.memberlist.udp.sent/received` | Measures the total number of bytes sent/received by an agent through the UDP protocol. | bytes sent or bytes received / interval | counter | +| `consul.memberlist.tcp.connect` | Counts the number of times an agent has initiated a push/pull sync with an other agent. | push/pull initiated / interval | counter | +| `consul.memberlist.tcp.sent` | Measures the total number of bytes sent by an agent through the TCP protocol | bytes sent / interval | counter | +| `consul.memberlist.gossip` | Measures the time taken for gossip messages to be broadcasted to a set of randomly selected nodes. | ms | timer | +| `consul.memberlist.msg_alive` | Counts the number of alive messages, that the agent has processed so far, based on the message information given by the network layer. | messages / Interval | counter | +| `consul.memberlist.msg_dead` | The number of dead messages that the agent has processed so far, based on the message information given by the network layer. | messages / Interval | counter | +| `consul.memberlist.msg_suspect` | The number of suspect messages that the agent has processed so far, based on the message information given by the network layer. | messages / Interval | counter | +| `consul.memberlist.probeNode` | Measures the time taken to perform a single round of failure detection on a select agent. | nodes / Interval | counter | +| `consul.memberlist.pushPullNode` | Measures the number of agents that have exchanged state with this agent. | nodes / Interval | counter | +| `consul.serf.member.failed` | Increments when an agent is marked dead. This can be an indicator of overloaded agents, network problems, or configuration errors where agents cannot connect to each other on the [required ports](/docs/agent/config/config-files#ports). | failures / interval | counter | +| `consul.serf.member.flap` | Available in Consul 0.7 and later, this increments when an agent is marked dead and then recovers within a short time period. This can be an indicator of overloaded agents, network problems, or configuration errors where agents cannot connect to each other on the [required ports](/docs/agent/config/config-files#ports). | flaps / interval | counter | +| `consul.serf.member.join` | Increments when an agent joins the cluster. If an agent flapped or failed this counter also increments when it re-joins. | joins / interval | counter | +| `consul.serf.member.left` | Increments when an agent leaves the cluster. | leaves / interval | counter | +| `consul.serf.events` | Increments when an agent processes an [event](/commands/event). Consul uses events internally so there may be additional events showing in telemetry. There are also a per-event counters emitted as `consul.serf.events.`. | events / interval | counter | +| `consul.serf.events.` | Breakdown of `consul.serf.events` by type of event. | events / interval | counter | +| `consul.serf.msgs.sent` | This metric is sample of the number of bytes of messages broadcast to the cluster. In a given time interval, the sum of this metric is the total number of bytes sent and the count is the number of messages sent. | message bytes / interval | counter | +| `consul.autopilot.failure_tolerance` | Tracks the number of voting servers that the cluster can lose while continuing to function. | servers | gauge | +| `consul.autopilot.healthy` | Tracks the overall health of the local server cluster. If all servers are considered healthy by Autopilot, this will be set to 1. If any are unhealthy, this will be 0. | boolean | gauge | +| `consul.session_ttl.active` | Tracks the active number of sessions being tracked. | sessions | gauge | +| `consul.catalog.service.query` | Increments for each catalog query for the given service. | queries | counter | +| `consul.catalog.service.query-tag` | Increments for each catalog query for the given service with the given tag. | queries | counter | +| `consul.catalog.service.query-tags` | Increments for each catalog query for the given service with the given tags. | queries | counter | +| `consul.catalog.service.not-found` | Increments for each catalog query where the given service could not be found. | queries | counter | +| `consul.catalog.connect.query` | Increments for each connect-based catalog query for the given service. | queries | counter | +| `consul.catalog.connect.query-tag` | Increments for each connect-based catalog query for the given service with the given tag. | queries | counter | +| `consul.catalog.connect.query-tags` | Increments for each connect-based catalog query for the given service with the given tags. | queries | counter | +| `consul.catalog.connect.not-found` | Increments for each connect-based catalog query where the given service could not be found. | queries | counter | +| `consul.mesh.active-root-ca.expiry` | The number of seconds until the root CA expires, updated every hour. | seconds | gauge | +| `consul.mesh.active-signing-ca.expiry` | The number of seconds until the signing CA expires, updated every hour. | seconds | gauge | +| `consul.agent.tls.cert.expiry` | The number of seconds until the Agent TLS certificate expires, updated every hour. | seconds | gauge | ## Connect Built-in Proxy Metrics diff --git a/website/content/docs/api-gateway/consul-api-gateway-install.mdx b/website/content/docs/api-gateway/consul-api-gateway-install.mdx index 087c0e70e..ffe6b2768 100644 --- a/website/content/docs/api-gateway/consul-api-gateway-install.mdx +++ b/website/content/docs/api-gateway/consul-api-gateway-install.mdx @@ -15,7 +15,7 @@ Ensure that the environment you are deploying Consul API Gateway in meets the re ## Installation --> **NOTE:** When you see `VERSION` in examples of commands or configuration settings, replace `VERSION` with the version number of the release you are installing, like `0.2.0`. If there is a lower case "v" in front of `VERSION` the version number needs to follow the "v" as is `v0.2.0` +-> **Version reference convention:** Replace `VERSION` in command and configuration examples with the Consul API Gateway version you are installing, such as `0.3.0`. In some instances, `VERSION` is prepended with a lowercase _v_. This indicates that you must include the `v` as is part of the version, for example `v0.3.0`. 1. Issue the following command to install the CRDs: @@ -44,7 +44,7 @@ Ensure that the environment you are deploying Consul API Gateway in meets the re 1. Install Consul API Gateway using the standard Consul Helm chart and specify the custom values file. Available versions of the [Consul Helm chart](https://github.com/hashicorp/consul-k8s/releases) can be found in GitHub releases. ```shell-session - $ helm install consul hashicorp/consul --version 0.43.0 --values values.yaml --create-namespace --namespace consul + $ helm install consul hashicorp/consul --version 0.45.0 --values values.yaml --create-namespace --namespace consul ``` ## Usage @@ -168,9 +168,12 @@ The following table describes the allowed parameters for the `spec` array: | `consul.ports.http` | Specifies the port for Consul's HTTP server. | Integer | `8500` | | `consul.scheme` | Specifies the scheme to use for connecting to Consul. The supported values are `"http"` and `"https"`. | String | `"http"` | | `copyAnnotations.service` | List of annotations to copy to the gateway service. | Array | `["external-dns.alpha.kubernetes.io/hostname"]` | -| `image.consulAPIGateway` | The image to use for consul-api-gateway. View available image tags on [DockerHub](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags). | String | `"hashicorp/consul-api-gateway:RELEASE_VERSION"` | -| `image.envoy` | Specifies the container image to use for Envoy. View available image tags on [DockerHub](https://hub.docker.com/r/envoyproxy/envoy/tags). | String | `"envoyproxy/envoy:RELEASE_VERSION"` | -| `logLevel` | Specifies the error reporting level for logs. You can specify the following values: `error`, `warning`, `info`, `debug`, `trace`. | String | `"info"` | +| `deployment.defaultInstances` | Specifies the number of instances to deploy by default for each gateway. | Integer | 1 | +| `deployment.maxInstances` | Specifies the maximum allowed number of instances per gateway. | Integer | 8 | +| `deployment.minInstances` | Specifies the minimum allowed number of instances per gateway. | Integer | 1 | +| `image.consulAPIGateway` | The image to use for consul-api-gateway. View available image tags on [DockerHub](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags). | String | `"hashicorp/consul-api-gateway:RELEASE_VERSION"` | +| `image.envoy` | Specifies the container image to use for Envoy. View available image tags on [DockerHub](https://hub.docker.com/r/envoyproxy/envoy/tags). | String | `"envoyproxy/envoy:RELEASE_VERSION"` | +| `logLevel` | Specifies the error reporting level for logs. You can specify the following values: `error`, `warning`, `info`, `debug`, `trace`. | String | `"info"` | | `nodeSelector` | Specifies a set of parameters that constrain the nodes on which the pod can run. Defining nodes with the `nodeSelector` enables the pod to fit on a node. The selector must match a node's labels for the pod to be scheduled on that node. Refer to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) for additional information. | Object | N/A | | `serviceType` | Specifies the ingress methods for a service. The following values are supported:
    `ClusterIP`
    `NodePort`
    `LoadBalancer`. | String | N/A | | `useHostPorts` | If set to `true`, then the Envoy container ports are mapped to host ports. | Boolean | `false` | @@ -241,7 +244,56 @@ spec:
    -Refer to the [Kubernetes Gateway API documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Gateway) for details about configuring gateways: +If you configure a listener's `certificateRefs` to reference a secret in a different namespace, you must also create a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) in the same namespace as the secret. The `ReferencePolicy` grants the listener the permission to read the secret. + +The following example creates a `Gateway` named `example-gateway` in `gateway-namespace`. This `Gateway` has a `certificateRef` in `secret-namespace`. +The listener can use the certificate because `reference-policy` in `secret-namespace` is configured to allow `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace`. + + + +```yaml +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: Gateway +metadata: + name: example-gateway + namespace: gateway-namespace + annotations: + 'external-dns.alpha.kubernetes.io/hostname': DNS_HOSTNAME +spec: + gatewayClassName: test-gateway-class + listeners: + - protocol: HTTPS + hostname: DNS_HOSTNAME + port: 443 + name: https + allowedRoutes: + namespaces: + from: Same + tls: + certificateRefs: + - name: gateway-production-certificate + namespace: secret-namespace +--- + +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: ReferencePolicy +metadata: + name: reference-policy + namespace: secret-namespace +spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: gateway-namespace + to: + - group: "" + kind: Secret + name: gateway-production-certificate +``` + + + +Refer to the [Kubernetes Gateway API documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Gateway) for further details about configuring gateways. #### Listeners @@ -263,6 +315,25 @@ Add the `listener` object to the `gateway` configuration and specify the followi Refer to the [Kubernetes Gateway API documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Listener) for details about configuring listeners. +#### Scaling + +You can scale a logical gateway object to multiple instances with the [`kubectl scale`](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#scaling-a-deployment) command. The object scales according to the bounds set in `GatewayClassConfig`. + +```shell-session +$ kubectl get deployment --selector api-gateway.consul.hashicorp.com/name=example-gateway +NAME READY UP-TO-DATE AVAILABLE +example-gateway 1/1 1 1 +``` +```shell-session +$ kubectl scale deployment/example-gateway --replicas=3 +deployment.apps/example-gateway scaled +``` +```shell-session +$ kubectl get deployment --selector api-gateway.consul.hashicorp.com/name=example-gateway +NAME READY UP-TO-DATE AVAILABLE +example-gateway 3/3 3 3 +``` + ### Route Routes are independent configuration objects that are associated with specific listeners. diff --git a/website/content/docs/api-gateway/index.mdx b/website/content/docs/api-gateway/index.mdx index 5234eac60..5a6c75af4 100644 --- a/website/content/docs/api-gateway/index.mdx +++ b/website/content/docs/api-gateway/index.mdx @@ -38,7 +38,7 @@ are used, see the [documentation in our GitHub repo](https://github.com/hashicor | [`Gateway`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Gateway) |
    • Supported protocols: `HTTP`, `HTTPS`, `TCP`
    • Header-based hostname matching (no SNI support)
    • Supported filters: header addition, removal, and setting
    • TLS modes supported: `terminate`
    • Certificate types supported: `core/v1/Secret`
    • Extended options: TLS version and cipher constraints
    | | [`HTTPRoute`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRoute) |
    • Weight-based load balancing
    • Supported rules: path, header, query, and method-based matching
    • Supported filters: header addition, removal, and setting
    • Supported backend types:
      1. `core/v1/Service` (must map to a registered Consul service)
      2. `api-gateway.consul.hashicorp.com/v1alpha1/MeshService`
    | | [`TCPRoute`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute) |
    • Supported backend types:
      1. `core/v1/Service` (must map to a registered Consul service)
      2. `api-gateway.consul.hashicorp.com/v1alpha1/MeshService`
    | -| [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) |
    • Required to allow any reference from an `HTTPRoute` or `TCPRoute` to a Kubernetes `core/v1/Service` in a different namespace.
    • A route with an unpermitted BackendRef caused by the lack of a ReferencePolicy sets a `ResolvedRefs` status to `False` with the reason `RefNotPermitted`. The gateway listener rejects routes with an unpermitted BackendRef.
    • WARNING: If a route BackendRef becomes unpermitted, the entire route is removed from the gateway listener.
      • A BackendRef can become unpermitted when you delete a Reference Policy or add a new unpermitted BackendRef to an existing route.
    | +| [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) |
    • Required to allow any reference from a `Gateway` to a Kubernetes `core/v1/Secret` in a different namespace.
      • A Gateway with an unpermitted `certificateRefs` caused by the lack of a` ReferencePolicy` sets a `ResolvedRefs` status to `False` with the reason `InvalidCertificateRef`. The Gateway will not become ready in this case.
    • Required to allow any reference from an `HTTPRoute` or `TCPRoute` to a Kubernetes `core/v1/Service` in a different namespace.
      • A route with an unpermitted `backendRefs` caused by the lack of a `ReferencePolicy` sets a `ResolvedRefs` status to `False` with the reason `RefNotPermitted`. The gateway listener rejects routes with an unpermitted `backendRefs`.
      • WARNING: If a route `backendRefs` becomes unpermitted, the entire route is removed from the gateway listener.
        • A `backendRefs` can become unpermitted when you delete a `ReferencePolicy` or add a new unpermitted `backendRefs` to an existing route.
    | ## Additional Resources diff --git a/website/content/docs/api-gateway/tech-specs.mdx b/website/content/docs/api-gateway/tech-specs.mdx index bfd9e893c..e49934919 100644 --- a/website/content/docs/api-gateway/tech-specs.mdx +++ b/website/content/docs/api-gateway/tech-specs.mdx @@ -18,9 +18,10 @@ Verify that your environment meets the following requirements prior to using Con Your datacenter must meet the following requirements prior to configuring the Consul API Gateway: - Kubernetes 1.21+ + - Kubernetes 1.24 is not supported at this time. - `kubectl` 1.21+ -- Consul 1.12.0+ -- HashiCorp Consul Helm chart 0.43.0+ +- Consul 1.11.2+ +- HashiCorp Consul Helm chart 0.45.0+ - Consul Service Mesh must be deployed on the Kubernetes cluster that API Gateway is deployed on. - Envoy: Envoy proxy support is determined by the Consul version deployed. Refer to [Envoy Integration](/docs/connect/proxies/envoy) for details. diff --git a/website/content/docs/api-gateway/upgrade-specific-versions.mdx b/website/content/docs/api-gateway/upgrade-specific-versions.mdx index fa38c15bd..59381baa7 100644 --- a/website/content/docs/api-gateway/upgrade-specific-versions.mdx +++ b/website/content/docs/api-gateway/upgrade-specific-versions.mdx @@ -9,11 +9,166 @@ description: >- This topic describes how to upgrade Consul API Gateway. -## Breaking Changes -Consul API Gateway v0.2.0 introduces a breaking change for people upgrading from Consul API Gateway v0.1.0. Routes with a `backendRef` defined in a different namespace now require a [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) that explicitly allows traffic from the route's namespace to the `backendRef`'s namespace. +## Upgrade to v0.3.0 from v0.2.0 or lower -## Requirements +Consul API Gateway v0.3.0 introduces a change for people upgrading from lower versions. Gateways with `listeners` with a `certificateRef` defined in a different namespace now require a [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) that explicitly allows `Gateways` from the gateway's namesapce to use `certificateRef` in the `certificateRef`'s namespace. + +### Requirements + +Ensure that the following requirements are met prior to upgrading: + +- Consul API Gateway should be running version v0.2.1 or lower. +- You should have the ability to run `kubectl` CLI commands. +- `kubectl` should be configured to point to the cluster containing the installation you are upgrading. +- You should have the following permission rights on your Kubernetes cluster: + - `Gateway.read` + - `ReferencePolicy.create` +- (Optional) The [jq](https://stedolan.github.io/jq/download/) command line processor for JSON can be installed, which will ease gateway retrieval during the upgrade process. + +### Procedure + + +1. Verify the current version of the `consul-api-gateway-controller` `Deployment`: + + ```shell-session + $ kubectl get deployment --namespace consul consul-api-gateway-controller --output=jsonpath= "{@.spec.template.spec.containers[?(@.name=='api-gateway-controller')].image}" + ``` + + You should receive a response similar to the following: + + ```log + "hashicorp/consul-api-gateway:0.2.1" + ``` + +1. Retrieve all gateways that have a `certificateRefs` in a different namespace. If you have installed the [`jq`](https://stedolan.github.io/jq/) utility, you can skip to [step 4](#jq-command-secrets). Otherwise, issue the following command to get all `Gateways` across all namespaces: + + ```shell-session + $ kubectl get Gateway --output json --all-namespaces + ``` + + If you have any active `Gateways`, you will receive output similar to the following response. The output has been truncated to show only relevant fields: + + ```yaml + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: Gateway + metadata: + name: example-gateway + namespace: gateway-namespace + spec: + gatewayClassName: "consul-api-gateway" + listeners: + - name: https + port: 443 + protocol: HTTPS + allowedRoutes: + namespaces: + from: All + tls: + certificateRefs: + - group: "" + kind: Secret + name: example-certificate + namespace: certificate-namespace + ``` + +1. Inspect the `certificateRefs` entries for each of the routes. + + If a `namespace` field is not defined in the `certificateRefs` or if the namespace matches the namespace of the parent `Gateway`, then no additional action is required for the `certificateRefs`. Otherwise, note the `namespace` field values for `certificateRefs` configurations with a `namespace` field that do not match the namespace of the parent `Gateway`. You must also note the `namespace` of the parent gateway. You will need these to create a `ReferencePolicy` that explicitly allows each cross-namespace certificateRefs-to-gateway pair. (see [step 5](#create-secret-reference-policy)). + + After completing this step, you will have a list of all secrets similar to the following: + + + + ```yaml hideClipboard + example-certificate: + - namespace: certificate-namespace + parentNamespace: gateway-namespace + ``` + + + + Proceed with the [standard-upgrade](#standard-upgrade) if your list is empty. + + + +1. If you have installed [`jq`](https://stedolan.github.io/jq/), issue the following command to get all `Gateways` and filter for secrets that require a `ReferencePolicy`. + + ```shell-session + + $ kubectl get Gateway -o json -A | jq -r '.items[] | {gateway_name: .metadata.name, gateway_namespace: .metadata.namespace, kind: .kind, crossNamespaceSecrets: ( .metadata.namespace as $parentnamespace | .spec.listeners[] | select(has("tls")) | .tls.certificateRefs[] | select(.namespace != null and .namespace != $parentnamespace ) )} ' + + ``` + + The output will resemble the following response if gateways that require a new `ReferencePolicy` are returned: + + + + ```log hideClipboard + { + "gateway_name": "example-gateway", + "gateway_namespace": "gateway-namespace", + "kind": "Gateway", + "crossNamespaceSecrets": { + "group": "", + "kind": "Secret", + "name": "cexample-certificate", + "namespace": "certificate-namespace" + } + } + ``` + + + + If your output is empty, proceed with the [standard-upgrade](#standard-upgrade). + + +1. Using the list of secrets you created earlier as a guide, create a [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) to allow each gateway cross namespace secret access. + The `ReferencePolicy` explicitly allows each cross-namespace gateway to secret pair. The `ReferencePolicy` must be created in the same `namespace` as the `certificateRefs`. + + Skip to the next step if you've already created a `ReferencePolicy`. + + The following example `ReferencePolicy` enables `example-gateway` in `gateway-namespace` to utilize `certificateRefs` in the `certificate-namespace` namespace: + + + + ```yaml + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: ReferencePolicy + metadata: + name: reference-policy + namespace: certificate-namespace + spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: gateway-namespace + to: + - group: "" + kind: Secret + ``` + + + +1. If you have already created a `ReferencePolicy`, modify it to allow your gateway to access your `certificateRef` and save it as `referencepolicy.yaml`. Note that each `ReferencePolicy` only supports one `to` field and one `from` field (refer the [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/api-types/referencepolicy/#api-design-decisions) documentation). As a result, you may need to create multiple `ReferencePolicy`s. + +1. Issue the following command to apply it to your cluster: + + ```shell-session + $ kubectl apply --filename referencepolicy.yaml + ``` + + Repeat this step as needed until each of your cross-namespace `certificateRefs` have a corresponding `ReferencePolicy`. + + Proceed with the [standard-upgrade](#standard-upgrade). + +## Upgrade to v0.2.0 + +Consul API Gateway v0.2.0 introduces a change for people upgrading from Consul API Gateway v0.1.0. Routes with a `backendRef` defined in a different namespace now require a [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) that explicitly allows traffic from the route's namespace to the `backendRef`'s namespace. + +### Requirements Ensure that the following requirements are met prior to upgrading: @@ -26,9 +181,7 @@ Ensure that the following requirements are met prior to upgrading: - `ReferencePolicy.create` - (Optional) The [jq](https://stedolan.github.io/jq/download/) command line processor for JSON can be installed, which will ease route retrieval during the upgrade process. -## Procedure - --> **NOTE** When you see `VERSION` in examples of commands or configuration settings, replace `VERSION` with the version number of the release you are installing, like `0.2.0`. If there is a lower case "v" in front of `VERSION` the version number needs to follow the "v" as is `v0.2.0` +### Procedure 1. Verify the current version of the `consul-api-gateway-controller` `Deployment`: @@ -99,7 +252,7 @@ Ensure that the following requirements are met prior to upgrading: 1. Inspect the `backendRefs` entries for each of the routes. - If a `namespace` field is not defined in the `backendRef` or if the namespace matches the namespace of the route, then no additional action is required for the `backendRef`. Otherwise, note the `group`, `kind`, `name`, and `namespace` field values for `backendRef` configurations that have a `namespace` defined that do not match the namespace of the parent route. You must also note the `kind` and `namespace` of the parent route. You will need these to create a `ReferencePolicy` that explicitly allows each cross-namespace route-to-service pair to prevent the route from breaking (see [step 5](#create-reference-policy)). + If a `namespace` field is not defined in the `backendRef` or if the namespace matches the namespace of the route, then no additional action is required for the `backendRef`. Otherwise, note the `group`, `kind`, `name`, and `namespace` field values for `backendRef` configurations that have a `namespace` defined that do not match the namespace of the parent route. You must also note the `kind` and `namespace` of the parent route. You will need these to create a `ReferencePolicy` that explicitly allows each cross-namespace route-to-service pair (see [step 5](#create-reference-policy)). After completing this step, you will have a list of all routes similar to the following: @@ -127,7 +280,7 @@ Ensure that the following requirements are met prior to upgrading: - Skip to [step 8](#step-8) if your list is empty. + Proceed with [standard-upgrade](#standard-upgrade) if your list is empty. 1. If you have installed [`jq`](https://stedolan.github.io/jq/), issue the following command to get all `HTTPRoutes` and `TCPRoutes` and filter for routes that require a `ReferencePolicy`. @@ -173,11 +326,11 @@ Ensure that the following requirements are met prior to upgrading: - If your output is empty, skip to [step 8](#step-8). + If your output is empty, proceed with the [standard-upgrade](#standard-upgrade). 1. Using the list of routes you created earlier as a guide, create a [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) to allow cross namespace traffic for each route service pair. - The `ReferencePolicy` explicitly allows each cross-namespace route to service pair to prevent the route from breaking. The `ReferencePolicy` must be created in the same `namespace` as the backend `Service`. + The `ReferencePolicy` explicitly allows each cross-namespace route to service pair. The `ReferencePolicy` must be created in the same `namespace` as the backend `Service`. Skip to the next step if you've already created a `ReferencePolicy`. **v1.11.0+:** This config entry is supported in Consul Enterprise versions 1.11.0+. +-> **v1.11.0+:** This config entry is supported in Consul versions 1.11.0+. ## Introduction -You can configure Consul to export services contained in an admin partition to one or more additional partitions by declaring the `exported-services` configuration entry in the `kind` field. This enables you to route traffic between services in different clusters that share a single set of Consul servers. +To configure Consul to export services contained in a Consul Enterprise admin partition or Consul OSS datacenter to one or more additional clusters, create a new configuration entry and declare `exported-services` in the `kind` field. This configuration entry enables you to route traffic between services in different clusters. -You can configure the settings defined in the `exported-services` configuration entry to apply to all namespaces and federated datacenters. +You can configure the settings defined in the `exported-services` configuration entry to apply to all namespaces in a Consul Enterprise admin partition. ## Requirements -- A Consul Enterprise binary -- A corresponding partition that the configuration entry can export to. For example, the `exported-services` configuration entry for a partition named `frontend` requires an existing `frontend` partition. +- A 1.11.0+ Consul Enteprise binary or a 1.13.0+ Consul OSS binary. +- **Enterprise Only**: A corresponding partition that the configuration entry can export from. For example, the `exported-services` configuration entry for a partition named `frontend` requires an existing `frontend` partition. ## Usage @@ -37,6 +35,56 @@ You can configure the settings defined in the `exported-services` configuration Configure the following parameters to define a `exported-services` configuration entry: + + + + +```hcl +Kind = "exported-services" +Name = "default" +Services = [ + { + Name = "" + Consumers = [ + { + PeerName = "" + } + ] + } +] +``` + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: default +spec: + services: + - name: + consumers: + - peerName: +``` + +```json +"Kind": "exported-services", +"Name": "default", +"Services": [ + { + "Name": "", + "Consumers": [ + { + "PeerName": "" + } + ] + } +] +``` + + + + + ```hcl @@ -49,8 +97,8 @@ Services = [ Namespace = "" Consumers = [ { - Partition = "" - }, + PeerName = "" + } ] } ] @@ -66,7 +114,7 @@ spec: - name: namespace: consumers: - - partition: + - peerName: ``` ```json @@ -75,18 +123,73 @@ spec: "Name": "", "Services": [ { - "Consumers": [ - { - "Partition": "" - } - ], "Name": "", "Namespace": "" + "Consumers": [ + { + "PeerName": "" + } + ] } ] ``` + + + + + +```hcl +Kind = "exported-services" +Partition = "" +Name = "" +Services = [ + { + Name = "" + Namespace = "" + Consumers = [ + { + Partition = "" + } + ] + } +] +``` + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: +spec: + services: + - name: + namespace: + consumers: + - partition: +``` + +```json +"Kind": "exported-services", +"Partition": "", +"Name": "", +"Services": [ + { + "Name": "", + "Namespace": "" + "Consumers": [ + { + "Partition": "" + } + ] + } +] +``` + + + + ### Configuration Parameters @@ -94,25 +197,113 @@ The following table describes the parameters associated with the `exported-servi | Parameter | Description | Required | Default | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- | -| `Kind` | String value that enables the configuration entry. The value should always be `exported-services` (HCL and JSON) or `ExportedServices` (YAML) | Required | None | -| `Partition` | String value that specifies the name of the partition that contains the services you want to export. | Required | None | -| `Name` | String value that specifies the name of the partition that contains the services you want to export. | Required | None | -| `Services` | List of objects that specify which services to export. See [`Services`](#services) for details. | Required | None | -| `Meta` | Object that defines a map of the max 64 key/value pairs. | Optional | None | +| `Kind` | String value that enables the configuration entry. The value should always be `exported-services` (HCL and JSON) or `ExportedServices` (YAML) | Required | None | +| `Partition` | String value that specifies the name of the partition that contains the services you want to export. | Required | None | +| `Name` | String value that specifies the name of the partition that contains the services you want to export. Must be `default` in Consul OSS. | Required | None | +| `Services` | List of objects that specify which services to export. For details, refer to [`Services`](#services). | Required | None | +| `Meta` | Object that defines a map of the max 64 key/value pairs. | Optional | None | ### Services -The `Services` parameter contains one or more lists of parameters that specify which services to export, which namespaces the services reside, and the destination partition for the exported services. Each list in the `Services` block must contain the following parameters: +The `Services` parameter contains a list of one or more parameters that specify which services to export, which namespaces the services reside, and the destination cluster for the exported services. Each item in the `Services` list must contain the following parameters: -- `Name`: Specifies the name of the service to export. You can use a asterisk wildcard (`*`) to include all services in the namespace. -- `Namespace`: Specifies the namespace containing the services to export. You can use a asterisk wildcard (`*`) to include all namespaces in the partition. -- `Consumers`: Specifies one ore more objects that identify a destination partition for the exported services. +- `Name`: Specifies the name of the service to export. You can use an asterisk wildcard (`*`) to include all services in the namespace. +- `Namespace`: Specifies the namespace containing the services to export. You can use an asterisk wildcard (`*`) to include all namespaces in the partition. +- `Consumers`: Specifies one or more objects that identify a destination cluster for the exported services. -## Example +### Consumers -The following example configures the agent to export the `billing` service from the `default` namespace of the `finance` admin partition to the `frontend` and `backend` partitions. Additionally, all services in all namespaces within the `finance` partition will be exported to the `monitoring` partition. +The `Consumers` parameter contains a list of one or more parameters that specify the destination cluster for +an exported service. Each item in the `Consumers` list must contain exactly one of the following parameters: - +- `PeerName`: Specifies the name of the peered cluster to export the service to. +A asterisk wildcard (`*`) cannot be specified as the `PeerName`. Added in Consul 1.13.0. +- `Partition`: Specifies an admin partition in the datacenter to export the service to. +A asterisk wildcard (`*`) cannot be specified as the `Partition`. + +## Examples + + +### Exporting services to peered clusters + + + + + +The following example configures Consul to export the `payments` and `refunds` services to the peered `web-shop` cluster. + + + +```hcl +Kind = "exported-services" +Name = "default" + +Services = [ + { + Name = "payments" + Consumers = [ + { + PeerName = "web-shop" + }, + ] + }, + { + Name = "refunds" + Consumers = [ + { + PeerName = "web-shop" + } + ] + } +] +``` + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +Kind: ExportedServices +metadata: + name: default +spec: + services: + - name: payments + consumers: + - peerName: web-shop + - name: refunds + consumers: + - peerName: web-shop +``` + +```json +"Kind": "exported-services", + "Name": "default", + "Services": [ + { + "Name": "payments", + "Consumers": [ + { + "PeerName": "web-shop" + }, + ], + }, + { + "Name": "refunds", + "Consumers": [ + { + "PeerName": "web-shop" + } + ] + } + ] +``` + + + + + + +The following example configures Consul to export the `payments` and `refunds` services from the `billing` namespace of the `finance` admin partition to the `web-shop` peer. + + ```hcl Kind = "exported-services" @@ -121,23 +312,20 @@ Name = "finance" Services = [ { - Name = "billing" - Namespace = "default" + Name = "payments" + Namespace = "billing" Consumers = [ { - Partition = "frontend" + PeerName = "web-shop" }, - { - Partition = "backend" - } ] }, { - Name = "*" - Namespace = "*" + Name = "refunds" + Namespace = "billing" Consumers = [ { - Partition = "monitoring" + PeerName = "web-shop" } ] } @@ -151,15 +339,14 @@ metadata: name: finance spec: services: - - name: mesh-gateway - namespace: default + - name: payments + namespace: billing consumers: - - partition: default - - name: billing - namespace: default + - peerName: web-shop + - name: refunds + namespace: billing consumers: - - partition: frontend - - partition: backend + - peerName: web-shop ``` ```json @@ -168,41 +355,327 @@ spec: "Name": "finance", "Services": [ { + "Name": "payments", + "Namespace": "billing" "Consumers": [ { - "Partition": "frontend" + "PeerName": "web-shop" }, - { - "Partition": "backend" - } ], - "Name": "billing", - "Namespace": "default" }, { + "Name": "refunds", + "Namespace": "billing", "Consumers": [ { - "Partition": "monitoring" + "PeerName": "web-shop" } - ], - "Name": "*", - "Namespace": "*" + ] } ] ``` + + + + +The following example configures Consul to export the `payments` and `refunds` services from the `billing` namespace of the `finance` admin partition to the `web-shop` partition. + + + +```hcl +Kind = "exported-services" +Partition = "finance" +Name = "finance" + +Services = [ + { + Name = "payments" + Namespace = "billing" + Consumers = [ + { + Partition = "web-shop" + }, + ] + }, + { + Name = "refunds" + Namespace = "billing" + Consumers = [ + { + Partition = "web-shop" + } + ] + } +] +``` + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +Kind: ExportedServices +metadata: + name: finance +spec: + services: + - name: payments + namespace: billing + consumers: + - partition: web-shop + - name: refunds + namespace: billing + consumers: + - partition: web-shop +``` + +```json +"Kind": "exported-services", + "Partition": "finance", + "Name": "finance", + "Services": [ + { + "Name": "payments", + "Namespace": "billing" + "Consumers": [ + { + "Partition": "web-shop" + }, + ], + }, + { + "Name": "refunds", + "Namespace": "billing", + "Consumers": [ + { + "Partition": "web-shop" + } + ] + } + ] +``` + + + + + +### Exporting all services + + + + +The following example configures Consul to export all services in the datacenter to the peered `monitoring` and `platform` clusters. + + + +```hcl +Kind = "exported-services" +Name = "default" + +Services = [ + { + Name = "*" + Consumers = [ + { + PeerName = "monitoring" + }, + { + PeerName = "platform" + } + ] + } +] +``` + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +Kind: ExportedServices +metadata: + name: default +spec: + services: + - name: * + consumers: + - peerName: monitoring + - peerName: platform +``` + +```json +"Kind": "exported-services", + "Name": "default", + "Services": [ + { + "Name": "*", + "Namespace": "*" + "Consumers": [ + { + "PeerName": "monitoring" + }, + { + "PeerName": "platform" + } + ] + } + ] +``` + + + + + + +The following example configures Consul to export all services in all namespaces of the `finance` partition to the peered `monitoring` and `platform` clusters. + + + +```hcl +Kind = "exported-services" +Partition = "finance" +Name = "finance" + +Services = [ + { + Name = "*" + Namespace = "*" + Consumers = [ + { + PeerName = "monitoring" + }, + { + PeerName = "platform" + } + ] + } +] +``` + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +Kind: ExportedServices +metadata: + name: finance +spec: + services: + - name: * + namespace: * + consumers: + - peerName: monitoring + - peerName: platform +``` + +```json +"Kind": "exported-services", + "Partition": "finance", + "Name": "finance", + "Services": [ + { + "Name": "*", + "Namespace": "*" + "Consumers": [ + { + "PeerName": "monitoring" + }, + { + "PeerName": "platform" + } + ] + } + ] +``` + + + + + + +The following example configures Consul to export all services in all namespaces of the `finance` partition to the `monitoring` and `platform` partitions. + + + +```hcl +Kind = "exported-services" +Partition = "finance" +Name = "finance" + +Services = [ + { + Name = "*" + Namespace = "*" + Consumers = [ + { + Partition = "monitoring" + }, + { + Partition = "platform" + } + ] + } +] +``` + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +Kind: ExportedServices +metadata: + name: finance +spec: + services: + - name: * + namespace: * + consumers: + - partition: monitoring + - partition: platform +``` + +```json +"Kind": "exported-services", + "Partition": "finance", + "Name": "finance", + "Services": [ + { + "Name": "*", + "Namespace": "*" + "Consumers": [ + { + "Partition": "monitoring" + }, + { + "Partition": "platform" + } + ] + } + ] +``` + + + + ## Reading Services -When an exported service has been imported to another partition, you can use the `health` REST API endpoint to query the service on the consumer partition. The following example queries the `finance` partition for the imported `billing` service: +When an exported service has been imported to another cluster, you can use the `health` REST API endpoint to query the service on the consumer cluster. + + + + +The following example queries the `finance` peer for the imported `payments` service: ```shell-session -$ curl 'localhost:8500/v1/health/connect/billing?partition=finance' +$ curl 'localhost:8500/v1/health/service/payments?peer=finance' ``` + -An ACL token with `service:write` permissions is required for the partition from which the query is made. If the call in the previous example is made from a service named `web` in a partition named `frontend`, then the request will require a token with `write` permissions to `web` in the `frontend` partition. + -Exports are available to all services in the consumer partition. In the previous example, any service with `write` permissions for the `frontend` partition will be able to read exports. +The following example queries the `finance` partition for the imported `payments` service: -See [Health HTTP Endpoint](/api-docs/health) for additional information. +```shell-session +$ curl 'localhost:8500/v1/health/service/payments?partition=finance' +``` + + + +An ACL token with `service:write` permissions is required for the cluster the query is made from. If the call in the previous example is made from a service named `web` in a partition named `frontend`, then the request requires a token with `write` permissions to `web` in the `frontend` partition. + +Exports are available to all services in the consumer cluster. In the previous example, any service with `write` permissions for the `frontend` partition can read exports. + +For additional information, refer to [Health HTTP Endpoint](/api-docs/health). diff --git a/website/content/docs/connect/config-entries/service-intentions.mdx b/website/content/docs/connect/config-entries/service-intentions.mdx index b73eb61fb..c6dd14de9 100644 --- a/website/content/docs/connect/config-entries/service-intentions.mdx +++ b/website/content/docs/connect/config-entries/service-intentions.mdx @@ -446,27 +446,37 @@ spec: }, }, { - name: 'Namespace', - enterprise: true, - type: 'string', + name: 'Peer', + type: 'string: ""', description: { hcl: - "The namespace of the source service. Defaults to the namespace of the destination service (i.e. the config entry's namespace)", + "Specifies the [peer](/docs/connect/cluster-peering/index.mdx) of the source service. `Peer` is mutually exclusive with `Partition`.", yaml: - 'The namespace of the source service. Defaults to the namespace of the destination service (i.e. `spec.destination.namespace`)', + "Specifies the [peer](/docs/connect/cluster-peering/index.mdx) of the source service. `peer` is mutually exclusive with `partition`.", + }, + }, + { + name: 'Namespace', + enterprise: true, + type: 'string: ""', + description: { + hcl: + "The namespace of the source service. If `Peer` is empty, `Namespace` defaults to the namespace of the destination service (i.e. the config entry's namespace).", + yaml: + 'The namespace of the source service. If `peer` is empty, `namespace` defaults to the namespace of the destination service (i.e. `spec.destination.namespace`).', }, }, { name: 'Partition', enterprise: true, - type: 'string', + type: 'string: ""', description: { hcl: - "Specifies the admin partition of the source service. Defaults to the destination service's partition, i.e., the configuration entry's partition", + "Specifies the admin partition of the source service. If `Peer` is empty, `Partition` defaults to the destination service's partition (i.e. the configuration entry's partition). `Partition` is mutually exclusive with `Peer`.", yaml: - "Specifies the admin partition of the source service. Defaults to the destination service's partition, i.e. `spec.destination.partition`", + "Specifies the admin partition of the source service. If `peer` is empty, `partition` defaults to the destination service's partition (i.e. `spec.destination.partition`). `partition` is mutually exclusive with `peer`.", }, - }, + }, { name: 'Action', type: 'string: ""', @@ -501,7 +511,7 @@ spec: intention behavior is defined by the default [ACL policy](/docs/agent/config/config-files#acl_default_policy).

    This should be omitted for an L4 intention as it is mutually exclusive with the \`action\` field.

    - Setting \`permissions\` is not valid if a wildcard is used for the \`spec.destination.name\` or \`spec.destination.namespace\` + Setting \`permissions\` is not valid if a wildcard is used for the \`spec.destination.name\` or \`spec.destination.namespace\` because they can only be applied to services with a compatible protocol.`, }, }, diff --git a/website/content/docs/connect/registration/service-registration.mdx b/website/content/docs/connect/registration/service-registration.mdx index 3deac820e..66b1aa96f 100644 --- a/website/content/docs/connect/registration/service-registration.mdx +++ b/website/content/docs/connect/registration/service-registration.mdx @@ -35,13 +35,13 @@ port = "destination_service_name": "", "" : "" }, - "port": + "port": } ```
    -The following table describes the parameters that must be added to the service definition to declare the service as a proxy. +The following table describes the parameters that must be added to the service definition to declare the service as a proxy. | Parameter | Description | Required | Default | | --- | --- | --- | --- | @@ -82,8 +82,8 @@ proxy = { ### Sidecar Proxy Configuration -Many service mesh proxies are deployed as sidecars. -Sidecar proxies are co-located with the single service instance they represent and proxy all inbound traffic to. +Many service mesh proxies are deployed as sidecars. +Sidecar proxies are co-located with the single service instance they represent and proxy all inbound traffic to. Specify the following parameters in the `proxy` code block to configure a sidecar proxy in its own service registration: @@ -99,7 +99,7 @@ The following example includes values for all available options when registering -```hcl +```hcl kind = "connect-proxy" name = "redis-proxy" port = 8181 @@ -166,6 +166,7 @@ You can configure the service mesh proxy to create listeners for upstream servic | --- | --- | --- | --- | |`destination_name` | String value that specifies the name of the service or prepared query to route the service mesh to. The prepared query should be the name or the ID of the prepared query. | Required | None | | `destination_namespace` | String value that specifies the namespace containing the upstream service. | Optional | `default` | +| `destination_peer` | String value that specifies the name of the peer cluster containing the upstream service. | Optional | None | | `destination_partition` | String value that specifies the name of the admin partition containing the upstream service. | Optional | `default` | | `local_bind_port` | Integer value that specifies the port to bind a local listener to. The application will make outbound connections to the upstream from the local port. | Required | None | | `local_bind_address` | String value that specifies the address to bind a local listener to. The application will make outbound connections to the upstream service from the local bind address. | Optional | `127.0.0.1` | @@ -265,7 +266,7 @@ You can configure which mode a proxy operates in by specifying `"direct"` or `"t * `transparent`: In this mode, inbound and outbound application traffic is captured and redirected through the proxy. This mode does not enable the traffic redirection. It directs Consul to configure Envoy as if traffic is already being redirected. * `direct`: In this mode, the proxy's listeners must be dialed directly by the local application and other proxies. -You can also specify an empty string (`""`), which configures the proxy to operate in the default mode. The default mode is inherited from parent parameters in the following order of precedence: +You can also specify an empty string (`""`), which configures the proxy to operate in the default mode. The default mode is inherited from parent parameters in the following order of precedence: 1. Proxy service's `Proxy` configuration 1. The `service-defaults` configuration for the service. @@ -294,12 +295,12 @@ registrations](/docs/discovery/services#service-definition-parameter-case). - `outbound_listener_port` `(int: 15001)` - The port the proxy should listen on for outbound traffic. This must be the port where outbound application traffic is captured and redirected to. - `dialed_directly` `(bool: false)` - Determines whether this proxy instance's IP address can be dialed - directly by transparent proxies. Typically transparent proxies dial upstreams using the "virtual" - tagged address, which load balances across instances. Dialing individual instances can be helpful - in cases like stateful services such as a database cluster with a leader. + directly by transparent proxies. Transparent proxies typically dial upstreams using the "virtual" + tagged address, which load balances across instances. A database cluster with a leader is an example + where dialing individual instances can be helpful. ~> **Note:** Dynamic routing rules such as failovers and redirects do not apply to services dialed directly. - Additionally, the connection is proxied using a TCP proxy with a connection timeout of 5 seconds. + Additionally, the connection is proxied using a TCP proxy with a connection timeout of 5 seconds. ### Mesh Gateway Configuration Reference diff --git a/website/content/docs/discovery/dns.mdx b/website/content/docs/discovery/dns.mdx index 8a542b4ef..17c25e6e6 100644 --- a/website/content/docs/discovery/dns.mdx +++ b/website/content/docs/discovery/dns.mdx @@ -469,7 +469,9 @@ options. ## Namespaced/Partitioned Services and Nodes -Consul Enterprise supports resolving namespaced and partitioned services and nodes via DNS. +Consul Enterprise supports resolving namespaced and partitioned services via DNS. +The DNS server in Consul Enterprise can resolve services assigned to namespaces and partitions. +The DNS server can also resolve nodes assigned to partitions. To maintain backwards compatibility existing queries can be used and these will resolve services within the `default` namespace and partition. However, for resolving services from other namespaces or partitions the following form can be used: @@ -478,12 +480,18 @@ services from other namespaces or partitions the following form can be used: [tag.].service..ns..ap..dc. ``` -This is the canonical name of a Consul Enterprise service. Currently at least 2 of -`[namespace, partition, datacenter]` must be present - in a future version (once the -[`prefer_namespace` configuration](/docs/agent/config/config-files#dns_prefer_namespace) has been -deprecated), the namespace, partition and datacenter components will all become optional -and may be individually omitted to default to the `default` namespace, local partition -or local datacenter respectively. +This sequence is the canonical naming convention of a Consul Enterprise service. At least two of the following +fields must be present: +* `namespace` +* `partition` +* `datacenter` + +For node lookups, only the partition and datacenter need to be specified as nodes cannot be +namespaced. + +```text +[tag.].service..ap..dc. +``` For node lookups, only the partition and datacenter need to be specified (nodes cannot be namespaced): diff --git a/website/content/docs/download-tools.mdx b/website/content/docs/download-tools.mdx index 16fca097f..51da01366 100644 --- a/website/content/docs/download-tools.mdx +++ b/website/content/docs/download-tools.mdx @@ -55,6 +55,7 @@ These Consul tools are created and managed by the amazing members of the Consul - [Gonsul](https://github.com/miniclip/gonsul) - A Git to Consul standalone tool made in Go. Updates Consul KV from a repo with multiple strategies. - [gradle-consul-plugin](https://github.com/amirkibbar/red-apple) - A Consul Gradle plugin - [hashi-ui](https://github.com/jippi/hashi-ui) - A modern user interface for the Consul and Nomad +- [HashiBox](https://github.com/nunchistudio/hashibox) - Vagrant environment to simulate highly-available cloud with Consul, Nomad, Vault, and optional support for Waypoint. OSS & Enterprise supported. - [helios-consul](https://github.com/SVT/helios-consul) - Service registrar plugin for Helios - [Jenkins Consul Plugin](https://plugins.jenkins.io/consul) - Jenkins plugin for service discovery and K/V store - [marathon-consul](https://github.com/allegro/marathon-consul) - Service registry bridge for Marathon diff --git a/website/content/docs/ecs/architecture.mdx b/website/content/docs/ecs/architecture.mdx index 83e46783d..e09223eaa 100644 --- a/website/content/docs/ecs/architecture.mdx +++ b/website/content/docs/ecs/architecture.mdx @@ -23,8 +23,10 @@ The following diagram shows the main components of the Consul architecture when for Consul and Envoy. 1. **Health Syncing:** Optionally, an additional `health-sync` container can be included in a task to sync health statuses from ECS into Consul. -1. **ACL Controller:** Automatically provisions Consul ACL tokens for Consul clients and service mesh services - in an ECS Cluster. +1. **ACL Controller:** The ACL controller is responsible for automating configuration and cleanup in the Consul servers. + The ACL controller will automatically configure the [AWS IAM Auth Method](/docs/security/acl/auth-methods/aws-iam), and cleanup + unused ACL tokens from Consul. When using Consul Enterprise namespaces, the ACL controller will automatically create Consul + namespaces for ECS tasks. For more information about how Consul works in general, see Consul's [Architecture Overview](/docs/architecture). @@ -39,15 +41,27 @@ This diagram shows the timeline of a task starting up and all its containers: - **T0:** ECS starts the task. The `consul-client` and `mesh-init` containers start: - - `consul-client` uses the `retry-join` option to join the Consul cluster - - `mesh-init` registers the service for the current task and its sidecar proxy with - Consul. It runs `consul connect envoy -bootstrap` to generate Envoy’s - bootstrap JSON file and write it to a shared volume. `mesh-init` exits after completing these operations. - + - `consul-client` does the following: + - If ACLs are enabled, a startup script runs a `consul login` command to obtain a + token from the AWS IAM auth method for the Consul client. This token has `node:write` + permissions. + - It uses the `retry-join` option to join the Consul cluster. + - `mesh-init` does the following: + - If ACLs are enabled, mesh-init runs a `consul login` command to obtain a token from + the AWS IAM auth method for the service registration. This token has `service:write` + permissions for the service and its sidecar proxy. This token is written to a shared + volume for use by the `health-sync` container. + - It registers the service for the current task and its sidecar proxy with Consul. + - It runs `consul connect envoy -bootstrap` to generate Envoy’s bootstrap JSON file and + writes it to a shared volume. - **T1:** The following containers start: - - The `sidecar-proxy` container starts and runs Envoy by executing `envoy -c `. - - If applicable, the `health-sync` container syncs health checks from ECS to Consul (see [ECS Health Check Syncing](#ecs-health-check-syncing)). -- **T2:** The `sidecar-proxy` container is marked as healthy by ECS. It uses a health check that detects if its public listener port is open. At this time, your application containers are started since all Consul machinery is ready to service requests. The only running containers are `consul-client`, `sidecar-proxy`, and your application container(s). + - `sidecar-proxy` starts using a custom entrypoint command, `consul-ecs envoy-entrypoint`. + The entrypoint command starts Envoy by running `envoy -c `. + - `health-sync` starts if ECS health checks are defined or if ACLs are enabled. It syncs health + checks from ECS to Consul (see [ECS Health Check Syncing](#ecs-health-check-syncing)). +- **T2:** The `sidecar-proxy` container is marked as healthy by ECS. It uses a health check that + detects if its public listener port is open. At this time, your application containers are started + since all Consul machinery is ready to service requests. ## Task Shutdown @@ -62,10 +76,19 @@ This diagram shows an example timeline of a task shutting down: - **T0**: ECS sends a TERM signal to all containers. Each container reacts to the TERM signal: - `consul-client` begins to gracefully leave the Consul cluster. - `health-sync` stops syncing health status from ECS into Consul checks. - - `sidecar-proxy` ignores the TERM signal and continues running until the `user-app` container exits. This allows the application container to continue making outgoing requests through the proxy to the mesh. - - `user-app` exits if it is not configured to ignore the TERM signal. The `user-app` container will continue running if it is configured to ignore the TERM signal. + - `sidecar-proxy` ignores the TERM signal and continues running until the `user-app` container + exits. The custom entrypoint command, `consul-ecs envoy-entrypoint`, monitors the local ECS task + metadata. It waits until the `user-app` container has exited before terminating Envoy. This + enables the application to continue making outgoing requests through the proxy to the mesh for + graceful shutdown. + - `user-app` exits if it is not configured to ignore the TERM signal. The `user-app` container + will continue running if it is configured to ignore the TERM signal. - **T1**: - - `health-sync` updates its Consul checks to critical status and exits. This ensures this service instance is marked unhealthy. + - `health-sync` does the following: + - It updates its Consul checks to critical status and exits. This ensures this service instance is marked unhealthy. + - If ACLs are enabled, it runs `consul logout` for the two tokens created by the `consul-client` and `mesh-init` containers. + This removes those tokens from Consul. If `consul logout` fails for some reason, the ACL controller will remove the tokens + after the task has stopped. - `sidecar-proxy` notices the `user-app` container has stopped and exits. - **T2**: `consul-client` finishes gracefully leaving the Consul datacenter and exits. - **T3**: @@ -73,45 +96,154 @@ This diagram shows an example timeline of a task shutting down: - Updates about this task have reached the rest of the Consul cluster, so downstream proxies have been updated to stopped sending traffic to this task. - **T4**: At this point task shutdown should be complete. Otherwise, ECS will send a KILL signal to any containers still running. The KILL signal cannot be ignored and will forcefully stop containers. This will interrupt in-progress operations and possibly cause errors. +## ACL Tokens + +Two types of ACL tokens are required by ECS tasks: + +* **Client tokens:** used by the `consul-client` containers to join the Consul cluster +* **Service tokens:** used by sidecar containers for service registration and health syncing + +With Consul on ECS, these tokens are obtained dynamically when a task starts up by logging +in via Consul's AWS IAM auth method. + +### Consul Client Token + +Consul client tokens require `node:write` for any node name, which is necessary because the Consul node +names on ECS are not known until runtime. + +### Service Token + +Service tokens are associated with a [service identity](/docs/security/acl#service-identities). +The service identity includes `service:write` permissions for the service and sidecar proxy. + +## AWS IAM Auth Method + +Consul's [AWS IAM Auth Method](/docs/security/acl/auth-methods/aws-iam) is used by ECS tasks to +automatically obtain Consul ACL tokens. When a service mesh task on ECS starts up, it runs two +`consul login` commands to obtain a client token and a service token via the auth method. When the +task stops, it attempts two `consul logout` commands in order to destroy these tokens. + +During a `consul login`, the [task's IAM +role](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) is presented +to the AWS IAM auth method on the Consul servers. The role is validated with AWS. If the role is +valid, and if the auth method trusts the IAM role, then the role is permitted to login. A new Consul +ACL token is created and [Binding Rules](/docs/security/acl/auth-methods#binding-rules) associate +permissions with the newly created token. These permissions are mapped to the token based on the IAM +role details. For example, tags on the IAM role are used to specify the service name and the +Consul Enterprise namespace to be associated with a service token that is created by a successful +login to the auth method. + +### Task IAM Role + +The following configuration is required for the task IAM role in order to be compatible with the +auth method. When using Terraform, the `mesh-task` module creates the task role with this +configuration by default. + +* A scoped `iam:GetRole` permission must be included on the IAM role, enabling the role to fetch + details about itself. +* A `consul.hashicorp.com.service-name` tag on the IAM role must be set to the Consul service name. +* A consul.hashicorp.com.namespace tag must be set on the + IAM role to the Consul Enterprise namespace of the Consul service for the task. + +Task IAM roles should not typically be shared across task families. Since a task family represents a +single Consul service, and since the task role must include the Consul service name, one task role +is required for each task family when using the auth method. + +### Security + +The auth method relies on the configuration of AWS resources, such as IAM roles, IAM policies, and +ECS tasks. If these AWS resources are misconfigured or if the account has loose access controls, +then the security of your service mesh may be at risk. + +Any entity in your AWS account with the ability to obtain credentials for an IAM role could potentially +obtain a Consul ACL token and impersonate a Consul service. The `mesh-task` Terraform module +mitigates against this concern by creating the task role with an `AssumeRolePolicyDocument` that +allows only the AWS ECS service to assume the task role. By default, other entities are unable +to obtain credentials for task roles, and are unable to abuse the AWS IAM auth method to obtain +Consul ACL tokens. + +However, other entities in your AWS account with the ability to create or modify IAM roles can +potentially circumvent this. For example, if they are able to create an IAM role with the correct +tags, they can obtain a Consul ACL token for any service. Or, if they can pass a role to an ECS task +and start an ECS task, they can use the task to obtain a Consul ACL token via the auth method. + +The IAM policy actions `iam:CreateRole`, `iam:TagRole`, `iam:PassRole`, and `sts:AssumeRole` can be +used to restrict these capabilities in your AWS account and improve security when using the AWS IAM +auth method. See the [AWS +documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html) to learn how +to restrict these permissions in your AWS account. + ## ACL Controller -The ACL controller performs the following operations: -* Provisions Consul ACL tokens for Consul clients and service mesh services. -* Manages Consul admin partitions and namespaces. +The ACL controller performs the following operations on the Consul servers: -### Automatic ACL Token Provisioning +* Configures the Consul AWS IAM auth method. +* Monitors tasks in ECS cluster where the controller is running. +* Cleans up unused Consul ACL tokens created by tasks in this cluster. +* Manages Consul admin partitions and namespaces. -Consul ACL tokens secure communication between agents and services. -The following containers in a task require an ACL token: +### Auth Method Configuration -- `consul-client`: The Consul client uses a token to authorize itself with Consul servers. - All `consul-client` containers share the same token. -- `mesh-init`: The `mesh-init` container uses a token to register the service with Consul. - This token is unique for the Consul service, and is shared by instances of the service. +The ACL controller is responsible for configuring the AWS IAM auth method. The following resources +are created by the ACL controller when it starts up: -The ACL controller automatically creates ACL tokens for mesh-enabled tasks in an ECS cluster. -The `acl-controller` Terraform module creates the ACL controller task. The controller creates the -ACL token used by `consul-client` containers at startup and then watches for tasks in the cluster. It checks tags -to determine if the task is mesh-enabled. If so, it creates the service ACL token for the task, if the -token does not yet exist. +* **Client role**: The controller creates the Consul (not IAM) role and policy used for client + tokens if these do not exist. This policy has `node:write` permissions to enable Consul clients to + join the Consul cluster. +* **Auth method for client tokens**: One instance of the AWS IAM auth method is created for client + tokens, if it does not exist. A binding rule is configured that attaches the Consul client role to each + token created during a successful login to this auth method instance. +* **Auth method for service tokens**: One instance of the AWS IAM auth method is created for service + tokens, if it does not exist: + * A binding rule is configured to attach a [service identity](/docs/security/acl#service-identities) + to each token created during a successful login to this auth method instance. The service name for + this service identity is taken from the tag, `consul.hashicorp.com.service-name`, on the IAM role + used to log in. + * A namespace binding rule is configured to create service tokens in + the namespace specified by the tag, consul.hashicorp.com.namespace, on the IAM + role used to log in. -The ACL controller stores all ACL tokens in AWS Secrets Manager, and tasks are configured to pull these -tokens from AWS Secrets Manager when they start. +The ACL controller configures both instances of the auth method to permit only certain IAM roles to login, +by setting the [`BoundIAMPrincipalARNs`](/docs/security/acl/auth-methods/aws-iam#boundiamprincipalarns) +field of the AWS IAM auth method as follows: + +* By default, the only IAM roles permitted to log in must have an ARN matching the pattern, + `arn:aws:iam:::role/consul-ecs/*`. This allows IAM roles at the role path `/consul-ecs/` + to log in, and only those IAM roles in the same AWS account where the ACL controller is running. +* The role path can be changed by setting the `iam_role_path` input variable for the `mesh-task` and + `acl-controller` modules, or by passing the `-iam-role-path` flag to the `consul-ecs + acl-controller` command. +* Each instance of the auth method is shared by ACL controllers in the same Consul datacenter. Each + controller updates the auth method, if necessary, to include additional entries in the + `BoundIAMPrincipalARNs` list. This enables the use of the auth method with ECS clusters in + different AWS accounts, for example. This does not apply when using Consul Enterprise admin + partitions because auth method instances are not shared by multiple controllers in that case. + +### Task Monitoring + +After startup, the ACL controller monitors tasks in the same ECS cluster where the ACL controller is +running in order to discover newly running tasks and tasks that have stopped. + +The ACL controller cleans up tokens created by `consul login` for tasks that are no longer running. +Normally, each task attempts `consul logout` commands when the task stops to destroy its tokens. +However, in unstable conditions the `consul logout` command may fail to clean up a token. +The ACL controller runs continually to ensure those unused tokens are soon removed. ### Admin Partitions and Namespaces When [admin partitions and namespaces](/docs/ecs/enterprise#admin-partitions-and-namespaces) are enabled, -the ACL controller is assigned to its configured admin partition. The ACL controller provisions ACL -tokens for tasks in a single admin partition and supports one ACL controller instance per ECS +the ACL controller is assigned to its configured admin partition. It supports one ACL controller instance per ECS cluster. This results in an architecture with one admin partition per ECS cluster. -The ACL controller automatically performs the following actions: -* Creates its admin partition at startup if it does not exist. -* Inspects ECS task tags for the task's intended partition and namespace. - The ACL controller ignores tasks that do not match the `partition` tag. +When admin partitions and namespace are enabled, the ACL controller performs the following +additional actions: + +* At startup, creates its assigned admin partition if it does not exist. +* Inspects task tags for new ECS tasks to discover the task's intended partition + and namespace. The ACL controller ignores tasks with a partition tag that does not match the + controller's assigned partition. * Creates namespaces when tasks start up. Namespaces are only created if they do not exist. -* Provisions ACL tokens and ACL policies that are scoped to the applicable admin partition and namespace. -* Provision ACL tokens that allow services to communicate with upstreams across admin partitions and namespaces. +* Creates auth method instances for client and service tokens in controller's assigned admin partition. ## ECS Health Check Syncing @@ -121,6 +253,5 @@ If the following conditions apply, ECS health checks automatically sync with Con * have ECS `healthChecks` * are not configured with native Consul health checks -The `mesh-init` container creates a TTL health check for -every container that fits these criteria and the `health-sync` container ensures -that the ECS and Consul health checks remain in sync. +The `mesh-init` container creates a TTL health check for every container that fits these criteria +and the `health-sync` container ensures that the ECS and Consul health checks remain in sync. diff --git a/website/content/docs/ecs/configuration-reference.mdx b/website/content/docs/ecs/configuration-reference.mdx index 54b4c2c0f..fe57cab1a 100644 --- a/website/content/docs/ecs/configuration-reference.mdx +++ b/website/content/docs/ecs/configuration-reference.mdx @@ -44,9 +44,109 @@ These are the top-level fields for the Consul ECS configuration format. | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | | `bootstrapDir` | `string` | required | The directory at which to mount the shared volume where Envoy bootstrap configuration is written by `consul-ecs mesh-init`. | +| `consulCACertFile` | `string` | optional | The file path of the Consul server CA certificate. | +| `consulHTTPAddr` | `string` | optional | The HTTP(S) URL of the Consul server. Required when `authMethod.enabled` is set | +| [`consulLogin`](#consullogin) | `object` | optional | Configuration for logging into the AWS IAM auth method. | +| [`gateway`](#gateway) | `object` | optional | Configuration for the gateway proxy registration. | | `healthSyncContainers` | `array` | optional | The names of containers that will have health check status synced from ECS into Consul. Cannot be specified with `service.checks`. | +| `logLevel` | `string` | optional | Sets the log level for the `consul-ecs mesh-init` and `consul-ecs health-sync` commands. Defaults to `INFO`. Must be one of `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, or `null`. | | [`proxy`](#proxy) | `object` | optional | Configuration for the sidecar proxy registration with Consul. | -| [`service`](#service) | `object` | required | Configuration for Consul service registration. | +| [`service`](#service) | `object` | optional | Configuration for Consul service registration. | + +# `consulLogin` + +Configuration for logging into the AWS IAM auth method. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `enabled` | `boolean` | optional | Enables logging into Consul's AWS IAM auth method to obtain an ACL token. The auth method must be configured on the Consul server and the ECS task role must be trusted by the auth method. After logging in, the token is written to the file `/service-token`. | +| `extraLoginFlags` | `array` | optional | Additional CLI flags to pass to the `consul login` command. These are appended to the command `consul login -type aws -method -token-sink-file -aws-auto-bearer-token -aws-include-identity`. | +| `includeEntity` | `boolean` | optional | Adds the `-aws-include-entity` flag to the `consul login` command. Defaults to `true`. Set to `false` to remove the flag from the command. The `-aws-include-entity` flag should only be passed if the Consul AWS IAM auth method is configured with `EnableIAMEntityDetails=true`. | +| `method` | `string` | optional | The name of Consul auth method. This is passed as the `-method` option to the `consul login` command. Defaults to `iam-ecs-service-token`. | + +# `gateway` + +Configuration for the gateway proxy registration. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `kind` | `string` | required | Specifies the type of gateway to register. Must be `mesh-gateway`. | +| [`lanAddress`](#gateway-lanaddress) | `object` | optional | LAN address and port for the gateway. If not specified, defaults to the task/node address. | +| `meta` | `object` | optional | Key-value pairs of metadata to include for the gateway. | +| `name` | `string` | optional | The name the gateway will be registered as in Consul. Defaults to the Task family name. | +| `namespace` | `string` | optional | Consul namespace in which the gateway will be registered. | +| `partition` | `string` | optional | Consul admin partition in which the gateway will be registered. | +| [`proxy`](#gateway-proxy) | `object` | optional | Object that contains the proxy parameters. | +| `tags` | `array` | optional | List of string values that can be used to add labels to the gateway. | +| [`wanAddress`](#gateway-wanaddress) | `object` | optional | WAN address and port for the gateway. If not specified, defaults to the task/node address. | + +# `gateway.lanAddress` + +LAN address and port for the gateway. If not specified, defaults to the task/node address. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `address` | `string` | optional | | +| `port` | `integer` | optional | | + +# `gateway.proxy` + +Object that contains the proxy parameters. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `config` | `object` | optional | | + +# `gateway.wanAddress` + +WAN address and port for the gateway. If not specified, defaults to the task/node address. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `address` | `string` | optional | | +| `port` | `integer` | optional | | + +# `proxy` + +Configuration for the sidecar proxy registration with Consul. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `config` | `object` | optional | Object value that specifies an opaque JSON configuration. The JSON is stored and returned along with the service instance when called from the API. | +| [`meshGateway`](#proxy-meshgateway) | `object` | optional | Specifies the mesh gateway configuration for the proxy. | +| [`upstreams`](#proxy-upstreams) | `array` | optional | The list of the upstream services that the proxy should create listeners for. | + +# `proxy.meshGateway` + +Specifies the mesh gateway configuration for the proxy. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `mode` | `string` | required | Specifies how upstreams with a remote destination datacenter are resolved. Must be one of `none`, `local`, or `remote`. | + +# `proxy.upstreams` + +The list of the upstream services that the proxy should create listeners for. Each `upstream` object may contain the following fields. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `config` | `object` | optional | Specifies opaque configuration options that will be provided to the proxy instance for the upstream. | +| `datacenter` | `string` | optional | Specifies the datacenter to issue the discovery query to. | +| `destinationName` | `string` | required | Specifies the name of the upstream service or prepared query to route the service mesh to. | +| `destinationNamespace` | `string` | optional | Specifies the namespace containing the upstream service. | +| `destinationPartition` | `string` | optional | Specifies the name of the admin partition containing the upstream service. | +| `destinationType` | `string` | optional | Specifies the type of discovery query the proxy should use for finding service mesh instances. Must be one of `service`, `prepared_query`, or `null`. | +| `localBindAddress` | `string` | optional | Specifies the address to bind a local listener to. | +| `localBindPort` | `integer` | required | Specifies the port to bind a local listener to. The application will make outbound connections to the upstream from the local port. | +| [`meshGateway`](#proxy-upstreams-meshgateway) | `object` | optional | Specifies the mesh gateway configuration for the proxy for this upstream. | + +## `proxy.upstreams.meshGateway` + +Specifies the mesh gateway configuration for the proxy for this upstream. + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `mode` | `string` | required | Specifies how the upstream with a remote destination datacenter gets resolved. Must be one of `none`, `local`, or `remote`. | # `service` @@ -58,15 +158,15 @@ Configuration for Consul service registration. | `enableTagOverride` | `boolean` | optional | Determines if the anti-entropy feature for the service is enabled | | `meta` | `object` | optional | Key-value pairs of metadata to include for the Consul service. | | `name` | `string` | optional | The name the service will be registered as in Consul. Defaults to the Task family name if empty or null. | -| `namespace` | `string` | optional | The Consul namespace where the service will be registered [Consul Enterprise]. | -| `partition` | `string` | optional | The Consul admin partition where the service will be registered [Consul Enterprise]. | +| `namespace` | `string` | optional | The Consul namespace where the service will be registered. | +| `partition` | `string` | optional | The Consul admin partition where the service will be registered. | | `port` | `integer` | required | Port the application listens on, if any. | | `tags` | `array` | optional | List of string values that can be used to add service-level labels. | | [`weights`](#service-weights) | `object` | optional | Configures the weight of the service in terms of its DNS service (SRV) response. | # `service.checks` -Defines the Consul checks for the service. Each check may contain these fields. +Defines the Consul checks for the service. Each `check` object may contain the following fields. | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | @@ -86,14 +186,14 @@ Defines the Consul checks for the service. Each check may contain these fields. | `method` | `string` | optional | Specifies the HTTP method to be used for an HTTP check. When no value is specified, `GET` is used. | | `name` | `string` | optional | The name of the check. | | `notes` | `string` | optional | Specifies arbitrary information for humans. This is not used by Consul internally. | -| `status` | `string` | optional | Specifies the initial status the health check. Must be one of `passing`, `warning`, `critical`, `maintenance`, or`null`. | +| `status` | `string` | optional | Specifies the initial status the health check. Must be one of `passing`, `warning`, `critical`, `maintenance`, or `null`. | | `successBeforePassing` | `integer` | optional | Specifies the number of consecutive successful results required before check status transitions to passing. | | `tcp` | `string` | optional | Specifies this is a TCP check. Must be an IP/hostname plus port to which a TCP connection is made every `interval`. | -| `udp` | `string` | optional | Specifies this is a UDP check. Must be an IP/hostname plus port to which UDP datagrams are sent every `interval`. | | `timeout` | `string` | optional | Specifies a timeout for outgoing connections. Applies to script, HTTP, TCP, UDP, and gRPC checks. Must be a duration string, such as `10s` or `5m`. | | `tlsServerName` | `string` | optional | Specifies an optional string used to set the SNI host when connecting via TLS. | | `tlsSkipVerify` | `boolean` | optional | Specifies if the certificate for an HTTPS check should not be verified. | | `ttl` | `string` | optional | Specifies this is a TTL check. Must be a duration string, such as `10s` or `5m`. | +| `udp` | `string` | optional | Specifies this is a UDP check. Must be an IP/hostname plus port to which UDP datagrams are sent every `interval`. | # `service.weights` @@ -104,45 +204,3 @@ Configures the weight of the service in terms of its DNS service (SRV) response. | `passing` | `integer` | required | Weight for the service when its health checks are passing. | | `warning` | `integer` | required | Weight for the service when it has health checks in `warning` status. | -# `proxy` - -Configuration for the sidecar proxy registration with Consul. - -| Field | Type | Required | Description | -| ----- | ---- | -------- | ----------- | -| `config` | `object` | optional | Object value that specifies an opaque JSON configuration. The JSON is stored and returned along with the service instance when called from the API. | -| [`meshGateway`](#proxy-meshgateway) | `object` | optional | Specifies the mesh gateway configuration for the proxy. | -| [`upstreams`](#proxy-upstreams) | `array` | optional | The list of the upstream services that the proxy should create listeners for. | - -# `proxy.upstreams` - -The list of the upstream services that the proxy should create listeners for. Each upstream may contain these fields. - -| Field | Type | Required | Description | -| ----- | ---- | -------- | ----------- | -| `config` | `object` | optional | Specifies opaque configuration options that will be provided to the proxy instance for the upstream. | -| `datacenter` | `string` | optional | Specifies the datacenter to issue the discovery query to. | -| `destinationName` | `string` | required | Specifies the name of the upstream service or prepared query to route the service mesh to. | -| `destinationNamespace` | `string` | optional | Specifies the namespace containing the upstream service [Consul Enterprise]. | -| `destinationPartition` | `string` | optional | Specifies the name of the admin partition containing the upstream service [Consul Enterprise]. | -| `destinationType` | `string` | optional | Specifies the type of discovery query the proxy should use for finding service mesh instances. Must be one of `service`, `prepared_query`, or`null`. | -| `localBindAddress` | `string` | optional | Specifies the address to bind a local listener to. | -| `localBindPort` | `integer` | required | Specifies the port to bind a local listener to. The application will make outbound connections to the upstream from the local port. | -| [`meshGateway`](#proxy-upstreams-meshgateway) | `object` | optional | Specifies the mesh gateway configuration for the proxy for this upstream. | - -## `proxy.upstreams.meshGateway` - -Specifies the mesh gateway configuration for the proxy for this upstream. - -| Field | Type | Required | Description | -| ----- | ---- | -------- | ----------- | -| `mode` | `string` | required | Specifies how the upstream with a remote destination datacenter gets resolved. Must be one of `none`, `local`, or`remote`. | - -# `proxy.meshGateway` - -Specifies the mesh gateway configuration for the proxy. - -| Field | Type | Required | Description | -| ----- | ---- | -------- | ----------- | -| `mode` | `string` | required | Specifies how upstreams with a remote destination datacenter get resolved. Must be one of `none`, `local`, or`remote`. | - diff --git a/website/content/docs/ecs/enterprise.mdx b/website/content/docs/ecs/enterprise.mdx index 6ee42d5be..9350fc6db 100644 --- a/website/content/docs/ecs/enterprise.mdx +++ b/website/content/docs/ecs/enterprise.mdx @@ -7,31 +7,29 @@ description: >- # Consul Enterprise -Consul on ECS supports running Consul Enterprise by specifying the Consul Enterprise -Docker image in the Terraform module parameters. +You can run Consul Enterprise on ECS by specifying the Consul Enterprise Docker image in the Terraform module parameters. -## How To Use Consul Enterprise +## Specify the Consul image -When instantiating the [`mesh-task`](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/mesh-task) module, -set the parameter `consul_image` to a Consul Enterprise image, e.g. `hashicorp/consul-enterprise:1.10.0-ent`: +When you set up an instance of the [`mesh-task`](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/mesh-task) or [`gateway-task`](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/gateway-task) module, +set the parameter `consul_image` to a Consul Enterprise image. The following example instructs the `mesh-task` module to import Consul Enterprise version 1.12.0: ```hcl module "my_task" { source = "hashicorp/consul-ecs/aws//modules/mesh-task" version = "" - consul_image = "hashicorp/consul-enterprise:-ent" + consul_image = "hashicorp/consul-enterprise:1.12.0-ent" ... } ``` ## Licensing -~> **Warning:** Consul Enterprise is currently only fully supported when [ACLs are enabled](/docs/ecs/production-installation#deploy-acl-controller). +~> **Warning:** Consul Enterprise is currently only fully supported when [ACLs are enabled](/docs/ecs/terraform/secure-configuration). Consul Enterprise [requires a license](/docs/enterprise/license/overview). If running -Consul on ECS with [ACLs enabled](/docs/ecs/production-installation#deploy-acl-controller), the license -will be automatically pulled down from Consul servers. +Consul on ECS with ACLs enabled, the license will be automatically pulled down from Consul servers. Currently there is no capability for specifying the license when ACLs are disabled so if you wish to run Consul Enterprise clients then you must enable ACLs. @@ -62,14 +60,16 @@ If client support is required for any of the features, then you must use a Consu ### Admin Partitions and Namespaces -Consul on ECS supports [admin partitions](/docs/enterprise/admin-partitions) and [namespaces](/docs/enterprise/namespaces) when Consul Enterprise servers and clients are used. -These features have the following requirements: -* ACLs must be enabled. -* ACL controller must run in the ECS cluster. -* `mesh-tasks` must use a Consul Enterprise client image. +Consul on ECS supports [admin partitions](/docs/enterprise/admin-partitions) and [namespaces](/docs/enterprise/namespaces) when Consul Enterprise servers and clients are used. These features have the following requirements: -The ACL controller automatically manages ACL policies and token provisioning for clients and services on the service mesh. -It also creates admin partitions and namespaces if they do not already exist. +- ACLs must be enabled. +- ACL controller must run in the ECS cluster. +- `mesh-task` must use a Consul Enterprise client image. +- `gateway-task` must use a Consul Enterprise client image. + +The ACL controller manages configuration of the AWS IAM auth method on the Consul servers. It +ensures unused tokens created by tasks are cleaned up. It also creates admin partitions and +namespaces if they do not already exist. -> **NOTE:** The ACL controller does not delete admin partitions or namespaces once they are created. @@ -84,7 +84,7 @@ is not provided when `consul_partitions_enabled = true`, will default to the `de ```hcl module "acl_controller" { - source = "hashicorp/consul/aws-ecs//modules/acl-controller" + source = "hashicorp/consul/aws//modules/acl-controller" ... @@ -109,7 +109,7 @@ The following example demonstrates how to create a `mesh-task` assigned to the a ```hcl module "my_task" { - source = "hashicorp/consul/aws-ecs//modules/mesh-task" + source = "hashicorp/consul/aws//modules/mesh-task" family = "my_task" ... diff --git a/website/content/docs/ecs/index.mdx b/website/content/docs/ecs/index.mdx index e5c1f2e42..73c1cbf81 100644 --- a/website/content/docs/ecs/index.mdx +++ b/website/content/docs/ecs/index.mdx @@ -8,15 +8,13 @@ description: >- # AWS ECS -Consul service mesh applications can be deployed on [AWS Elastic Container Service](https://aws.amazon.com/ecs/) (ECS) -using either our official [Terraform modules](/docs/ecs/terraform/install) or without Terraform by [manually configuring -the task definition](/docs/ecs/manual/install). +You can deploy Consul service mesh applications to [AWS Elastic Container Service](https://aws.amazon.com/ecs/) (ECS) using either our official [Terraform modules](/docs/ecs/terraform/install) or by [manually configuring the task definition](/docs/ecs/manual/install). ## Service Mesh Using Consul on AWS ECS enables you to add your ECS tasks to the service mesh and take advantage of features such as zero-trust-security, intentions, observability, -traffic policy, and more. +traffic policy, and more. You can also connect service meshes so that services deployed across your infrastructure environments can communicate. ## Architecture diff --git a/website/content/docs/ecs/manual/acl-controller.mdx b/website/content/docs/ecs/manual/acl-controller.mdx index 39005ec9e..28c91bd97 100644 --- a/website/content/docs/ecs/manual/acl-controller.mdx +++ b/website/content/docs/ecs/manual/acl-controller.mdx @@ -7,48 +7,28 @@ description: >- # Install the ACL Controller -This topic describes how to manually deploy the ACL controller to [automatically provision ACL tokens](/docs/ecs/architecture#automatic-acl-token-provisioning) for Consul on ECS. -If you are using Terraform, refer to the [Terraform Secure Configuration](/docs/ecs/terraform/secure-configuration) page to deploy the ACL controller. +This topic describes how to manually deploy the ACL controller, which will automatically configure the [AWS IAM Auth Method](/docs/security/acl/auth-methods/aws-iam). If you are using Terraform, refer to the [Terraform Secure Configuration](/docs/ecs/terraform/secure-configuration) page to deploy the ACL controller. ## Prerequisites * Your application tasks must include certain tags to be compatible with the ACL controller. Refer to the [Task Tags](/docs/ecs/manual/install#task-tags) section of the installation page. -* You should be familiar with configuring Consul's secure features, including how to create ACL tokens and policies. Refer to the following [Learn Guides](https://learn.hashicorp.com/collections/consul/security) for an introduction and the [ACL system](/docs/security/acl) documentation for more information. +* You should be familiar with configuring Consul's secure features, including how to create ACL tokens and policies. Refer to the [Learn Guides about Consul Security](https://learn.hashicorp.com/collections/consul/security) for an introduction and the [ACL system](/docs/security/acl) documentation for more information. +* If you are using Consul with multiple ECS clusters, each cluster requires its own instance of the ACL controller. ## Set Up Secrets -The ACL controller supports managing secrets in AWS Secrets Manager. +Before deploying the ACL controller for the first time, you must [store the following secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) from Consul in AWS Secrets Manager. -Before deploying the ACL controller for the first time, you must [create the following secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) from Consul in AWS Secrets Manager. - -| Secret | Initial Value | Sample Secret Name | -| --------------------- | -------------- | ------------------------------ | -| Consul server CA cert | Set | `my-consul-ca-cert` | -| Bootstrap ACL Token | Set | `my-consul-bootstrap-token` | -| Consul Client ACL Token | Empty | `-consul-client-token` | - -The secret for the client token must be initially empty. The ACL controller creates the client token in Consul -and stores the token in Secrets Manager. In the secret name, `` should be replaced with the -[secret name prefix](/docs/ecs/manual/acl-controller#secret-name-prefix) of your choice. - -### Secret Name Prefix - -The ACL controller requires that the secrets it reads and writes are named with a unique prefix. The name prefix is used -in the [Task Role Policy](/docs/ecs/manual/acl-controller#task-role-policy) to limit the ACL controller's access within -AWS Secrets Manager to only those secrets strictly needed by the ACL controller. - -The name prefix should be unique among secrets in your AWS account. We recommend using a short (8 character) random -string for the prefix. - --> **NOTE:** If you are using the ACL controller with multiple ECS clusters, each cluster requires -its own instance of the ACL controller, and each instance of the ACL controller should have a unique -name prefix. +| Secret | Sample Secret Name | Description | +| --------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Consul server CA cert | `my-consul-ca-cert` | The Consul server CA Cert for the HTTPS interface. This is required if the Consul server uses a self-signed or internal CA. It is not required for Consul servers in HCP. | +| Bootstrap ACL Token | `my-consul-bootstrap-token` | A Consul ACL token with `acl:write` and `operator:write` permissions. | ## Task Definition You must create a task definition to deploy the ACL controller in your ECS cluster. -The ACL controller must run in the same ECS cluster hosting your service mesh application +The ACL controller must run in the same ECS cluster that hosts your service mesh application tasks. The following example shows how the task definition should be configured for the ACL controller. @@ -62,11 +42,7 @@ The following example shows how the task definition should be configured for the "name": "acl-controller", "image": "public.ecr.aws/hashicorp/consul-ecs:", "essential": true, - "command": [ - "acl-controller", - "-consul-client-secret-arn", "arn:aws:secretsmanager:us-west-2:000000000000:secret:-consul-client-token", - "-secret-name-prefix", "", - ], + "command": ["acl-controller", "-iam-role-path", "/consul-ecs/"], "secrets": [ { "name": "CONSUL_HTTP_TOKEN", @@ -97,21 +73,28 @@ You must include the following top-level fields. In the `containerDefinitions` list, include one container with the following fields. -| Field name | Type | Description | -| ----------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `name` | string | The container name, which should be `acl-controller` | -| `image` | string | The `consul-ecs` image. Use our public AWS registry, `public.ecr.aws/hashicorp/consul-ecs`, to avoid rate limits. | -| `command` | list | Must be set as shown. The startup command for the ACL controller. | -| `essential` | boolean | Must be `true` to ensure the health of your application container affects the health status of the task. | -| `secrets` | list | Must have `CONSUL_HTTP_TOKEN` set to the ACL bootstrap token and `CONSUL_CACERT_PEM` set to the Consul server CA certificate. | -| `environment` | string | Must set the `CONSUL_HTTP_ADDR` environment variable to the address of the HTTP API of your Consul servers. | +| Field name | Type | Description | +| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | string | The container name, which should be `acl-controller` | +| `image` | string | The `consul-ecs` image. Use our public AWS registry, `public.ecr.aws/hashicorp/consul-ecs`, to avoid rate limits. | +| `command` | list | Should be set as shown. The startup command for the ACL controller. | +| `essential` | boolean | Must be `true` to ensure the health of your application container affects the health status of the task. | +| `secrets` | list | Should be set as shown. Configures the secrets the ECS service will retrieve and set as environment variables in the `acl-controller` container. | +| `environment` | string | Must be set as shown. Configures environment variables that the ECS service will set in the `acl-controller` container. Must set the `CONSUL_HTTP_ADDR` environment variable to the HTTP(S) address of the Consul servers. | -The following CLI options are required in the `command` field of the container definition. +The following CLI options are available in the `command` field of the container definition. -| Flag | Type | Description | -| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------- | -| `-consul-client-secret-arn` | string | The secret where the ACL controller will store the Consul client token. | -| `-secret-name-prefix` | string | The [secret name prefix](/docs/ecs/manual/acl-controller#secret-name-prefix) that you chose for this ACL controller. | +| Flag | Type | Description | +| ------------------ | ------- | --------------------------------------------------------------------------------------------- | +| `-iam-role-path` | string | Specifies the path to IAM roles trusted by the AWS IAM auth method created by the controller. | +| `-log-level` | string | The log level for the ACL controller. Can be set to `DEBUG` for additional detail. | + +The following describes the entries to include in the `secrets` list. + +| Name | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `CONSUL_HTTP_TOKEN` | Must be set to the secret containing the bootstrap ACL token. | +| `CONSUL_CACERT_PEM` | If applicable, should be set to the secret containing the Consul server CA certificate. This must not be set when using Consul servers in HCP. | ## ECS Service @@ -162,29 +145,17 @@ secrets. "ecs:DescribeTasks" ], "Resource": ["*"] - }, - { - "Effect": "Allow", - "Action": [ - "secretsmanager:GetSecretValue", - "secretsmanager:UpdateSecret" - ], - "Resource": [ - "arn:aws:secretsmanager:us-west-2:000000000000:secret:-*" - ] } ] } ``` -The following are the required permissions. You will need to substitute `` with your chosen [name prefix](/docs/ecs/manual/acl-controller#secret-name-prefix). +The following are the required permissions. -| Action | Resource | Description | -| ------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| `ecs:ListTasks` | `*` | Allow the ACL controller to watch for new tasks. | -| `ecs:DescribeTasks` | `*` | Allow the ACL controller to retrieve details for new tasks. | -| `secretsmanager:GetSecretValue` | `arn:aws:secretsmanager:us-west-2:000000000000:secret:-*` | Allow the ACL controller to read secrets with a name prefix. | -| `secretsmanager:UpdateSecret` | `arn:aws:secretsmanager:us-west-2:000000000000:secret:-*` | Allow the ACL controller to store Consul ACL tokens in secrets with a name prefix. | +| Action | Resource | Description | +| --------------------- | --------- | ------------------------------------------------------------ | +| `ecs:ListTasks` | `*` | Allow the ACL controller to watch for new tasks. | +| `ecs:DescribeTasks` | `*` | Allow the ACL controller to retrieve details for new tasks. | ### Execution Role Policy @@ -205,9 +176,15 @@ The following example shows the policy needed for the execution role. ], "Resource": [ "arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-bootstrap-token", - "arn:aws:secretsmanager:us-west-2:000000000000:secret:-consul-client-token" + "arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-ca-cert" ] } ] } ``` + +The following are the required permissions. + +| Action | Resource | Description | +| ------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `secretsmanager:GetSecretValue` | `arn:aws:secretsmanager:us-west-2:000000000000:secret:` | Allow ECS to retrieve this secret and inject the secret into the task. | diff --git a/website/content/docs/ecs/manual/install.mdx b/website/content/docs/ecs/manual/install.mdx index 1046c69c6..aac1b416b 100644 --- a/website/content/docs/ecs/manual/install.mdx +++ b/website/content/docs/ecs/manual/install.mdx @@ -7,7 +7,9 @@ description: >- # Manual Installation -The following instructions describe how to manually create the ECS task definition using the [`consul-ecs` Docker image](https://gallery.ecr.aws/hashicorp/consul-ecs) without Terraform. Refer to the [Consul ECS Terraform module](/docs/ecs/terraform/install) documentation for an alternative method for installing Consul on ECS. +The following instructions describe how to use the [`consul-ecs` Docker image](https://gallery.ecr.aws/hashicorp/consul-ecs) to manually create the ECS task definition without Terraform. If you prefer to use Terraform, refer to [Consul ECS Terraform module](/docs/ecs/terraform/install). + +If you intend to peer the service mesh to multiple Consul datacenters or partitions, you must use the Consul ECS Terraform module to install your service mesh on ECS. Manually configuring mesh gateways without using the `gateway-task` Terraform module is not supported. This topic does not include instructions for creating all AWS resources necessary to install Consul, such as a VPC or the ECS cluster. Refer to the linked guides in the [Getting Started](/docs/ecs#getting-started) section for complete, runnable examples. @@ -17,13 +19,13 @@ You should have some familiarity with AWS ECS. See [What is Amazon Elastic Conta ## Task Definition -You must create a task definition, which includes the following containers: +Configure a task definition that creates the containers: -* Your application container -* An Envoy sidecar-proxy container -* A Consul client container -* A `consul-ecs-mesh-init` container for service mesh setup -* Optionally, a `consul-ecs-health-sync` container to sync ECS health checks into Consul +- Your application container +- An Envoy sidecar-proxy container +- A Consul client container +- A `consul-ecs-mesh-init` container for service mesh setup +- (Optional) A `consul-ecs-health-sync` container to sync ECS health checks into Consul ## Top-level fields @@ -40,10 +42,10 @@ during task startup. "networkMode": "awsvpc", "volumes": [ { - "name": "consul_data", + "name": "consul_data" }, { - "name": "consul_binary", + "name": "consul_binary" } ], "containerDefinitions": [...] @@ -69,7 +71,7 @@ during task startup. ### Task Tags -The `tags` list must include the following if you are using the ACL controller in a [secure configuration](/docs/manual/secure-configuration). +The `tags` list must include the following if you are using the ACL controller in a [secure configuration](/docs/ecs/manual/secure-configuration). Without these tags, the ACL controller will be unable to provision a service token for the task. | Tag Key | Tag Value | Description | @@ -367,7 +369,7 @@ configuration to a shared volume. | `image` | string | The `consul-ecs` image. Use our public AWS registry, `public.ecr.aws/hashicorp/consul-ecs`, to avoid rate limits. | | `mountPoints` | list | Must be set as show above, so the `consul` and `consul-ecs` binaries can be shared among containers for task setup. | | `command` | list | Set to `["mesh-init"]` so that the container runs the `consul-ecs mesh-init` command. | -| `environment` | list | This must include the [`CONSUL_ECS_CONFIG_JSON`](/docs/ecs/manual-installation#consul_ecs_config_json) variable. See below for details. | +| `environment` | list | This must include the [`CONSUL_ECS_CONFIG_JSON`](/docs/ecs/manual/install#consul_ecs_config_json) variable. See below for details. | ### `CONSUL_ECS_CONFIG_JSON` @@ -403,7 +405,7 @@ the `consul-ecs-health-sync` container. | Field name | Type | Description | | ---------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `bootstrapDir` | string | This is the path of a shared volume that is mounted to other containers, where `consul-ecs-mesh-init` will write out Envoy configuration. | -| `healthSyncContainers` | list | Used for [health status syncing](/docs/ecs/manual-installation#consul-ecs-health-sync-container) from ECS to Consul. See below for details. | +| `healthSyncContainers` | list | Used for [health status syncing](/docs/ecs/architecture#ecs-health-check-syncing) from ECS to Consul. See below for details. | | `proxy.upstreams` | list | The upstream services that your application calls over the service mesh, if any. The `destinationName` and `localBindPort` fields are required. | | `service.name` | string | The name used to register this service into the Consul service catalog. | | `service.port` | integer | The port your application listens on. Set to `0` if your application does not listen on any port. | @@ -549,6 +551,6 @@ and `consul-ecs-mesh-init` containers. # Next Steps -* Create the task definition using the [AWS Console](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html) or the [AWS CLI](https://docs.aws.amazon.com/cli/latest/reference/ecs/register-task-definition.html), or another method of your choice. +* Create the task definition using the [AWS Console](https://docs.aws.amazon.com/AmazonECS/latest/userguide/create-task-definition-classic.html) or the [AWS CLI](https://docs.aws.amazon.com/cli/latest/reference/ecs/register-task-definition.html), or another method of your choice. * Create an [ECS Service](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html) to start tasks using the task definition. * Follow the [Secure Configuration](/docs/ecs/manual/secure-configuration) to get production-ready. diff --git a/website/content/docs/ecs/manual/secure-configuration.mdx b/website/content/docs/ecs/manual/secure-configuration.mdx index c96d0afdf..1bb800c00 100644 --- a/website/content/docs/ecs/manual/secure-configuration.mdx +++ b/website/content/docs/ecs/manual/secure-configuration.mdx @@ -7,41 +7,102 @@ description: >- # Secure Configuration -For a production-ready installation of Consul on ECS, you will need to make sure that the cluster is secured. -A secure Consul cluster should include the following: - -1. [TLS Encryption](/docs/security/encryption#rpc-encryption-with-tls) for RPC communication between Consul clients and servers. -1. [Gossip Encryption](/docs/security/encryption#gossip-encryption) for encrypting gossip traffic. -1. [Access Control (ACLs)](/docs/security/acl) for authentication and authorization for Consul clients and services on the mesh. - --> **NOTE:** In this topic, we assume that you have already configured your Consul server with the security-related features. +This topic describes how to enable Consul security features for your production workloads. ## Prerequisites -* You should already have followed the [installation instructions](/docs/ecs/manual/install) to understand how to define -the necessary components of the task definition for Consul on ECS. -* You should be familiar with [specifying sensitive data](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html) on ECS. -* You should be familiar with configuring Consul's secure features, including how to create ACL tokens and policies. Refer to the following [Learn Guides](https://learn.hashicorp.com/collections/consul/security) for an introduction and the [ACL system](/docs/security/acl) documentation for more information. +The following features must be configured for your Consul server cluster: -## ACL Tokens +- [TLS encryption](/docs/security/encryption#rpc-encryption-with-tls) for RPC communication between Consul clients and servers. +- [Gossip encryption](/docs/security/encryption#gossip-encryption) for encrypting gossip traffic. +- [Access control lists (ACLs)](/docs/security/acl) for authentication and authorization for Consul clients and services on the mesh. -You must create two types of ACL tokens for Consul on ECS: +You should already have followed the [manual installation instructions](/docs/ecs/manual/install) to define the necessary components of the task definition for Consul on ECS. + +You should be familiar with [specifying sensitive data](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html) on ECS. + +You should be familiar with configuring Consul's secure features, including how to create ACL tokens and policies. Refer to the [ACL system documentation](/docs/security/acl) and [Day 1: Security tutorials](https://learn.hashicorp.com/collections/consul/security) for an introduction and additional information. + +## Auth Method + +Tokens are artifacts within the ACL system that authenticate users, services, and Consul agents. Tokens are linked to policies that specify the resources the token bearer has access to when making requests in the network. + +Auth Methods are a Consul server component that performs authentication against a trusted external party to authorize the creation of ACL tokens. The [AWS IAM auth method](/docs/security/acl/auth-methods/aws-iam) is used to enable an ECS task to automatically obtain ACL tokens when the task starts up. + +There are two types of ACL tokens for Consul on ECS: * **Client tokens:** used by the `consul-client` containers to join the Consul cluster * **Service tokens:** used by sidecar containers for service registration and health syncing -The following sections describe the ACL polices which must be associated with these token types. +This section describes how to manually configure the AWS IAM auth method for Consul on ECS. Alternatively, you can install the ACL controller to ease the burden of creating these resources. The ACL controller can automatically configure ACL resources for Consul on ECS. For additional details, refer to [ACL Controller](/docs/manual/acl-controller) and [Architecture](/docs/ecs/architecture). --> **NOTE:** This section describes how operators would create ACL tokens by hand. To ease operator -burden, the ACL Controller can automatically create ACL tokens for Consul on ECS. Refer to the -[ACL Controller](/docs/manual/acl-controller) page for installation details. +### ECS Task Role Configuration -### Create Consul client token +The ECS [task role](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) +is an IAM role associated with an ECS task. -You must create a token for the Consul client. This is a shared token used by the `consul-client` -containers to join the Consul cluster. +When an ECS task starts up, it runs a `consul login` command. The `consul login` command obtains +credentials for the task role role from AWS. It uses those credentials to sign the login request to +the AWS IAM auth method. This proves the ECS task's identity to the Consul servers. -The following is the ACL policy needed for the Consul client token: +The task role must be configured with the following details to compatible with the AWS IAM auth +method. + +* An `iam:GetRole` permission to fetch itself. See + [IAM Policies](/docs/security/acl/auth-methods/aws-iam#iam-policies) for details. +* A `consul.hashicorp.com.service-name` tag on the task role which contains the Consul service name + for the application in this task. +* A consul.hashicorp.com.namespace tag on the task role + indicating the Consul Enterprise namespace where this service is registering. + +The following sections describe how to configure the auth method to enable task roles to +successfully authenticate to the AWS IAM auth method and obtain tokens with the necessary +permissions. + +### Auth Method for Client Tokens + +The following steps configure an instance of the auth method that creates client tokens for tasks. + +1. Create the auth method instance +2. Create the client policy and role +3. Create the binding rule + +#### Create Auth Method for Client Tokens + +The following Consul CLI command creates an instance of the auth method for client tokens. + +```shell +consul acl auth-method create \ + -type aws-iam \ + -name iam-ecs-client-token \ + -description="AWS IAM auth method for ECS client tokens" \ + -config '{ + "BoundIAMPrincipalArns": ["arn:aws:iam:::role/consul-ecs/*"], + "EnableIAMEntityDetails": true +}' +``` + +The following flags are required: + +| Flag | Type | Description | +| --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `-type` | string | Must be `aws-iam`. | +| `-name` | string | A name of your choice. Must be unique among all auth methods. | +| `-description` | string | A description of your choice. | +| `-config` | string | A JSON string containing the [configuration](/docs/security/acl/auth-methods/aws-iam#config-parameters) for the the auth method. | + +In the `-config` option, the following fields are required: + +| Field | Type | Description | +| ------------------------- | ------- | --------------------------------------------------------------------------------------------------------- | +| `BoundIAMPrincipalArns` | list | The list of trusted IAM roles. We recommend using a wildcard to trust IAM roles at a particular path. | +| `EnableIAMEntityDetails` | boolean | Must be true so that the auth method can retrieve IAM role details, such as the role path and role tags. | + +#### Create Client Policy and Role + +Configure the following ACL policy for Consul client tokens: + + ```hcl node_prefix "" { @@ -52,31 +113,173 @@ service_prefix "" { } ``` -This policy allows `node:write` for any node name, which is necessary because the Consul node -names on ECS are not known until runtime. + -### Create service tokens +The policy allows `node:write` for any node name, which is necessary because the Consul node names on ECS are not known until runtime. -Service tokens should be associated with a [service identity](/docs/security/acl#service-identities). -The service identity includes `service:write` permissions for the service and sidecar proxy. +You can add the policy in Consul using the [`consul acl policy create`](/commands/acl/policy/create) command or the [`[PUT] /v1/acl/policy`](/api-docs/acl/policies#create-a-policy) API endpoint. -The following example shows how to use the Consul CLI to create a service token for a service named `example-client-app`: +After the policy is created, create a Consul role associated with the policy by using the [`consul acl role create`](/commands/acl/role/create) command or the [`[PUT] /v1/acl/role`](/api-docs/acl/roles#create-a-role) API endpoint. + +The following example shows how to use the Consul CLI to create the client policy and role. ```shell -consul acl token create -service-identity=example-client-app ... +consul acl policy create -name consul-ecs-client-policy -rules @client-token-policy.hcl + +consul acl role create -name consul-ecs-client-role -policy-name consul-ecs-client-policy ``` --> **NOTE**: You will need to create one service token for each registered Consul service in ECS, -including when new services are added to the service mesh. +#### Create Binding Rule for Client Tokens + +The following creates a binding rule for the auth method for client tokens. The binding rule +associates the client role with each token created by a successful login to this auth +method instance. + +```shell +consul acl binding-rule create -method iam-ecs-client-token \ + -description 'Bind a role for Consul clients on ECS' \ + -bind-type role \ + -bind-name consul-ecs-client-role +``` + +### Auth Method for Service Tokens + + +The following steps configure an instance of the auth method that creates service tokens for tasks. + +* Create the auth method instance +* Create the binding rule + +#### Create Auth Method for Service Tokens + +The following uses the Consul CLI to create an instance of the auth method +for service tokens. This configures the auth method to associate a service identity +to each token created during login to this auth method instance. + +```shell +consul acl auth-method create \ + -type aws-iam \ + -name iam-ecs-service-token \ + -description="AWS IAM auth method for ECS service tokens" \ + -config '{ + "BoundIAMPrincipalArns": ["arn:aws:iam:::role/consul-ecs/*"], + "EnableIAMEntityDetails": true, + "IAMEntityTags": [ + "consul.hashicorp.com.service-name" + ] +}' +``` + +The following flags are required: + +| Flag | Type | Description | +| --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `-type` | string | Must be `aws-iam`. | +| `-name` | string | A name of your choice. Must be unique among all auth methods. | +| `-description` | string | A description of your choice. | +| `-config` | string | A JSON string containing the [configuration](/docs/security/acl/auth-methods/aws-iam#config-parameters) for the the auth method. | + +In the `-config` option, the following fields are required: + +| Field | Type | Description | +| --- | --- | --- | +| `BoundIAMPrincipalArns` | list | The list of trusted IAM roles. We recommend using a wildcard to trust IAM roles at a particular path. | +| `EnableIAMEntityDetails` | boolean | Must be true so that the auth method can retrieve IAM role details, such as the role path and role tags. | +| `IAMEntityTags` | list | The list of IAM role tags to make available to binding rules. Must include the service name tag as shown. | + +The following binding rule is used to associate a service identity with each token created by +successful login to this instance of the auth method. The service identity name is taken from the +`consul.hashicorp.com.service-name` tag from the authenticaing IAM role identity. + +#### Create Binding Rule + +```shell +consul acl binding-rule create \ + -method iam-ecs-service-token \ + -description 'Bind a service identity from IAM role tags for ECS service tokens' \ + -bind-type service \ + -bind-name '${entity_tags.consul.hashicorp.com.service-name}' +``` + +### Configuration for Consul Enterprise + +When using Consul Enterprise namespaces and admin partitions, pass the `-partition ` +option to the Consul CLI when creating Consul ACL roles, policies, auth methods, and binding rules, +in order to create these resources in a particular partition. + +The following shows how to create the ACL policy for client tokens. This ensures permissions for the +client token are scoped to a particular partition. + + + +```hcl +partition "" { + node_prefix "" { + policy = "write" + } + namespace_prefix "" { + service_prefix "" { + policy = "read" + } + } +} +``` + + + +The following commands show how to use the Consul CLI to create the policy and role. + +```shell +consul acl policy create -partition \ + -name consul-ecs-client-policy \ + -rules @client-token-policy-ent.hcl + +consul acl role create \ + -partition \ + -name consul-ecs-client-role \ + -policy-name consul-ecs-client-policy +``` + +The auth method for *service tokens* requires the following additional configuration to include a +namespace binding rule. This ensures the service tokens are created in the right namespace during +login. (The namespace binding rule must not be configured on the instance of the auth method +instance for *client tokens*.) + + + +```shell +consul acl auth-method create \ + -partition \ + -type aws-iam \ + -name iam-ecs-service-token \ + -description="AWS IAM auth method for ECS service tokens" \ + -namespace-rule-selector 'entity_tags["consul.hashicorp.com.namespace"] != ""' \ + -namespace-rule-bind-namespace '${entity_tags.consul.hashicorp.com.namespace}' \ + -config '{ + "BoundIAMPrincipalArns": ["arn:aws:iam:::role/consul-ecs/*"], + "EnableIAMEntityDetails": true, + "IAMEntityTags": [ + "consul.hashicorp.com.service-name", + "consul.hashicorp.com.namespace" + ] +}' +``` + + + +| Field | Type | Description | +| -------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-partition` | string | The Consul Enterprise admin partition in which the auth method is created. | +| `-namespace-rule-selector` | string | When this expression evaluates to true during login, the `-namespace-rule-bind-namespace` is applied. As shown, it evaluates to true when the `consul.hashicorp.com.namespace` tag is non-empty on the task IAM role. | +| `-namespace-rule-bind-namespace` | string | This expression is evaluted to determine the namespace where the token is created during login. As shown, it uses the namespace from the `consul.hashicorp.com.namespace` tag on the task IAM role. | +| `IAMEntityTags` | list | Must include `consul.hashicorp.com.namespace` to enable use of this tag in binding rules. | ## Secret storage You should securely store the following secrets in order to make them available to ECS tasks. -1. Consul Server CA certificate +1. Consul Server CA certificates. More than one may be required for different Consul protocols. 2. Consul gossip encryption key -3. Consul client ACL token -4. Consul service ACL tokens (one per service) These secrets can be securely stored and passed to ECS tasks using either of the following AWS secret services: @@ -86,20 +289,18 @@ These secrets can be securely stored and passed to ECS tasks using either of the Once the secrets are stored they can be referenced using their ARN. The following shows example secret ARNs when using AWS Secrets Manager: -| Secret | Sample Secret ARN | -| ---------------------- | ---------------------------------------------------------------------------------- | -| Consul Server CA Cert | `arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-ca-cert` | -| Gossip encryption key | `arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-gossip-key` | -| Client token | `arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-client-token` | -| Service token | `arn:aws:secretsmanager:us-west-2:000000000000:secret:my-example-client-app-token` | +| Secret | Sample Secret ARN | +| ---------------------- | ---------------------------------------------------------------------------------- | +| Consul Server CA Cert for RPC | `arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-ca-cert` | +| Consul Server CA Cert for HTTPS | `arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-https-ca-cert` | +| Gossip encryption key | `arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-gossip-key` | ## Configure `consul-client` The following secrets must be passed to the `consul-client` container: -* Consul server CA certificate +* Consul server CA certificates * Gossip encryption key -* Consul client ACL token The following example shows how to include these secrets in the task definition. The `secrets` list specifies environment variable `name`s that will be set to the secret values for this container. @@ -113,16 +314,16 @@ ECS automatically fetches the secret values specified in the `valueFrom` fields "image": "public.ecr.aws/hashicorp/consul:", "secrets": [ { - "name": "CONSUL_CACERT", + "name": "CONSUL_CACERT_PEM", "valueFrom": "arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-ca-cert" }, + { + "name": "CONSUL_HTTPS_CACERT_PEM", + "valueFrom": "arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-https-ca-cert" + }, { "name": "CONSUL_GOSSIP_ENCRYPTION_KEY", "valueFrom": "arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-gossip-key" - }, - { - "name": "AGENT_TOKEN", - "valueFrom": "arn:aws:secretsmanager:us-west-2:000000000000:secret:my-consul-client-token" } ] }, @@ -131,19 +332,76 @@ ECS automatically fetches the secret values specified in the `valueFrom` fields } ``` -Next, update Consul configuration options to pass the secrets to the Consul client. +Next, update the Consul client startup script with the `consul login` command and additional Consul configuration +options for a secure configuration. The following is an example of the *additional* content to include in the `consul-client` startup script. Refer to the [install page](/docs/ecs/manual/install#consul-client-container) for the remainder of the startup script and how to pass this script to the container. - + ```shell ... -# Write the CA Cert to a file -echo "$CONSUL_CACERT" > /tmp/consul-ca-cert.pem +# Obtain details from the task metadata +ECS_TASK_META=$(curl -s $ECS_CONTAINER_METADATA_URI_V4/task) +TASK_REGION=$(echo "$ECS_TASK_META" | jq -r .TaskARN | cut -d ':' -f 4) +TASK_ID=$(echo "$ECS_TASK_META" | jq -r .TaskARN | cut -d '/' -f 3) +CLUSTER_ARN=$(echo "$ECS_TASK_META" | jq -r .TaskARN | sed -E 's|:task/([^/]+).*|:cluster/\1|') + +# Write the CA certs to a files in the shared volume +echo "$CONSUL_CACERT_PEM" > /consul/consul-ca-cert.pem +echo "$CONSUL_HTTPS_CACERT_PEM" > /consul/consul-https-ca-cert.pem + +consul_login() { + echo "Logging into auth method" + consul login \ + -http-addr "" \ + -ca-file /consul/consul-https-ca-cert.pem \ + -partition "" \ + -type aws-iam \ + -method iam-ecs-client-token \ + -meta "consul.hashicorp.com/task-id=$TASK_ID" \ + -meta "consul.hashicorp.com/cluster=$CLUSTER_ARN" \ + -aws-region "$TASK_REGION" \ + -aws-auto-bearer-token -aws-include-entity \ + -token-sink-file /consul/client-token +} + +read_token_stale() { + consul acl token read -http-addr ${ consul_http_addr } \ + -ca-file /consul/consul-https-ca-cert.pem \ + -stale -self -token-file /consul/client-token \ + > /dev/null +} + +# Retry in order to login successfully. +while ! consul_login; do + sleep 2 +done + +# Allow the health-sync container to read this token for consul logout. +chmod 0644 /consul/client-token + +# Wait for raft replication to hopefully occur. Without this, an "ACL not found" may be cached for a while. +COUNT=20 +while [ "$COUNT" -gt 0 ]; do + echo "Checking that the ACL token exists when reading it in the stale consistency mode ($COUNT attempts remaining)" + if read_token_stale; then + echo "Successfully read ACL token from the server" + break + fi + sleep 0.1 + COUNT=$((COUNT - 1)) +done +if [ "$COUNT" -eq 0 ]; then + echo "Unable to read ACL token from a Consul server; please check that your server cluster is healthy" + exit 1 +fi + +# This is interpolated into the agent-defaults.hcl +export AGENT_TOKEN=$(cat /consul/client-token) # Write the Consul agent configuration file. cat << EOF > /consul/agent-defaults.hcl @@ -159,7 +417,7 @@ auto_encrypt = { } tls { defaults { - ca_file = "/tmp/consul-ca-cert.pem" + ca_file = "/consul/consul-ca-cert.pem" verify_outgoing = true } } @@ -174,46 +432,75 @@ acl { } } +partition = "" + EOF ``` +The following describes the additional steps added to the startup script: + +* Fetch additional details from the task metadata: the AWS region, task id, and cluster arn. + These details are necessary for the `consul login` command used to obtain a Consul client token. +* Write CA certificates to files for Consul CLI and Consul client +* Run the `consul login` command in a retry loop. +* Wait for Raft replication to hopefully occur for this token. +* Configure the Consul client config file with additional fields necessary for secure configuration. + +The following flags are passed to the `consul login` command: + +| Field name | Type | Description | +| ------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------ | +| [`-http-addr`](/commands/login#http-addr) | string | HTTP(S) address of the Consul server. | +| [`-ca-file`](/commands/login#ca-file) | string | Path of the CA cert for Consul's HTTPS interface. Not required when using Consul servers on HCP. | +| `-partition` | string | The Consul Enterprise admin partition the auth method belongs to. | +| [`-type`](/commands/login#type) | string | The auth method type. Must be `aws-iam`. | +| [`-method`](/commands/login#type) | string | The auth method name. Must be the name of the auth method for ECS client tokens. | +| [`-meta`](/commands/login#meta) | string | Metadata to set in description of the created token. Should include the task id and cluster as shown. | +| `-aws-region` | string | The AWS region where the task is running. | +| `-aws-auto-bearer-token` | | Must be set to login to the AWS IAM auth method. | +| `-aws-include-entity` | | Must be set to enable IAM role details. | + The following table describes the additional fields that must be included in the Consul client configuration file. -| Field name | Type | Description | -| -------------------------------------------------------------------------------| ------- | ------------------------------------------------------------------------------------ | -| [`encrypt`](/docs/agent/config/cli-flags#_encrypt) | string | Specifies the gossip encryption key | -| [`tls.defaults.ca_file`](/docs/agent/config/config-files#tls_defaults_ca_file) | string | Specifies the Consul server CA cert for TLS verification. | -| [`acl.enabled`](/docs/agent/config/config-files#acl_enabled) | boolean | Enable ACLs for this agent. | -| [`acl.tokens.agent`](/docs/agent/config/config-files#acl_tokens_agent) | string | Specifies the Consul client token which authorizes this agent with Consul servers. | +| Field name | Type | Description | +| ------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------ | +| [`encrypt`](/docs/agent/config/cli-flags#_encrypt) | string | Gossip encryption key | +| [`tls.defaults.ca_file`](/docs/agent/config/config-files#tls_defaults_ca_file) | string | Consul server CA cert for TLS verification. | +| [`acl.enabled`](/docs/agent/config/config-files#acl_enabled) | boolean | Enable ACLs for this agent. | +| [`acl.tokens.agent`](/docs/agent/config/config-files#acl_tokens_agent) | string | Consul client token which authorizes this agent with Consul servers. | +| [`partition`](/docs/agent/config/config-files#partition-1) | string | The Consul Enterprise admin partition this agent belongs to. | ## Configure `consul-ecs-mesh-init` and `consul-ecs-health-sync` -Both `consul-ecs-mesh-init` and `consul-ecs-health-sync` containers need to be configured with -the service ACL token. This allows these containers to make HTTP API requests to the local -Consul client for service registration and health syncing. +The following *additional* options should be set in the [`CONSUL_ECS_CONFIG_JSON`](/docs/ecs/manual/install#consul_ecs_config_json) environment variable. When these options are specified, the `consul-ecs mesh-init` command will run the `consul login` command to obtain a service token from the Consul AWS IAM Auth method. The `consul-ecs health-sync` command is responsible for running a `consul logout` command for both the service and client tokens when the task shuts down. -The following shows how to set the `CONSUL_HTTP_TOKEN` variable to the service token for the `example-client-app` service, -if the token is stored in AWS Secrets Manager. - - + ```json { - "containerDefinitions": [ - { - "secrets": [ - { - "name": "CONSUL_HTTP_TOKEN", - "valueFrom": "arn:aws:secretsmanager:us-west-2:000000000000:secret:my-example-client-app-token" - } - ] - }, - ... - ], + "consulHTTPAddr": "", + "consulCACertFile": "/consul/consul-https-ca-cert.pem", + "consulLogin": { + "enabled": true, + "method": "iam-ecs-service-token", + "extraLoginFlags": ["-partition", ""] + }, + "bootstrapDir": "/consul", + "healthSyncContainers": [], ... } ``` + +The following table explains the additional fields for the `CONSUL_ECS_CONFIG_JSON`: + +| Field name | Type | Description | +| ----------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `consulHTTPAddr` | string | HTTP(S) address for the Consul server. | +| `consulCACertFile` | string | Path of the CA cert file for Consul's HTTPS interface. Not required for Consul servers in HCP. | +| `consulLogin.enabled` | boolean | Must be set to true to log in to the auth method. | +| `consulLogin.method` | string | Must be set to the name of the auth method instance for service tokens. | +| `consulLogin.extraLoginFlags` | list | Additional flags passed to the `consul login` command. This shows how to pass the Consul Enterprise admin partition to the `consul login` command. | diff --git a/website/content/docs/ecs/terraform/install.mdx b/website/content/docs/ecs/terraform/install.mdx index e810e141a..2d520e418 100644 --- a/website/content/docs/ecs/terraform/install.mdx +++ b/website/content/docs/ecs/terraform/install.mdx @@ -7,34 +7,42 @@ description: >- # Installation with Terraform -This topic describes how to use the [`mesh-task`](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/mesh-task) Terraform module to launch your application in AWS ECS as part of Consul service mesh. If you do not use Terraform, see the [Manual Installation](/docs/ecs/manual-installation) page to install Consul on ECS without Terraform. +This topic describes how to use HashiCorp’s Terraform modules to launch your application in AWS ECS as part of Consul service mesh. If you do not use Terraform, refer to the [Manual Installation](/docs/ecs/manual/install) page to install Consul on ECS without Terraform. -This topic does not include instructions for creating all AWS resources necessary to install Consul, such as a VPC or the ECS cluster. Refer to the linked guides in the [Getting Started](/docs/ecs#getting-started) section for complete, runnable examples. +This topic does not include instructions for creating all AWS resources necessary to install Consul, such as a VPC or the ECS cluster. Refer to the guides in the [Getting Started](/docs/ecs#getting-started) section for complete and runnable examples. ## Overview -This topic describes the following procedure: +The following procedure describes the general workflow: 1. Create Terraform configuration files for the necessary components: - * [ECS task definition](#using-the-mesh-task-module): Use the `mesh-task` module to create an ECS task definition for Consul on ECS - * [ECS service](#ecs-service): Use the `aws_ecs_service` resource to create an ECS service that schedules service mesh tasks to run on ECS + - [ECS task definition](#create-the-task-definition): Use the HashiCorp Terraform modules to create the ECS task definition. + - [ECS service](#ecs-service): Use the `aws_ecs_service` resource to create an ECS service that schedules service mesh tasks to run on ECS. 2. [Run Terraform](#running-terraform) to deploy the resources in AWS -## Prerequisites +If you want to operate Consul in production environments, follow the instructions in the [Secure Configuration](/docs/ecs/terraform/secure-configuration) documentation. The instructions describe how to enable ACLs and TLS and gossip encyption, which provide network security for production-grade deployments. + +## Requirements * You should have some familiarity with using Terraform. Refer to the [Terraform documentation](https://www.terraform.io/docs) to learn about infrastructure as code and how to get started with Terraform. * You should also be familiar with AWS ECS before following these instructions. See [What is Amazon Elastic Container Service](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html) for details. +* If you intend to [use the `gateway-task` module to deploy mesh gateways](#configure-the-gateway-task-module), all Consul server and client agents in all datacenters must have TLS and gossip encryption enabled. Refer to the [Secure Configuration](/docs/ecs/terraform/secure-configuration) documentation for instructions. -## Using the Mesh Task Module +## Create the task definition -To run an application in ECS with Consul service mesh, you must create an ECS task definition, which includes your application container(s) -and additional sidecar containers, such as the Consul agent container and the Envoy sidecar proxy container. +To run an application in ECS with Consul service mesh, you must create an ECS task definition. The task definition includes your application containers and additional sidecar containers, such as the Consul agent container and the Envoy sidecar proxy container. -The [`mesh-task` module](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/mesh-task) will automatically include the necessary sidecar containers. +Create a Terraform configuration file and include the `mesh-task` module. The module automatically adds the necessary sidecar containers. -The following example shows a Terraform configuration file that creates a task definition with an application container called `example-client-app` in a file called `mesh-task.tf`: +If you intend to peer the service mesh to multiple Consul datacenters or partitions, you can also include the `gateway-task` module. The module enables connectivity between datacenters across service meshes. + +### Configure the mesh task module + +Create a Terraform configuration file and specify the `mesh-task` module in the `source` field. The [`mesh-task` module](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/mesh-task) automatically includes the necessary sidecar containers. + +In the following example, the Terraform configuration file called `mesh-task.tf` creates a task definition with an application container called `example-client-app`: @@ -80,10 +88,216 @@ The following fields are required. Refer to the [module reference documentation] | `container_definitions` | list | This is the list of [container definitions](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definitions) for the task definition. This is where you include your application containers. | | `essential` | boolean | Must be `true` to ensure the health of your application container affects the health status of the task. | | `port` | integer | The port that your application listens on, if any. If your application does not listen on a port, set `outbound_only = true`. | -| `retry_join` | list | The is the [`retry_join`](/docs/agent/options#_retry_join) option for the Consul agent, which specifies the locations of your Consul servers. | -| `consul_datacenter` | string | The name of your Consul datacenter. | +| `retry_join` | list | This is the [`retry_join`](/docs/agent/options#retry_join) option for the Consul agent, which specifies the locations of your Consul servers. | +### Configure an ECS service for the mesh task module -### Running Terraform +[ECS services](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html) are one of the most common +ways to start tasks using a task definition. + +To define an ECS service, reference the `mesh-task` module's `task_definition_arn` output value +in your `aws_ecs_service` resource. The following example shows how to include the service in the `mesh-task.tf` file. + + + +```hcl +module "my_task" { + source = "hashicorp/consul-ecs/aws//modules/mesh-task" + ... +} + +resource "aws_ecs_service" "my_task" { + name = "my_task_service" + task_definition = module.my_task.task_definition_arn + launch_type = "FARGATE" + propagate_tags = "TASK_DEFINITION" + ... +} +``` + + + +The example shows a partially configured ECS service to highlight significant fields. Refer to [`aws_ecs_service`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) for a complete configuration reference. + +| Input Variable | Type | Description | +| ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------- | +| `name` | string | The name of the ECS service. This name is required by AWS but is not used by Consul service mesh. | +| `task_definition` | string | The task definition used to start tasks. Set this option to the task definition ARN returned by the `mesh-task` module. | +| `launch_type` | string | The launch type. Consul on ECS supports the `FARGATE` and `EC2` launch types. | +| `propagate_tags` | string | This option must be set to `TASK_DEFINITION` so that tags added by `mesh-task` to the task definition are copied to tasks. | + +After including the ECS service in your Terraform configuration, run `terraform apply` +from your project directory to create the ECS service resource. The ECS service +then starts your application in a task. The task automatically registers itself +into the Consul service catalog during startup. + +-> **NOTE:** If your tasks run in a public subnet, they must have `assign_public_ip = true` +in their [`network_configuration`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#network_configuration) block so that ECS can pull the Docker images. + +### Configure the gateway task module + +Add the `gateway-task` to your Terraform configuration if you want to deploy a mesh gateway. Mesh gateways enable service to service communication across the WAN, as well as federate service mesh traffic across Consul admin partitions and Consul datacenters over the WAN. Refer to the following documentation to learn more about mesh gateways: + +* [WAN Federation via Mesh Gateways](/docs/connect/gateways/mesh-gateway/wan-federation-via-mesh-gateways) +* [Service-to-service Traffic Across Datacenters](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters). + +You must add and configure a `gateway-task` for each Consul datacenter in your network. You must also enable TLS and gossip encryption on all server and client agents in all data centers per the [Requirements](#requirements). Mesh gateways operate by sniffing and extracting the server name indication (SNI) header from the service mesh session and routing the connection to the appropriate destination based on the server name requested. + +The module creates an ECS service and a task definition that includes the following containers: + +- Consul client +- Envoy gateway proxy +- Mesh init + +You will need to provide inputs for the artifacts created by the `gateway-task` module. The following example defines a mesh gateway task called `my-gateway` in a file called `mesh-gateway.tf`: + + + +```hcl +module "my_mesh_gateway" { + source = "hashicorp/consul-ecs/aws//modules/gateway-task" + version = "" + kind = "mesh-gateway" + + family = "my-gateway" + ecs_cluster_arn = "" + subnets = [""] + retry_join = ["
    "] + tls = true + consul_server_ca_cert_arn = "" + gossip_key_secret_arn = "" +} +``` + + + +The following fields are required. Refer to the [module reference documentation](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/gateway-task?tab=inputs) for a complete reference. + +| Input variable | Type | Description | +| --- | --- | --- | +| `source` | string | Specifies the source location of the `gateway-task` module. Must be set to `hashicorp/consul-ecs/aws//modules/gateway-task`. | +| `version` | string | Specifies the version of the `gateway-task` module. | +| `kind` | string | Declares the kind of gateway to create. Must be set to `mesh-gateway` to create a mesh-gateway. | +| `family` | string | Specifies the [ECS task definition family](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#family). The family is also used as the Consul service name by default. | +| `ecs_cluster_arn` | string | Specifies the ARN of the ECS cluster where the mesh gateway task should be launched. | +| `subnets` | list of strings | Specifies the subnet IDs where the task will be launched. | +| `retry_join` | list of strings | Defines a set of arguments to pass to the Consul agent [`-retry-join`](/docs/agent/config/cli-flags#_retry_join) flag. The arguments specify locations of the Consul servers in the local datacenter that Consul client agents can connect to. | +| `tls` | boolean | Set to `true` to enable TLS. | +| `consul_server_ca_cert_arn` | string | Specifies the ARN of the Secrets Manager containing the Consul server CA certificate | +| `gossip_key_secret_arn` | string | Specifies the ARN of the Secrets Manager containing the Consul's gossip encryption key. | + +Refer to the [gateway task configuration examples](#gateway-task-configuration-examples) for additional example configurations. + +#### ECS service + +The ECS service is automatically created by the `gateway-task` module. The service can run one or more instances of the gateway. + +#### Mesh init + +The `mesh-init` container is a short-lived container that sets up the initial configurations for Consul and Envoy. The `gateway-task` module automatically configures the `mesh-init` container based on the inputs specified in the [task definition](#task-definition) and [ECS service](#ecs-service) configuration. + +For additional information, refer to [Task Startup](/docs/ecs/architecture#task-startup) for additional information. + +#### Gateway task configuration examples + +The following examples illustrate how to configure the `gateway-task` for different use cases. + +##### Ingress + +Mesh gateways need to be reachable over the WAN to route traffic between datacenters. Configure the following options in the `gateway-task` to enable ingress through the mesh gateway. + +| Input variable | Type | Description | +| --- | --- | --- | +| `lb_enabled` | Boolean | Set to `true` to automatically deploy and configure a network load balancer for ingress to the mesh gateway. | +| `lb_vpc_id` | string | Specifies the VPC to launch the load balancer in. | +| `lb_subnets` | list of strings | Specifies one or more public subnets to associate with the load balancer. | + + + +```hcl +module "my_mesh_gateway" { + ... + + lb_enabled = true + lb_vpc_id = "" + lb_subnets = [""] +} +``` + + + +Alternatively, you can manually configure ingress to the mesh gateway and provide the `wan_address` and `wan_port` inputs to the `gateway-task` module. The `wan_port` field is optional. Port `8443` is used by default. + + + +```hcl +module "my_mesh_gateway" { + ... + + wan_address = "" + wan_port = +} +``` + + + +Mesh gateways route L4 TCP connections and do not terminate mTLS sessions. If you manually configure [AWS Elastic Load Balancing](https://aws.amazon.com/elasticloadbalancing/) for ingress to a mesh gateway, you must use an AWS [Network Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html) or a [Classic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/introduction.html). + + +##### ACLs + +Configure the following options in the `gateway-task` when ACLs are enabled. + +| Option | Type | Description | +| --- | --- | --- | +| `acl` | Boolean | Set to `true` if ACLs are enabled. | +| `consul_http_addr` | string | Specifies the HTTP `address:port` of the Consul server. Required for the mesh gateway task to log into Consul via the IAM Auth Method to obtain its client and service tokens. | +| `consul_https_ca_cert_arn` | string | Specifies ARN of the Secrets Manager secret that contains the certificate for the Consul HTTPS API. | + + + +```hcl +module "my_mesh_gateway" { + ... + + acls = true + consul_http_addr = "" + consul_https_ca_cert_arn = "" +} +``` + + + +##### WAN federation + +Configure the following options in the `gateway-task` to enable [WAN federation via mesh gateways](/docs/connect/gateways/mesh-gateway/wan-federation-via-mesh-gateways). + +| Option | Type | Description | +| --- | --- | --- | +| `consul_datacenter` | string | Specifies the name of the local Consul datacenter. | +| `consul_primary_datacenter` | string | Specifies the name of the primary Consul datacenter. | +| `enable_mesh_gateway_wan_federation` | Boolean | Set to `true` to enable WAN federation. | +| `enable_acl_token_replication` | Boolean | Set to `true` to enable ACL token replication and allow the creation of local tokens secondary datacenters. | + +The following example shows how to configure the `gateway-task` module. + + + +```hcl +module "my_mesh_gateway" { + ... + + consul_datacenter = "" + consul_primary_datacenter = "" + enable_mesh_gateway_wan_federation = true + enable_acl_token_replication = true +} +``` + + + +When federating Consul datacenters over the WAN with ACLs enabled, [ACL Token replication](/docs/security/acl/acl-federated-datacenters) must be enabled on all server and client agents in all datacenters. + +## Run Terraform You will need to run Terraform to create the task definition. @@ -132,52 +346,7 @@ Terraform should be run in your project directory as follows. Terraform automatically reads all files in the current directory that have a `.tf` file extension. Refer to the [Terraform documentation](https://www.terraform.io/docs) for more information and Terraform best practices. -## ECS Service - -[ECS services](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html) are one of the most common -ways to start tasks using a task definition. - -To define an ECS service, reference the `mesh-task` module's `task_definition_arn` output value -in your `aws_ecs_service` resource. The following example shows how to include the service in the `mesh-task.tf` file. - - - -```hcl -module "my_task" { - source = "hashicorp/consul-ecs/aws//modules/mesh-task" - ... -} - -resource "aws_ecs_service" "my_task" { - name = "my_task_service" - task_definition = module.my_task.task_definition_arn - launch_type = "FARGATE" - propagate_tags = "TASK_DEFINITION" - ... -} -``` - - - -This is a partial configuration to highlight some important fields. -See the [`aws_ecs_service`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) documentation for a complete reference. - -| Input Variable | Type | Description | -| ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------- | -| `name` | string | The name of the ECS service. This is required by AWS but is not used by Consul service mesh. | -| `task_definition` | string | The task definition used to start tasks. Set this to the task definition ARN returned by the `mesh-task` module. | -| `launch_type` | string | The launch type. Consul on ECS supports the `FARGATE` and `EC2` launch types. | -| `propagate_tags` | string | This must be set to `TASK_DEFINITION` so that tags added by `mesh-task` to the task definition are copied to tasks. | - -After including the ECS service in your Terraform configuration, run `terraform apply` -from your project directory to create the ECS service resource. The ECS service will -soon start your application in a task. The task will automatically register itself -into the Consul service catalog during startup. - --> **NOTE:** If your tasks run in a public subnet, they must have `assign_public_ip = true` -in their [`network_configuration`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#network_configuration) block so that ECS can pull the Docker images. - -## Routing +## Configure routes Now that your tasks are registered in the mesh, you're able to use the service mesh to route between them. @@ -239,37 +408,42 @@ module "web" { } ``` -## Bind Address +## Configure the bind address To ensure that your application only receives traffic through the service mesh, -you must change the address that your application is listening on to only the loopback address -(also known as `localhost`, `lo`, and `127.0.0.1`) -so that only the sidecar proxy running in the same task can make requests to it. +you must change the address that your application listens on to the loopback address. The loopback address is also called `localhost`, `lo`, and `127.0.0.1`. +Binding to the loopback address allows the sidecar proxy running in the same task to only make requests within the service mesh. -If your application is listening on all interfaces, e.g. `0.0.0.0`, then other +If your application is listening on all interfaces, such as `0.0.0.0`, then other applications can call it directly, bypassing its sidecar proxy. Changing the listening address is specific to the language and framework you're -using in your application. Regardless of which language/framework you're using, -it's a good practice to make the address configurable via environment variable. +using in your application. Regardless of which language or framework you're using, +binding the loopback address to a dynamic value, such as an environment variable, is a best practice: -For example in Go, you would use: +```bash +export BIND_ADDRESS="127.0.0.1:8080" +``` + +The following examples demonstrate how to bind the loopback address to an environment variable in golang and Django (Python): + + ```go s := &http.Server{ - Addr: "127.0.0.1:8080", + Addr: os.Getenv("BIND_ADDRESS"), ... } log.Fatal(s.ListenAndServe()) ``` -In Django you'd use: - ```bash -python manage.py runserver "127.0.0.1:8080" +python manage.py runserver "$BIND_ADDRESS" ``` -## Next Steps + + +## Next steps - Follow the [Secure Configuration](/docs/ecs/terraform/secure-configuration) to get production-ready. - Now that your applications are running in the service mesh, read about diff --git a/website/content/docs/ecs/terraform/migrate-existing-tasks.mdx b/website/content/docs/ecs/terraform/migrate-existing-tasks.mdx index 7281a7a11..574e1fda0 100644 --- a/website/content/docs/ecs/terraform/migrate-existing-tasks.mdx +++ b/website/content/docs/ecs/terraform/migrate-existing-tasks.mdx @@ -68,7 +68,7 @@ The `mesh-task` module is used as follows: ```hcl module "my_task" { - source = "hashicorp/consul/aws-ecs//modules/mesh-task" + source = "hashicorp/consul/aws//modules/mesh-task" version = "" family = "my_task" @@ -97,7 +97,7 @@ module "my_task" { The main differences are: -- You must remove the `execution_role_arn` and `task_role_arn` fields. The `mesh-task` module will create the task and execution roles. +- You should remove the `execution_role_arn` and `task_role_arn` fields. The `mesh-task` module creates the task and execution roles by default. If you need to use existing IAM role(s), set the `task_role` and `execution_role` fields to pass in existing roles. - You must set the `port` field to the port that your application listens on. If your application has no listening port, set `outbound_only = true` and remove the `port` field. - You must add the `retry_join` field. This specifies the location of your Consul servers so that your task can join the mesh. @@ -111,5 +111,5 @@ resource. Now that your task(s) are migrated to the `mesh-task` module, -- Start at the [ECS Service section](/docs/ecs/terraform/install#ecs-service) of the Installation Guide to continue installing Consul on ECS. +- Start at the [ECS Service section](/docs/ecs/terraform/install#configure-an-ecs-service-for-the-mesh-task-module) of the Installation Guide to continue installing Consul on ECS. - Refer to the [`mesh-task` reference documentation](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/mesh-task?tab=inputs) for all available inputs to your mesh tasks. diff --git a/website/content/docs/ecs/terraform/secure-configuration.mdx b/website/content/docs/ecs/terraform/secure-configuration.mdx index 6a932960e..24e3e165c 100644 --- a/website/content/docs/ecs/terraform/secure-configuration.mdx +++ b/website/content/docs/ecs/terraform/secure-configuration.mdx @@ -7,71 +7,120 @@ description: >- # Secure Configuration -For a production-ready installation of Consul on ECS, you will need to make sure that the cluster is secured. -A secure Consul cluster should include the following: +This topic describes how to enable Consul security features for your production workloads. -1. [TLS Encryption](/docs/security/encryption#rpc-encryption-with-tls) for RPC communication between Consul clients and servers. -1. [Gossip Encryption](/docs/security/encryption#gossip-encryption) for encrypting gossip traffic. -1. [Access Control (ACLs)](/docs/security/acl) for authentication and authorization for Consul clients and services on the mesh. +## Overview --> **NOTE:** This page assumes that you have already configured your Consul server with the above features. +To enable security in your production workloads, you must deploy the [ACL controller](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/acl-controller), which provisions tokens for other service mesh tasks. Refer to [Architecture](/docs/ecs/architecture#acl-controller) to learn more about the ACL controller. -## Deploy ACL Controller +The controller cannot provision tokens for itself, so you must create the token for the ACL controller. The following steps describe the overall process of enabling security features for your production workloads: -Before deploying your service, you will need to deploy the [ACL controller](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/acl-controller) so that it can provision the necessary tokens -for tasks on the service mesh. To learn more about the ACL Controller, please see [Automatic ACL Token Provisioning](/docs/ecs/architecture#automatic-acl-token-provisioning). +1. Enable the security features on your Consul server cluster per the [Prerequisites](#prerequisites). +1. Create the ACL token for the ACL controller in the datacenter. +1. Create a Secrets Manager secret containing the ACL controller's token. +1. Create a Secrets Manager secret containing the Consul CA certificate. +1. Deploy the ACL controller +1. Deploy the other services on the mesh. -To deploy the controller, you will first need to store an ACL token with `acl:write` and `operator:write` privileges, -and a CA certificate for the Consul server in AWS Secrets Manager. +## Prerequisites -```hcl -resource "aws_secretsmanager_secret" "bootstrap_token" { - name = "bootstrap-token" -} +Implement the following security features for your Consul server clusters before applying them to your workloads: -resource "aws_secretsmanager_secret_version" "bootstrap_token" { - secret_id = aws_secretsmanager_secret.bootstrap_token.id - secret_string = "" -} +1. [TLS encryption](/docs/security/encryption#rpc-encryption-with-tls) for RPC communication between Consul clients and servers. +1. [Gossip encryption](/docs/security/encryption#gossip-encryption) for encrypting gossip traffic. +1. [Access control lists (ACLs)](/docs/security/acl) for authentication and authorization for Consul clients and services on the mesh. -resource "aws_secretsmanager_secret" "ca_cert" { - name = "server-ca-cert" -} +## Auth Method -resource "aws_secretsmanager_secret_version" "ca_cert" { - secret_id = aws_secretsmanager_secret.ca_cert.id - secret_string = "" -} -``` +Consul on ECS uses the [AWS IAM Auth Method](/docs/ecs/architecture#aws-iam-auth-method) to enable +tasks to automatically obtain Consul ACL tokens during startup. -Use the [`acl-controller` terraform module](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/acl-controller?tab=inputs) to deploy the controller: +With the Terraform modules for Consul on ECS, the auth method is supported by default when ACLs are +enabled. The ACL controller sets up the auth method on the Consul servers. The `mesh-task` module +configures the ECS task definition to be compatible with the auth method. -```hcl -module "acl_controller" { - source = "hashicorp/consul/aws-ecs//modules/acl-controller" - consul_bootstrap_token_secret_arn = aws_secretsmanager_secret.bootstrap_token.arn - consul_server_http_addr = "https://consul-server.example.com:8501" - consul_server_ca_cert_arn = aws_secretsmanager_secret.ca_cert.arn - ecs_cluster_arn = "arn:aws:ecs:my-region:111111111111:cluster/consul-ecs" - region = "my-region" - subnets = ["subnet-abcdef123456789"] - name_prefix = "consul-ecs" -} -``` +A unique task IAM role is required for each ECS task family. Task IAM roles must not be shared by +different task families. This is because the task family represents only one Consul service and the +task IAM role must encode the Consul service name. -The `name_prefix` parameter is used to prefix any secrets that the ACL controller will -update in AWS Secrets Manager. +By default, the `mesh-task` module will create and configure the task IAM role for you. --> **NOTE:** Make sure that the `name_prefix` is unique for each ECS cluster where you are -deploying this controller. +-> **NOTE**: When passing an existing IAM role to the `mesh-task` module using the `task_role` input +variable, you must configure the IAM role as described in [ECS Task Role +Configuration](/docs/ecs/manual/secure-configuration#ecs-task-role-configuration) to be compatible with +the AWS IAM auth method. -## Deploy Services +## ACL controller -Once the ACL controller is up and running, you will be able to deploy services on the mesh using the [`mesh-task` module](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/mesh-task). -Start with the basic configuration for the [Task Module](/docs/ecs/terraform/install#task-module) and specify additional settings to make the configuration production-ready. +1. Create a policy that grants `acl:write` and `operator:write` access for the controller. Refer to the [ACL policies documentation](/docs/security/acl/acl-policies) for instructions. +1. Create a token and link it to the ACL controller policy. Refer to the [ACL tokens documentation](/docs/security/acl/acl-tokens) for instructions. +1. Create a Secrets Manager secret containing the ACL controller's token and a Secrets Manager secret containing the Consul CA cert. -First, you will need to create an AWS Secrets Manager secret for the gossip encryption key that the Consul clients -should use. + ```hcl + resource "aws_secretsmanager_secret" "bootstrap_token" { + name = "bootstrap-token" + } + + resource "aws_secretsmanager_secret_version" "bootstrap_token" { + secret_id = aws_secretsmanager_secret.bootstrap_token.id + secret_string = "" + } + + resource "aws_secretsmanager_secret" "ca_cert" { + name = "server-ca-cert" + } + + resource "aws_secretsmanager_secret_version" "ca_cert" { + secret_id = aws_secretsmanager_secret.ca_cert.id + secret_string = "" + } + ``` + +1. Use the [`acl-controller` terraform module](https://registry.terraform.io/modules/hashicorp/consul-ecs/aws/latest/submodules/acl-controller?tab=inputs) to deploy the controller. You must provide the ARN's for the token and CA cert in the `consul_bootstrap_token_secret_arn` and `consul_server_ca_cert_arn` fields, respectively. + + ```hcl + module "acl_controller" { + source = "hashicorp/consul/aws//modules/acl-controller" + version = "" + + consul_bootstrap_token_secret_arn = aws_secretsmanager_secret.bootstrap_token.arn + consul_server_http_addr = "https://consul-server.example.com:8501" + consul_server_ca_cert_arn = aws_secretsmanager_secret.ca_cert.arn + ecs_cluster_arn = "arn:aws:ecs:us-east-1:111111111111:cluster/consul-ecs" + region = "us-east-1" + subnets = ["subnet-abcdef123456789"] + name_prefix = "consul-ecs" + } + ``` + +The following table describes the required input variables for the `acl-controller` module. + +| Input Variable | Type | Description | +| ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `source` | string | The location of the `acl-controller` module source. | +| `version` | string | The version of the `acl-controller` module. | +| `consul_bootstrap_token_secret_arn` | string | The Secrets Manager secret containing an ACL token for the ACL controller. The ACL token must have `acl:write` and `operator:write` permissions. | +| `consul_server_http_addr` | string | The HTTP(S) address of the Consul servers. | +| `consul_server_ca_cert_arn` | string | (Optional) The Secrets Manager secret containing the CA cert for HTTPS communication with Consul servers. Required if the server's certificate is self-signed or signed by an internal CA. This is not required for Consul servers in HCP. | +| `ecs_cluster_arn` | string | The ECS cluster where the ACL controller will be deployed. | +| `region` | string | The AWS region where the AWS resources will be created. | +| `subnets` | list | The AWS VPC subnet ids where the ACL controller will be deployed. | +| `name_prefix` | string | AWS resources created by the `acl-controller` module will include this prefix in the resource name. | + + +If you are using Consul Enterprise, see Admin Partitions and Namespaces for +additional configuration required to support Consul Enterprise on ECS. + + +## Deploy your services + +Follow the instructions described in [Create a task definition](/docs/ecs/terraform/install#create-the-task-definition) to create the basic configuration for the task module. Add the following additional configurations to make the configuration production-ready. + +### Create an AWS Secrets Manager secret + +The secret stores the gossip encryption key that the Consul clients use. + + ```hcl resource "aws_secretsmanager_secret" "gossip_key" { @@ -83,26 +132,43 @@ resource "aws_secretsmanager_secret_version" "gossip_key" { secret_string = "" } ``` + -Next, add the following configurations to enable secure deployment. Note that the `acl_secret_name_prefix` +### Enable secure deployment + +Add the following configurations to enable secure deployment. The `acl_secret_name_prefix` should be the same as the `name_prefix` you provide to the ACL controller module. ```hcl module "my_task" { - source = "hashicorp/consul/aws-ecs//modules/mesh-task" - family = "my_task" + source = "hashicorp/consul/aws//modules/mesh-task" + version = "" ... - tls = true - consul_server_ca_cert_arn = aws_secretsmanager_secret.ca_cert.arn - gossip_key_secret_arn = aws_secretsmanager_secret.gossip_key.arn + tls = true + consul_server_ca_cert_arn = aws_secretsmanager_secret.ca_cert.arn + gossip_key_secret_arn = aws_secretsmanager_secret.gossip_key.arn - acls = true - consul_client_token_secret_arn = module.acl_controller.client_token_secret_arn - acl_secret_name_prefix = "consul-ecs" + acls = true + consul_http_addr = "https://consul-server.example.com:8501" + consul_https_ca_cert_arn = aws_secretsmanager_secret.ca_cert.arn } ``` -Now you can deploy your services! Follow the rest of the steps in the [Installation instructions](/docs/ecs/terraform/install#task-module) -to deploy and connect your services. +The following table explains the `mesh-task` input variables relevant to a secure configuration: + +| Input Variable | Type | Description | +| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- | +| `tls` | boolean | If true, TLS is enabled for RPC communication with the Consul servers. | +| `consul_server_ca_cert_arn` | string | The Secrets Manager secret containing the CA certificate for RPC communication with the Consul servers when TLS is enabled. | +| `gossip_key_secret_arn` | string | The Secrets Manager secret containing Consul's gossip encryption key. | +| `acls` | boolean | If true, ACLs are enabled. | +| `consul_http_addr` | string | The Consul server address. Required when `acls = true` in order to log in to Consul's AWS IAM auth method to obtain ACL tokens. | +| `consul_https_ca_cert_arn` | string | (optional) The Secrets Manager secret containing the CA cert for HTTPS communication with Consul servers. Required if the server's certificate is self-signed or signed by an internal CA. This is not required for Consul servers in HCP. | + +Complete the following steps described in the Installation with Terraform chapter to deploy and connect your services: + +1. [Run Terraform](/docs/ecs/terraform/install#run-terraform) +1. [Configure routes](/docs/ecs/terraform/install#configure-routes) +1. [Configure the bind address](/docs/ecs/terraform/install#configure-the-bind-address) diff --git a/website/content/docs/k8s/annotations-and-labels.mdx b/website/content/docs/k8s/annotations-and-labels.mdx index b98346563..2eba477f0 100644 --- a/website/content/docs/k8s/annotations-and-labels.mdx +++ b/website/content/docs/k8s/annotations-and-labels.mdx @@ -17,7 +17,7 @@ This allows the user to configure natively configure Consul on select Kubernetes ## Annotations -Resource annotations could be used on the Kubernetes pod to control connect-inject behavior. +The following Kubernetes resource annotations could be used on a pod to control connect-inject behavior: - `consul.hashicorp.com/connect-inject` - If this is "true" then injection is enabled. If this is "false" then injection is explicitly disabled. @@ -67,55 +67,91 @@ Resource annotations could be used on the Kubernetes pod to control connect-inje - `consul.hashicorp.com/connect-service-upstreams` - The list of upstream services that this pod needs to connect to via Connect along with a static local port to listen for those connections. When transparent proxy is enabled, - this annotation is optional. + this annotation is optional. There are a few formats this annotation can take: - - Services + - Unlabeled: + Use the unlabeled annotation format to specify a service name, Consul Enterprise namespaces and partitions, and + datacenters. To use [cluster peering](/docs/connect/cluster-peering/k8s) with upstreams, use the following + labeled format. + - Service name: Place the service name at the beginning of the annotation to specify the upstream service. You can + also append the datacenter where the service is deployed (optional). + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name]:[port]:[optional datacenter]" + ``` + - Namespace (requires Consul Enterprise 1.7+): Upstream services may be running in different a namespace. Place + the upstream namespace after the service name. For additional details about configuring the injector, refer to + [Consul Enterprise Namespaces](#consul-enterprise-namespaces) . + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].[service-namespace]:[port]:[optional datacenter]" + ``` + If the namespace is not specified, the annotation defaults to the namespace of the source service. + If you are not using Consul Enterprise 1.7+, Consul interprets the value placed in the namespace position as part of the service name. - The name of the service is the name of the service registered with Consul. You can optionally specify datacenters with this annotation. + - Admin partitions (requires Consul Enterprise 1.11+): Upstream services may be running in a different + partition. You must specify the namespace when specifying a partition. Place the partition name after the namespace. If you specify the name of the datacenter (optional), it must be the local datacenter. Communicating across partitions using this method is only supported within a + datacenter. For cross partition communication across datacenters, refer to [cluster + peering](/docs/connect/cluster-peering/k8s). + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].[service-namespace].[service-partition]:[port]:[optional datacenter]" + ``` + - [Prepared queries](/docs/connect/proxies#dynamic-upstreams-require-native-integration): Prepend the annotation + with `prepared_query` and place the name of the query at the beginning of the string. + ```yaml + annotations: + 'consul.hashicorp.com/connect-service-upstreams': 'prepared_query:[query name]:[port]' + ``` - ```yaml - annotations: - "consul.hashicorp.com/connect-service-upstreams":"[service-name]:[port]:[optional datacenter]" - ``` + - Labeled (requires Consul for Kubernetes v0.45.0+): + The labeled format is required when using the cluster peering feature and specifying an upstream in another + peer. You can specify a Consul Enterprise namespace, partition, or datacenter. The format supports only one peer, datacenter, or partition. + - Service name: Place the service name at the beginning of the annotation followed by `.svc` to specify the upstream service. + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].svc:[port]" + ``` + - Peer or datacenter: Place the peer or datacenter after `svc.` followed by either `peer` or `dc` and the port number. + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].svc.[service-peer].peer:[port]" + ``` + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].svc.[service-dc].dc:[port]" + ``` + - Namespace (required Consul Enterprise): Place the namespace after `svc.` followed by `ns` and the port number. + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].svc.[service-namespace].ns:[port]" + ``` + When specifying a peer, datacenter, or admin partition when namespaces are enabled, you must + provide the namespace . + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].svc.[service-namespace].ns.[service-peer].peer:[port]" + ``` + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].svc.[service-namespace].ns.[service-partition].ap:[port]" + ``` + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name].svc.[service-namespace].ns.[service-dc].dc:[port]" + ``` - - Consul Enterprise Namespaces + - Multiple upstreams: Delimit multiple services or upstreams with commas. You can specify any of the unlabeled, labeled, or prepared query formats when using the supported versions for the formats. - If running Consul Enterprise 1.7+, your upstream services may be running in different - namespaces. The upstream namespace can be specified after the service name - as `[service-name].[namespace]`. See [Consul Enterprise Namespaces](#consul-enterprise-namespaces) - below for more details on configuring the injector. + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name]:[port]:[optional datacenter],[service-name]:[port]:[optional datacenter]" + ``` - ```yaml - annotations: - "consul.hashicorp.com/connect-service-upstreams":"[service-name].[service-namespace]:[port]:[optional datacenter]" - ``` - - -> **NOTE:** If the namespace is not specified it will default to the namespace - of the source service. - - ~> **WARNING:** Setting a namespace when not using Consul Enterprise or using a version < 1.7 - is not supported. It will be treated as part of the service name. - - - [Prepared Query](/docs/connect/proxies#dynamic-upstreams-require-native-integration) - - ```yaml - annotations: - 'consul.hashicorp.com/connect-service-upstreams': 'prepared_query:[query name]:[port]' - ``` - - - Multiple Upstreams - - If you would like to specify multiple services or upstreams, delimit them with commas - - ```yaml - annotations: - "consul.hashicorp.com/connect-service-upstreams":"[service-name]:[port]:[optional datacenter],[service-name]:[port]:[optional datacenter]" - ``` - - ```yaml - annotations: - "consul.hashicorp.com/connect-service-upstreams":"[service-name]:[port]:[optional datacenter],prepared_query:[query name]:[port]" - ``` + ```yaml + annotations: + "consul.hashicorp.com/connect-service-upstreams":"[service-name]:[port]:[optional datacenter],prepared_query:[query name]:[port],[service-name].svc:[port]" + ``` - `consul.hashicorp.com/envoy-extra-args` - A space-separated list of [arguments](https://www.envoyproxy.io/docs/envoy/latest/operations/cli) to be passed to the injected envoy binary. @@ -182,6 +218,18 @@ Resource annotations could be used on the Kubernetes pod to control connect-inje - `consul.hashicorp.com/merged-metrics-port` - Override the default Helm value [`connectInject.metrics.defaultMergedMetricsPort`](/docs/k8s/helm#v-connectinject-metrics-defaultmergedmetricsport). - `consul.hashicorp.com/prometheus-scrape-port` - Override the default Helm value [`connectInject.metrics.defaultPrometheusScrapePort`](/docs/k8s/helm#v-connectinject-metrics-defaultprometheusscrapeport). - `consul.hashicorp.com/prometheus-scrape-path` - Override the default Helm value [`connectInject.metrics.defaultPrometheusScrapePath`](/docs/k8s/helm#v-connectinject-metrics-defaultprometheusscrapepath). +- `consul.hashicorp.com/prometheus-ca-file` - Local filesystem path to a CA file for Envoy to use + when serving TLS on the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr` + is set in proxy config. +- `consul.hashicorp.com/prometheus-ca-path` - Local filesystem path to a directory of CA certificates + for Envoy to use when serving TLS on the Prometheus metrics endpoint. Only applicable when + `envoy_prometheus_bind_addr` is set in proxy config. +- `consul.hashicorp.com/prometheus-cert-file` - Local filesystem path to a certificate file for Envoy to use + when serving TLS on the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr` + is set in proxy config. +- `consul.hashicorp.com/prometheus-key-file` - Local filesystem path to a private key file for Envoy to use + when serving TLS on the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr` + is set in proxy config. - `consul.hashicorp.com/service-metrics-port` - Set the port where the Connect service exposes metrics. - `consul.hashicorp.com/service-metrics-path` - Set the path where the Connect service exposes metrics. - `consul.hashicorp.com/connect-inject-mount-volume` - Comma separated list of container names to mount the connect-inject volume into. The volume will be mounted at `/consul/connect-inject`. The connect-inject volume contains Consul internals data needed by the other sidecar containers, for example the `consul` binary, and the Pod's Consul ACL token. This data can be valuable for advanced use-cases, such as making requests to the Consul API from within application containers. diff --git a/website/content/docs/k8s/installation/compatibility.mdx b/website/content/docs/k8s/compatibility.mdx similarity index 93% rename from website/content/docs/k8s/installation/compatibility.mdx rename to website/content/docs/k8s/compatibility.mdx index cb2370f83..2d651401d 100644 --- a/website/content/docs/k8s/installation/compatibility.mdx +++ b/website/content/docs/k8s/compatibility.mdx @@ -14,11 +14,11 @@ For every release of Consul on Kubernetes, a Helm chart, `consul-k8s-control-pla Starting with Consul Kubernetes 0.33.0, Consul Kubernetes versions all of its components (`consul-k8s` CLI, `consul-k8s-control-plane`, and Helm chart) with a single semantic version. -| Consul Version | Compatible consul-k8s Versions | -| -------------- | ------------------------------- | -| 1.12.x | 0.43.0 - latest | -| 1.11.x | 0.39.0 - 0.42.0, 0.44.0 | -| 1.10.x | 0.33.0 - 0.38.0 | +| Consul Version | Compatible consul-k8s Versions | +| -------------- | -------------------------------- | +| 1.12.x | 0.43.0 - latest | +| 1.11.x | 0.39.0 - 0.42.0, 0.44.0 - latest | +| 1.10.x | 0.33.0 - 0.38.0 | ### Prior to version 0.33.0 diff --git a/website/content/docs/k8s/helm.mdx b/website/content/docs/k8s/helm.mdx index 53274177a..83d6d73bf 100644 --- a/website/content/docs/k8s/helm.mdx +++ b/website/content/docs/k8s/helm.mdx @@ -19,26 +19,26 @@ with Consul. Use these links to navigate to a particular top-level stanza. -- [`global`](#global) -- [`server`](#server) -- [`externalServers`](#externalservers) -- [`client`](#client) -- [`dns`](#dns) -- [`ui`](#ui) -- [`syncCatalog`](#synccatalog) -- [`connectInject`](#connectinject) -- [`controller`](#controller) -- [`meshGateway`](#meshgateway) -- [`ingressGateways`](#ingressgateways) -- [`terminatingGateways`](#terminatinggateways) -- [`apiGateway`](#apigateway) -- [`webhookCertManager`](#webhookcertmanager) -- [`prometheus`](#prometheus) -- [`tests`](#tests) +- [`global`](#h-global) +- [`server`](#h-server) +- [`externalServers`](#h-externalservers) +- [`client`](#h-client) +- [`dns`](#h-dns) +- [`ui`](#h-ui) +- [`syncCatalog`](#h-synccatalog) +- [`connectInject`](#h-connectinject) +- [`controller`](#h-controller) +- [`meshGateway`](#h-meshgateway) +- [`ingressGateways`](#h-ingressgateways) +- [`terminatingGateways`](#h-terminatinggateways) +- [`apiGateway`](#h-apigateway) +- [`webhookCertManager`](#h-webhookcertmanager) +- [`prometheus`](#h-prometheus) +- [`tests`](#h-tests) ## All Values -### global +### global ((#h-global)) - `global` ((#v-global)) - Holds values that affect multiple components of the chart. @@ -61,6 +61,10 @@ Use these links to navigate to a particular top-level stanza. (see `-domain` (https://www.consul.io/docs/agent/config/cli-flags#_domain)) and the domain services synced from Consul into Kubernetes will have, e.g. `service-name.service.consul`. + - `peering` ((#v-global-peering)) - [Experimental] Configures the Cluster Peering feature. Requires Consul v1.13+ and Consul-K8s v0.45+. + - `enabled` ((#v-global-peering-enabled)) (`boolean: false`) - If true, the Helm chart enables Cluster Peering for the cluster. This option enables peering controllers and + allows use of the PeeringAcceptor and PeeringDialer CRDs for establishing service mesh peerings. + - `adminPartitions` ((#v-global-adminpartitions)) - Enabling `adminPartitions` allows creation of Admin Partitions in Kubernetes clusters. It additionally indicates that you are running Consul Enterprise v1.11+ with a valid Consul Enterprise license. Admin partitions enables deploying services across partitions, while sharing @@ -544,7 +548,7 @@ Use these links to navigate to a particular top-level stanza. - `consulAPITimeout` ((#v-global-consulapitimeout)) (`string: 5s`) - The time in seconds that the consul API client will wait for a response from the API before cancelling the request. -### server +### server ((#h-server)) - `server` ((#v-server)) - Server, when enabled, configures a server cluster to run. This should be disabled if you plan on connecting to a Consul cluster external to @@ -854,7 +858,7 @@ Use these links to navigate to a particular top-level stanza. feature, in case kubernetes cluster is behind egress http proxies. Additionally, it could be used to configure custom consul parameters. -### externalServers +### externalServers ((#h-externalservers)) - `externalServers` ((#v-externalservers)) - Configuration for Consul servers when the servers are running outside of Kubernetes. When running external servers, configuring these values is recommended @@ -897,7 +901,7 @@ Use these links to navigate to a particular top-level stanza. -o jsonpath="{.clusters[?(@.name=='')].cluster.server}" ``` -### client +### client ((#h-client)) - `client` ((#v-client)) - Values that configure running a Consul client on Kubernetes nodes. @@ -1162,7 +1166,7 @@ Use these links to navigate to a particular top-level stanza. ... ``` -### dns +### dns ((#h-dns)) - `dns` ((#v-dns)) - Configuration for DNS configuration within the Kubernetes cluster. This creates a service that routes to all agents (client or server) @@ -1193,7 +1197,7 @@ Use these links to navigate to a particular top-level stanza. This should be a multi-line string mapping directly to a Kubernetes ServiceSpec object. -### ui +### ui ((#h-ui)) - `ui` ((#v-ui)) - Values that configure the Consul UI. @@ -1295,7 +1299,7 @@ Use these links to navigate to a particular top-level stanza. - `service` ((#v-ui-dashboardurltemplates-service)) (`string: ""`) - Sets https://www.consul.io/docs/agent/options#ui_config_dashboard_url_templates_service. -### syncCatalog +### syncCatalog ((#h-synccatalog)) - `syncCatalog` ((#v-synccatalog)) - Configure the catalog sync process to sync K8S with Consul services. This can run bidirectional (default) or unidirectionally (Consul @@ -1471,7 +1475,7 @@ Use these links to navigate to a particular top-level stanza. anotherLabelKey: another-label-value ``` -### connectInject +### connectInject ((#h-connectinject)) - `connectInject` ((#v-connectinject)) - Configures the automatic Connect sidecar injector. @@ -1714,7 +1718,7 @@ Use these links to navigate to a particular top-level stanza. - `initContainer` ((#v-connectinject-initcontainer)) (`map`) - The resource settings for the Connect injected init container. -### controller +### controller ((#h-controller)) - `controller` ((#v-controller)) - Controller handles config entry custom resources. Requires consul >= 1.8.4. @@ -1767,7 +1771,7 @@ Use these links to navigate to a particular top-level stanza. - `secretKey` ((#v-controller-acltoken-secretkey)) (`string: null`) - The key within the Vault secret that holds the ACL token. -### meshGateway +### meshGateway ((#h-meshgateway)) - `meshGateway` ((#v-meshgateway)) - Mesh Gateways enable Consul Connect to work across Consul datacenters. @@ -1916,7 +1920,7 @@ Use these links to navigate to a particular top-level stanza. 'annotation-key': annotation-value ``` -### ingressGateways +### ingressGateways ((#h-ingressgateways)) - `ingressGateways` ((#v-ingressgateways)) - Configuration options for ingress gateways. Default values for all ingress gateways are defined in `ingressGateways.defaults`. Any of @@ -2032,7 +2036,7 @@ Use these links to navigate to a particular top-level stanza. - `name` ((#v-ingressgateways-gateways-name)) (`string: ingress-gateway`) -### terminatingGateways +### terminatingGateways ((#h-terminatinggateways)) - `terminatingGateways` ((#v-terminatinggateways)) - Configuration options for terminating gateways. Default values for all terminating gateways are defined in `terminatingGateways.defaults`. Any of @@ -2134,7 +2138,7 @@ Use these links to navigate to a particular top-level stanza. - `name` ((#v-terminatinggateways-gateways-name)) (`string: terminating-gateway`) -### apiGateway +### apiGateway ((#h-apigateway)) - `apiGateway` ((#v-apigateway)) - Configuration settings for the Consul API Gateway integration @@ -2233,7 +2237,7 @@ Use these links to navigate to a particular top-level stanza. - `initCopyConsulContainer` ((#v-apigateway-initcopyconsulcontainer)) (`map`) - The resource settings for the `copy-consul-bin` init container. -### webhookCertManager +### webhookCertManager ((#h-webhookcertmanager)) - `webhookCertManager` ((#v-webhookcertmanager)) - Configuration settings for the webhook-cert-manager `webhook-cert-manager` ensures that cert bundles are up to date for the mutating webhook. @@ -2242,14 +2246,14 @@ Use these links to navigate to a particular top-level stanza. This should be a multi-line string matching the Toleration array in a PodSpec. -### prometheus +### prometheus ((#h-prometheus)) - `prometheus` ((#v-prometheus)) - Configures a demo Prometheus installation. - `enabled` ((#v-prometheus-enabled)) (`boolean: false`) - When true, the Helm chart will install a demo Prometheus server instance alongside Consul. -### tests +### tests ((#h-tests)) - `tests` ((#v-tests)) - Control whether a test Pod manifest is generated when running helm template. When using helm install, the test Pod is not submitted to the cluster so this diff --git a/website/content/docs/k8s/operations/gossip-encryption-key-rotation.mdx b/website/content/docs/k8s/operations/gossip-encryption-key-rotation.mdx index 6270f926f..3f02c449b 100644 --- a/website/content/docs/k8s/operations/gossip-encryption-key-rotation.mdx +++ b/website/content/docs/k8s/operations/gossip-encryption-key-rotation.mdx @@ -161,7 +161,7 @@ The following steps need only be performed once in any single datacenter if your 1. `kubectl exec` into a Consul Agent pod (server or client) and add the new key to the Consul Keyring. This can be performed by running the following command: ```shell-session - kubectl exec -it consul-server-0 -- /bin/sh + $ kubectl exec -it consul-server-0 -- /bin/sh ``` 1. **Note:** If ACLs are enabled, export the bootstrap token as the CONSUL_HTTP_TOKEN to perform all `consul keyring` operations. diff --git a/website/content/docs/lambda/index.mdx b/website/content/docs/lambda/index.mdx index f95da5496..d8540285e 100644 --- a/website/content/docs/lambda/index.mdx +++ b/website/content/docs/lambda/index.mdx @@ -26,8 +26,8 @@ also be manually registered into Consul when using Lambda registrator is not pos See the [Registration page](/docs/lambda/registration) for more information about registring Lambda functions into Consul. -### Invoking Lambda Functions +### Invoking Lambda Functions from Consul Service Mesh Lambda functions can be invoked by any mesh service directly from connect proxies or through terminating gateways. The [Invocation page](/docs/lambda/invocation) -explains how to invoke Lambda functions from Consul connect services. +explains how to invoke Lambda functions from Consul service mesh services. diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx index 46379d965..357fd7032 100644 --- a/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx +++ b/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx @@ -72,6 +72,6 @@ This release includes the following features and capabilities: ## Kubernetes Gateway API Specification -Supported version of the Gateway API spec: `v1alpha2`(v0.4.1) +Supported version of the [Gateway API](https://gateway-api.sigs.k8s.io/) spec: `v1alpha2`(v0.4.1) For more detailed information, please refer to the [product documentation](/docs/api-gateway). diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_2_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_2_x.mdx index cb5370b3f..d7f26e5b1 100644 --- a/website/content/docs/release-notes/consul-api-gateway/v0_2_x.mdx +++ b/website/content/docs/release-notes/consul-api-gateway/v0_2_x.mdx @@ -18,9 +18,9 @@ description: >- was reachable, it didn’t allow users to set the more granular restrictions or permissions that they may expect. - This version of API Gateway implements Cross Namespace Reference Policies - and requires them when routes are in a different namespace than the services - (as specified by the `backendRef`) they are routing traffic to. + This version of API Gateway implements Cross Namespace Reference Policies + and requires them when routes are in a different namespace than the services + (as specified by the `backendRefs`) they are routing traffic to. ## Supported Software @@ -33,7 +33,7 @@ description: >- ## Kubernetes Gateway API Specification -Supported version of the Gateway API spec: `v1alpha2`(v0.4.1) +Supported version of the [Gateway API](https://gateway-api.sigs.k8s.io/) spec: `v1alpha2`(v0.4.1) ## Upgrading diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_3_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_3_x.mdx new file mode 100644 index 000000000..788564580 --- /dev/null +++ b/website/content/docs/release-notes/consul-api-gateway/v0_3_x.mdx @@ -0,0 +1,58 @@ +--- +layout: docs +page_title: 0.3.x +description: >- + Consul API Gateway release notes for version 0.3.x +--- + +# Consul API Gateway 0.3.0 + +## Release Highlights + +- **Improved High Availability and Scalability via Multiple Instances per Gateway:** + This release introduces support for deploying multiple instances of a logical + gateway (i.e. a K8s `gateway` config object). You can use an external load + balancing service to distribute incoming traffic across the instances of a + gateway. This provides higher availability of gateway services and also allows + a single logical gateway to handle a volume of network traffic that is much + greater than what a single instance of a gateway can. + + Administrators can set the number of instances independently for each logical + gateway. Administrators can also set, in the `gatewayClassConfig`, the + minimum, maximum and default number of gateway instances. These are applied + per `gateway` to all `gateways` in that `gatewayClass`. The minimum and + maximum settings will override settings configured on any given `gateway`. + + ~> **Note:** There is a hard limit of eight (8) instances per `gateway` and it can + not be overridden. This hard limit will probably be increased in the future. + +- **Cross Namespace Reference Policies for Certificates:** + Reference policies is a security feature introduced in the 0.2 release. In + this release, Reference Policies are now required for TLS certificates when a + `listener` has a `certificateRef` that is in a different `namespace`. + +## Supported Software + +- Consul 1.11.2+ +- HashiCorp Consul Helm chart 0.45.0+ +- Kubernetes 1.21+ + - Kubernetes 1.24 is not supported at this time. +- Kubectl 1.21+ +- Envoy proxy support is determined by the Consul version deployed. Refer to + [Envoy Integration](/docs/connect/proxies/envoy) for details. + +## Kubernetes Gateway API Specification + +Supported version of the [Gateway API](https://gateway-api.sigs.k8s.io/) spec: `v1alpha2`(v0.4.1) + +## Upgrading + +For detailed information on upgrading, please refer to the [upgrade details page](/docs/api-gateway/upgrade-specific-versions) + +## Changelogs + +The changelogs for this major release version and any maintenance versions are listed below. + +~> **Note:** The following link will take you to the changelogs on the GitHub website. + +- [0.3.0](https://github.com/hashicorp/consul-api-gateway/releases/tag/v0.3.0) diff --git a/website/data/api-docs-nav-data.json b/website/data/api-docs-nav-data.json index ee1df87c2..ab494f91b 100644 --- a/website/data/api-docs-nav-data.json +++ b/website/data/api-docs-nav-data.json @@ -89,6 +89,10 @@ "title": "Catalog", "path": "catalog" }, + { + "title": "Cluster Peering", + "path": "peering" + }, { "title": "Config", "path": "config" diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index e917d90db..fd3bb1888 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -321,6 +321,23 @@ } ] }, + { + "title": "Cluster Peering", + "routes": [ + { + "title": "What is Cluster Peering", + "path": "connect/cluster-peering" + }, + { + "title": "Create and Manage Peering Connections", + "path": "connect/cluster-peering/create-manage-peering" + }, + { + "title": "Cluster Peering on Kubernetes", + "path": "connect/cluster-peering/k8s" + } + ] + }, { "title": "Nomad", "path": "connect/nomad" @@ -553,10 +570,6 @@ "path": "k8s/installation/vault/wan-federation" } ] - }, - { - "title": "Compatibility Matrix", - "path": "k8s/installation/compatibility" } ] }, @@ -674,6 +687,10 @@ } ] }, + { + "title": "Compatibility Matrix", + "path": "k8s/compatibility" + }, { "title": "Helm Chart Configuration", "path": "k8s/helm" @@ -1253,12 +1270,16 @@ "title": "Consul API Gateway", "routes": [ { - "title": "v0.1.x", - "path": "release-notes/consul-api-gateway/v0_1_x" + "title": "v0.3.x", + "path": "release-notes/consul-api-gateway/v0_3_x" }, { "title": "v0.2.x", "path": "release-notes/consul-api-gateway/v0_2_x" + }, + { + "title": "v0.1.x", + "path": "release-notes/consul-api-gateway/v0_1_x" } ] }, diff --git a/website/public/img/consul-ecs-arch.png b/website/public/img/consul-ecs-arch.png index ae43ab9b4..7f6285894 100644 --- a/website/public/img/consul-ecs-arch.png +++ b/website/public/img/consul-ecs-arch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96298e2c4297f01f39c68e1b6c5f79e136efc4a297265a294a0ee44edd1f210f -size 77203 +oid sha256:2ab7c3840e55e3de875e1822d5f43b4a03882ad404c38bd38b2ab52bd2b365f2 +size 156481 diff --git a/website/redirects.js b/website/redirects.js index 59be38aad..c2f751df7 100644 --- a/website/redirects.js +++ b/website/redirects.js @@ -1109,7 +1109,12 @@ module.exports = [ }, { source: '/docs/k8s/upgrade/compatibility', - destination: '/docs/k8s/installation/compatibility', + destination: '/docs/k8s/compatibility', + permanent: true, + }, + { + source: '/docs/k8s/installation/compatibility', + destination: '/docs/k8s/compatibility', permanent: true, }, {