diff --git a/.changelog/9191.txt b/.changelog/9191.txt new file mode 100644 index 0000000000..25f7da43db --- /dev/null +++ b/.changelog/9191.txt @@ -0,0 +1,18 @@ +```release-note:deprecation +cli: **(Enterprise only)** The `-non-voting-server` flag is deprecated in favor of the new `-read-replica` flag. The `-non-voting-server` flag is still present along side the new flag but it will be removed in a future release. +``` +```release-note:improvement +cli: **(Enterprise only)** A new `-read-replica` flag can now be used to enable running a server as a read only replica. Previously this was enabled with the now deprecated `-non-voting-server` flag. +``` +```release-note:deprecation +config: **(Enterprise only)** The `non_voting_server` configuration setting is deprecated in favor of the new `read_replica` setting. The `non_voting_server` configuration setting is still present but will be removed in a future release. +``` +```release-note:improvement +config: **(Enterprise only)** A new `read_replica` configuration setting can now be used to enable running a server as a read only replica. Previously this was enabled with the now deprecated `non_voting_server` setting. +``` +```release-note:deprecation +server: **(Enterprise only)** Addition of the `nonvoter` tag to the service registration made for read replicas is deprecated in favor of the new tag name of `read_replica`. Both are present in the registration but the `nonvoter` tag will be completely removed in a future release. +``` +```release-note:deprecation +gossip: **(Enterprise only)** Read replicas now advertise themselves by setting the `read_replica` tag. The old `nonvoter` tag is still present but is deprecated and will be removed in a future release. +``` diff --git a/.circleci/config.yml b/.circleci/config.yml index 1b8eb39ec1..af00fc9180 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -236,16 +236,15 @@ jobs: name: go test -race command: | mkdir -p $TEST_RESULTS_DIR /tmp/jsonfile + pkgs="$(go list ./... | \ + grep -E -v '^github.com/hashicorp/consul/agent(/consul|/local|/xds|/routine-leak-checker)?$' | \ + grep -E -v '^github.com/hashicorp/consul/command/')" gotestsum \ - --format=short-verbose \ --jsonfile /tmp/jsonfile/go-test-race.log \ --junitfile $TEST_RESULTS_DIR/gotestsum-report.xml -- \ -tags="$GOTAGS" -p 2 \ -race -gcflags=all=-d=checkptr=0 \ - ./agent/{ae,cache,cache-types,checks,config,pool,proxycfg,router}/... \ - ./agent/consul/{authmethod,fsm,state,stream}/... \ - ./agent/{grpc,rpc,rpcclient,submatview}/... \ - ./snapshot + $pkgs - store_test_results: path: *TEST_RESULTS_DIR diff --git a/acl/authorizer_test.go b/acl/authorizer_test.go index b5534b00cd..b1833efd14 100644 --- a/acl/authorizer_test.go +++ b/acl/authorizer_test.go @@ -195,8 +195,6 @@ func (m *mockAuthorizer) Snapshot(ctx *AuthorizerContext) EnforcementDecision { } func TestACL_Enforce(t *testing.T) { - t.Parallel() - type testCase struct { method string resource Resource diff --git a/acl/chained_authorizer_test.go b/acl/chained_authorizer_test.go index 4ff7b2e4f1..870a00f9c3 100644 --- a/acl/chained_authorizer_test.go +++ b/acl/chained_authorizer_test.go @@ -94,11 +94,7 @@ func (authz testAuthorizer) Snapshot(*AuthorizerContext) EnforcementDecision { } func TestChainedAuthorizer(t *testing.T) { - t.Parallel() - t.Run("No Authorizers", func(t *testing.T) { - t.Parallel() - authz := NewChainedAuthorizer([]Authorizer{}) checkDenyACLRead(t, authz, "foo", nil) checkDenyACLWrite(t, authz, "foo", nil) @@ -129,8 +125,6 @@ func TestChainedAuthorizer(t *testing.T) { }) t.Run("Authorizer Defaults", func(t *testing.T) { - t.Parallel() - authz := NewChainedAuthorizer([]Authorizer{testAuthorizer(Default)}) checkDenyACLRead(t, authz, "foo", nil) checkDenyACLWrite(t, authz, "foo", nil) @@ -161,8 +155,6 @@ func TestChainedAuthorizer(t *testing.T) { }) t.Run("Authorizer No Defaults", func(t *testing.T) { - t.Parallel() - authz := NewChainedAuthorizer([]Authorizer{testAuthorizer(Allow)}) checkAllowACLRead(t, authz, "foo", nil) checkAllowACLWrite(t, authz, "foo", nil) @@ -193,8 +185,6 @@ func TestChainedAuthorizer(t *testing.T) { }) t.Run("First Found", func(t *testing.T) { - t.Parallel() - authz := NewChainedAuthorizer([]Authorizer{testAuthorizer(Deny), testAuthorizer(Allow)}) checkDenyACLRead(t, authz, "foo", nil) checkDenyACLWrite(t, authz, "foo", nil) diff --git a/acl/policy_authorizer_test.go b/acl/policy_authorizer_test.go index 0f074e49aa..d303eea924 100644 --- a/acl/policy_authorizer_test.go +++ b/acl/policy_authorizer_test.go @@ -13,8 +13,6 @@ import ( // ensure compatibility from version to version those tests have been only minimally altered. The tests in this // file are specific to the newer functionality. func TestPolicyAuthorizer(t *testing.T) { - t.Parallel() - type aclCheck struct { name string prefix string @@ -446,8 +444,6 @@ func TestPolicyAuthorizer(t *testing.T) { name := name tcase := tcase t.Run(name, func(t *testing.T) { - t.Parallel() - authz, err := NewPolicyAuthorizer([]*Policy{tcase.policy}, nil) require.NoError(t, err) @@ -458,7 +454,6 @@ func TestPolicyAuthorizer(t *testing.T) { } t.Run(checkName, func(t *testing.T) { check := check - t.Parallel() check.check(t, authz, check.prefix, nil) }) @@ -468,8 +463,6 @@ func TestPolicyAuthorizer(t *testing.T) { } func TestAnyAllowed(t *testing.T) { - t.Parallel() - type radixInsertion struct { segment string value *policyAuthorizerRadixLeaf @@ -719,8 +712,6 @@ func TestAnyAllowed(t *testing.T) { } func TestAllAllowed(t *testing.T) { - t.Parallel() - type radixInsertion struct { segment string value *policyAuthorizerRadixLeaf diff --git a/acl/static_authorizer_test.go b/acl/static_authorizer_test.go index a2865754ee..b9ed590933 100644 --- a/acl/static_authorizer_test.go +++ b/acl/static_authorizer_test.go @@ -5,11 +5,7 @@ import ( ) func TestStaticAuthorizer(t *testing.T) { - t.Parallel() - t.Run("AllowAll", func(t *testing.T) { - t.Parallel() - authz := AllowAll() checkDenyACLRead(t, authz, "foo", nil) checkDenyACLWrite(t, authz, "foo", nil) @@ -40,7 +36,6 @@ func TestStaticAuthorizer(t *testing.T) { }) t.Run("DenyAll", func(t *testing.T) { - t.Parallel() authz := DenyAll() checkDenyACLRead(t, authz, "foo", nil) checkDenyACLWrite(t, authz, "foo", nil) @@ -71,7 +66,6 @@ func TestStaticAuthorizer(t *testing.T) { }) t.Run("ManageAll", func(t *testing.T) { - t.Parallel() authz := ManageAll() checkAllowACLRead(t, authz, "foo", nil) checkAllowACLWrite(t, authz, "foo", nil) diff --git a/agent/agent.go b/agent/agent.go index 6a2a6fea04..ff3b800d1f 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1099,8 +1099,8 @@ func newConsulConfig(runtimeCfg *config.RuntimeConfig, logger hclog.Logger) (*co if runtimeCfg.SessionTTLMin != 0 { cfg.SessionTTLMin = runtimeCfg.SessionTTLMin } - if runtimeCfg.NonVotingServer { - cfg.NonVoter = runtimeCfg.NonVotingServer + if runtimeCfg.ReadReplica { + cfg.ReadReplica = runtimeCfg.ReadReplica } // These are fully specified in the agent defaults, so we can simply diff --git a/agent/auto-config/auto_config_test.go b/agent/auto-config/auto_config_test.go index b8ab0caf45..dc413ed4d0 100644 --- a/agent/auto-config/auto_config_test.go +++ b/agent/auto-config/auto_config_test.go @@ -634,6 +634,7 @@ type testAutoConfig struct { ac *AutoConfig tokenUpdates chan struct{} originalToken string + stop func() initialRoots *structs.IndexedCARoots initialCert *structs.IssuedCert @@ -835,6 +836,7 @@ func startedAutoConfig(t *testing.T, autoEncrypt bool) testAutoConfig { initialRoots: indexedRoots, initialCert: cert, extraCerts: extraCerts, + stop: cancel, } } @@ -1098,16 +1100,15 @@ func TestFallback(t *testing.T) { // now wait for the fallback routine to be invoked require.True(t, waitForChans(100*time.Millisecond, fallbackCtx.Done()), "fallback routines did not get invoked within the alotted time") - // persisting these to disk happens after the RPC we waited on above will have fired - // There is no deterministic way to know once its been written so we wrap this in a retry. - testretry.Run(t, func(r *testretry.R) { - resp, err := testAC.ac.readPersistedAutoConfig() - require.NoError(r, err) + testAC.stop() + <-testAC.ac.done - // ensure the roots got persisted to disk - require.Equal(r, thirdCert.CertPEM, resp.Certificate.GetCertPEM()) - require.Equal(r, secondRoots.ActiveRootID, resp.CARoots.GetActiveRootID()) - }) + resp, err := testAC.ac.readPersistedAutoConfig() + require.NoError(t, err) + + // ensure the roots got persisted to disk + require.Equal(t, thirdCert.CertPEM, resp.Certificate.GetCertPEM()) + require.Equal(t, secondRoots.ActiveRootID, resp.CARoots.GetActiveRootID()) } func TestIntroToken(t *testing.T) { diff --git a/agent/config/builder.go b/agent/config/builder.go index 062fb440f7..48a2c55bec 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -1034,7 +1034,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { NodeID: types.NodeID(b.stringVal(c.NodeID)), NodeMeta: c.NodeMeta, NodeName: b.nodeName(c.NodeName), - NonVotingServer: b.boolVal(c.NonVotingServer), + ReadReplica: b.boolVal(c.ReadReplica), PidFile: b.stringVal(c.PidFile), PrimaryDatacenter: primaryDatacenter, PrimaryGateways: b.expandAllOptionalAddrs("primary_gateways", c.PrimaryGateways), diff --git a/agent/config/builder_oss.go b/agent/config/builder_oss.go index 85cf081375..82a037994b 100644 --- a/agent/config/builder_oss.go +++ b/agent/config/builder_oss.go @@ -13,6 +13,9 @@ var ( "non_voting_server": func(c *Config) { // to maintain existing compatibility we don't nullify the value }, + "read_replica": func(c *Config) { + // to maintain existing compatibility we don't nullify the value + }, "segment": func(c *Config) { // to maintain existing compatibility we don't nullify the value }, diff --git a/agent/config/builder_oss_test.go b/agent/config/builder_oss_test.go index d7a94a9821..e94fec68a2 100644 --- a/agent/config/builder_oss_test.go +++ b/agent/config/builder_oss_test.go @@ -24,11 +24,18 @@ func TestBuilder_validateEnterpriseConfigKeys(t *testing.T) { cases := map[string]testCase{ "non_voting_server": { config: Config{ - NonVotingServer: &boolVal, + ReadReplica: &boolVal, }, keys: []string{"non_voting_server"}, badKeys: []string{"non_voting_server"}, }, + "read_replica": { + config: Config{ + ReadReplica: &boolVal, + }, + keys: []string{"read_replica"}, + badKeys: []string{"read_replica"}, + }, "segment": { config: Config{ SegmentName: &stringVal, @@ -118,11 +125,11 @@ func TestBuilder_validateEnterpriseConfigKeys(t *testing.T) { }, "multi": { config: Config{ - NonVotingServer: &boolVal, - SegmentName: &stringVal, + ReadReplica: &boolVal, + SegmentName: &stringVal, }, - keys: []string{"non_voting_server", "segment", "acl.tokens.agent_master"}, - badKeys: []string{"non_voting_server", "segment"}, + keys: []string{"non_voting_server", "read_replica", "segment", "acl.tokens.agent_master"}, + badKeys: []string{"non_voting_server", "read_replica", "segment"}, }, } diff --git a/agent/config/config.go b/agent/config/config.go index f966297baf..a6d4df8434 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -289,7 +289,7 @@ type Config struct { // Enterprise Only Audit *Audit `json:"audit,omitempty" hcl:"audit" mapstructure:"audit"` // Enterprise Only - NonVotingServer *bool `json:"non_voting_server,omitempty" hcl:"non_voting_server" mapstructure:"non_voting_server"` + ReadReplica *bool `json:"read_replica,omitempty" hcl:"read_replica" mapstructure:"read_replica" alias:"non_voting_server"` // Enterprise Only SegmentName *string `json:"segment,omitempty" hcl:"segment" mapstructure:"segment"` // Enterprise Only diff --git a/agent/config/flags.go b/agent/config/flags.go index a032944d3e..77dc2abb38 100644 --- a/agent/config/flags.go +++ b/agent/config/flags.go @@ -89,7 +89,8 @@ func AddFlags(fs *flag.FlagSet, f *BuilderOpts) { add(&f.Config.NodeName, "node", "Name of this node. Must be unique in the cluster.") add(&f.Config.NodeID, "node-id", "A unique ID for this node across space and time. Defaults to a randomly-generated ID that persists in the data-dir.") add(&f.Config.NodeMeta, "node-meta", "An arbitrary metadata key/value pair for this node, of the format `key:value`. Can be specified multiple times.") - add(&f.Config.NonVotingServer, "non-voting-server", "(Enterprise-only) This flag is used to make the server not participate in the Raft quorum, and have it only receive the data replication stream. This can be used to add read scalability to a cluster in cases where a high volume of reads to servers are needed.") + add(&f.Config.ReadReplica, "non-voting-server", "(Enterprise-only) DEPRECATED: -read-replica should be used instead") + add(&f.Config.ReadReplica, "read-replica", "(Enterprise-only) This flag is used to make the server not participate in the Raft quorum, and have it only receive the data replication stream. This can be used to add read scalability to a cluster in cases where a high volume of reads to servers are needed.") add(&f.Config.PidFile, "pid-file", "Path to file to store agent PID.") add(&f.Config.RPCProtocol, "protocol", "Sets the protocol version. Defaults to latest.") add(&f.Config.RaftProtocol, "raft-protocol", "Sets the Raft protocol version. Defaults to latest.") diff --git a/agent/config/runtime.go b/agent/config/runtime.go index 4acc823faa..ffaf1f7f09 100644 --- a/agent/config/runtime.go +++ b/agent/config/runtime.go @@ -845,12 +845,12 @@ type RuntimeConfig struct { // flag: -node-meta "key:value" -node-meta "key:value" ... NodeMeta map[string]string - // NonVotingServer is whether this server will act as a non-voting member + // ReadReplica is whether this server will act as a non-voting member // of the cluster to help provide read scalability. (Enterprise-only) // // hcl: non_voting_server = (true|false) // flag: -non-voting-server - NonVotingServer bool + ReadReplica bool // PidFile is the file to store our PID in. // diff --git a/agent/config/runtime_oss_test.go b/agent/config/runtime_oss_test.go index c7c21eb0a0..670008ce93 100644 --- a/agent/config/runtime_oss_test.go +++ b/agent/config/runtime_oss_test.go @@ -12,12 +12,16 @@ var entTokenConfigSanitize = `"EnterpriseConfig": {},` func entFullRuntimeConfig(rt *RuntimeConfig) {} -var enterpriseNonVotingServerWarnings []string = []string{enterpriseConfigKeyError{key: "non_voting_server"}.Error()} +var enterpriseReadReplicaWarnings []string = []string{enterpriseConfigKeyError{key: "read_replica"}.Error()} var enterpriseConfigKeyWarnings []string func init() { for k := range enterpriseConfigMap { + if k == "non_voting_server" { + // this is an alias for "read_replica" so we shouldn't see it in warnings + continue + } enterpriseConfigKeyWarnings = append(enterpriseConfigKeyWarnings, enterpriseConfigKeyError{key: k}.Error()) } } diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index 2dff70d1bf..a989add2f7 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -610,10 +610,10 @@ func TestBuilder_BuildAndValidate_ConfigFlagsAndEdgecases(t *testing.T) { `-data-dir=` + dataDir, }, patch: func(rt *RuntimeConfig) { - rt.NonVotingServer = true + rt.ReadReplica = true rt.DataDir = dataDir }, - warns: enterpriseNonVotingServerWarnings, + warns: enterpriseReadReplicaWarnings, }, { desc: "-pid-file", @@ -5315,6 +5315,7 @@ func TestFullConfig(t *testing.T) { "raft_snapshot_threshold": 16384, "raft_snapshot_interval": "30s", "raft_trailing_logs": 83749, + "read_replica": true, "reconnect_timeout": "23739s", "reconnect_timeout_wan": "26694s", "recursors": [ "63.38.39.58", "92.49.18.18" ], @@ -6004,6 +6005,7 @@ func TestFullConfig(t *testing.T) { raft_snapshot_threshold = 16384 raft_snapshot_interval = "30s" raft_trailing_logs = 83749 + read_replica = true reconnect_timeout = "23739s" reconnect_timeout_wan = "26694s" recursors = [ "63.38.39.58", "92.49.18.18" ] @@ -6749,7 +6751,7 @@ func TestFullConfig(t *testing.T) { NodeID: types.NodeID("AsUIlw99"), NodeMeta: map[string]string{"5mgGQMBk": "mJLtVMSG", "A7ynFMJB": "0Nx6RGab"}, NodeName: "otlLxGaI", - NonVotingServer: true, + ReadReplica: true, PidFile: "43xN80Km", PrimaryDatacenter: "ejtmd43d", PrimaryGateways: []string{"aej8eeZo", "roh2KahS"}, @@ -7684,13 +7686,13 @@ func TestSanitize(t *testing.T) { "NodeID": "", "NodeMeta": {}, "NodeName": "", - "NonVotingServer": false, "PidFile": "", "PrimaryDatacenter": "", "PrimaryGateways": [ "pmgw_foo=bar pmgw_key=baz pmgw_secret=boom pmgw_bang=bar" ], "PrimaryGatewaysInterval": "0s", + "ReadReplica": false, "RPCAdvertiseAddr": "", "RPCBindAddr": "", "RPCHandshakeTimeout": "0s", diff --git a/agent/consul/config.go b/agent/consul/config.go index 73db2d2967..7b4cbb507e 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -110,9 +110,9 @@ type Config struct { // RaftConfig is the configuration used for Raft in the local DC RaftConfig *raft.Config - // (Enterprise-only) NonVoter is used to prevent this server from being added + // (Enterprise-only) ReadReplica is used to prevent this server from being added // as a voting member of the Raft cluster. - NonVoter bool + ReadReplica bool // NotifyListen is called after the RPC listener has been configured. // RPCAdvertise will be set to the listener address if it hasn't been diff --git a/agent/consul/leader.go b/agent/consul/leader.go index d050e297b4..1fcef0624c 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -1230,7 +1230,9 @@ func (s *Server) handleAliveMember(member serf.Member) error { Warning: 1, }, Meta: map[string]string{ + // DEPRECATED - remove nonvoter in favor of read_replica in a future version of consul "non_voter": strconv.FormatBool(member.Tags["nonvoter"] == "1"), + "read_replica": strconv.FormatBool(member.Tags["read_replica"] == "1"), "raft_version": strconv.Itoa(parts.RaftVersion), "serf_protocol_current": strconv.FormatUint(uint64(member.ProtocolCur), 10), "serf_protocol_min": strconv.FormatUint(uint64(member.ProtocolMin), 10), diff --git a/agent/consul/leader_test.go b/agent/consul/leader_test.go index 60f817a8d3..0d02e522bd 100644 --- a/agent/consul/leader_test.go +++ b/agent/consul/leader_test.go @@ -334,7 +334,9 @@ func TestLeader_CheckServersMeta(t *testing.T) { versionToExpect := "19.7.9" retry.Run(t, func(r *retry.R) { + // DEPRECATED - remove nonvoter tag in favor of read_replica in a future version of consul member.Tags["nonvoter"] = "1" + member.Tags["read_replica"] = "1" member.Tags["build"] = versionToExpect err := s1.handleAliveMember(member) if err != nil { @@ -347,9 +349,13 @@ func TestLeader_CheckServersMeta(t *testing.T) { if service == nil { r.Fatal("client not registered") } + // DEPRECATED - remove non_voter in favor of read_replica in a future version of consul if service.Meta["non_voter"] != "true" { r.Fatalf("Expected to be non_voter == true, was: %s", service.Meta["non_voter"]) } + if service.Meta["read_replica"] != "true" { + r.Fatalf("Expected to be read_replica == true, was: %s", service.Meta["non_voter"]) + } newVersion := service.Meta["version"] if newVersion != versionToExpect { r.Fatalf("Expected version to be updated to %s, was %s", versionToExpect, newVersion) diff --git a/agent/consul/server_serf.go b/agent/consul/server_serf.go index ce57d9d4b8..d6b79f0a79 100644 --- a/agent/consul/server_serf.go +++ b/agent/consul/server_serf.go @@ -61,8 +61,11 @@ func (s *Server) setupSerf(conf *serf.Config, ch chan serf.Event, path string, w if s.config.BootstrapExpect != 0 { conf.Tags["expect"] = fmt.Sprintf("%d", s.config.BootstrapExpect) } - if s.config.NonVoter { + if s.config.ReadReplica { + // DEPRECATED - This tag should be removed when we no longer want to support + // upgrades from 1.8.x and below conf.Tags["nonvoter"] = "1" + conf.Tags["read_replica"] = "1" } if s.config.UseTLS { conf.Tags["use_tls"] = "1" @@ -351,7 +354,7 @@ func (s *Server) maybeBootstrap() { s.logger.Error("Member has bootstrap mode. Expect disabled.", "member", member) return } - if !p.NonVoter { + if !p.ReadReplica { voters++ } servers = append(servers, *p) @@ -410,7 +413,7 @@ func (s *Server) maybeBootstrap() { id := raft.ServerID(server.ID) suffrage := raft.Voter - if server.NonVoter { + if server.ReadReplica { suffrage = raft.Nonvoter } peer := raft.Server{ diff --git a/agent/consul/server_test.go b/agent/consul/server_test.go index 928e59257b..c645386388 100644 --- a/agent/consul/server_test.go +++ b/agent/consul/server_test.go @@ -240,7 +240,7 @@ func testServerDCExpectNonVoter(t *testing.T, dc string, expect int) (string, *S c.Datacenter = dc c.Bootstrap = false c.BootstrapExpect = expect - c.NonVoter = true + c.ReadReplica = true }) } diff --git a/agent/consul/state/catalog_events.go b/agent/consul/state/catalog_events.go index 0a11a94360..526eca3549 100644 --- a/agent/consul/state/catalog_events.go +++ b/agent/consul/state/catalog_events.go @@ -3,6 +3,7 @@ package state import ( memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/proto/pbsubscribe" @@ -10,6 +11,10 @@ import ( // EventPayloadCheckServiceNode is used as the Payload for a stream.Event to // indicates changes to a CheckServiceNode for service health. +// +// The stream.Payload methods implemented by EventPayloadCheckServiceNode are +// do not mutate the payload, making it safe to use in an Event sent to +// stream.EventPublisher.Publish. type EventPayloadCheckServiceNode struct { Op pbsubscribe.CatalogOp Value *structs.CheckServiceNode @@ -19,7 +24,11 @@ type EventPayloadCheckServiceNode struct { key string } -func (e EventPayloadCheckServiceNode) FilterByKey(key, namespace string) bool { +func (e EventPayloadCheckServiceNode) HasReadPermission(authz acl.Authorizer) bool { + return e.Value.CanRead(authz) == acl.Allow +} + +func (e EventPayloadCheckServiceNode) MatchesKey(key, namespace string) bool { if key == "" && namespace == "" { return true } diff --git a/agent/consul/state/catalog_events_test.go b/agent/consul/state/catalog_events_test.go index 7019c9254f..f9a2e1af21 100644 --- a/agent/consul/state/catalog_events_test.go +++ b/agent/consul/state/catalog_events_test.go @@ -1476,7 +1476,7 @@ func TestEventPayloadCheckServiceNode_FilterByKey(t *testing.T) { t.Skip("cant test namespace matching without namespace support") } - require.Equal(t, tc.expected, tc.payload.FilterByKey(tc.key, tc.namespace)) + require.Equal(t, tc.expected, tc.payload.MatchesKey(tc.key, tc.namespace)) } var testCases = []testCase{ diff --git a/agent/consul/state/store_integration_test.go b/agent/consul/state/store_integration_test.go index d75512195e..fc4d05591e 100644 --- a/agent/consul/state/store_integration_test.go +++ b/agent/consul/state/store_integration_test.go @@ -410,10 +410,14 @@ type nodePayload struct { node *structs.ServiceNode } -func (p nodePayload) FilterByKey(key, _ string) bool { +func (p nodePayload) MatchesKey(key, _ string) bool { return p.key == key } +func (p nodePayload) HasReadPermission(acl.Authorizer) bool { + return true +} + func createTokenAndWaitForACLEventPublish(t *testing.T, s *Store) *structs.ACLToken { token := &structs.ACLToken{ AccessorID: "3af117a9-2233-4cf4-8ff8-3c749c9906b4", diff --git a/agent/consul/stream/event.go b/agent/consul/stream/event.go index 09d96ee6db..74df46b5e1 100644 --- a/agent/consul/stream/event.go +++ b/agent/consul/stream/event.go @@ -4,7 +4,11 @@ to the state store. */ package stream -import "fmt" +import ( + "fmt" + + "github.com/hashicorp/consul/acl" +) // Topic is an identifier that partitions events. A subscription will only receive // events which match the Topic. @@ -18,72 +22,81 @@ type Event struct { Payload Payload } +// A Payload contains the topic-specific data in an event. The payload methods +// should not modify the state of the payload if the Event is being submitted to +// EventPublisher.Publish. type Payload interface { - // FilterByKey must return true if the Payload should be included in a subscription + // MatchesKey must return true if the Payload should be included in a subscription // requested with the key and namespace. // Generally this means that the payload matches the key and namespace or // the payload is a special framing event that should be returned to every // subscription. - FilterByKey(key, namespace string) bool + MatchesKey(key, namespace string) bool + + // HasReadPermission uses the acl.Authorizer to determine if the items in the + // Payload are visible to the request. It returns true if the payload is + // authorized for Read, otherwise returns false. + HasReadPermission(authz acl.Authorizer) bool } -// Len returns the number of events contained within this event. If the Payload -// is a []Event, the length of that slice is returned. Otherwise 1 is returned. -func (e Event) Len() int { - if batch, ok := e.Payload.(PayloadEvents); ok { - return len(batch) - } - return 1 +// PayloadEvents is a Payload that may be returned by Subscription.Next when +// there are multiple events at an index. +// +// Note that unlike most other Payload, PayloadEvents is mutable and it is NOT +// safe to send to EventPublisher.Publish. +type PayloadEvents struct { + Items []Event } -// Filter returns an Event filtered to only those Events where f returns true. -// If the second return value is false, every Event was removed by the filter. -func (e Event) Filter(f func(Event) bool) (Event, bool) { - batch, ok := e.Payload.(PayloadEvents) - if !ok { - return e, f(e) - } +func newPayloadEvents(items ...Event) *PayloadEvents { + return &PayloadEvents{Items: items} +} + +func (p *PayloadEvents) filter(f func(Event) bool) bool { + items := p.Items // To avoid extra allocations, iterate over the list of events first and // get a count of the total desired size. This trades off some extra cpu // time in the worse case (when not all items match the filter), for // fewer memory allocations. var size int - for idx := range batch { - if f(batch[idx]) { + for idx := range items { + if f(items[idx]) { size++ } } - if len(batch) == size || size == 0 { - return e, size != 0 + if len(items) == size || size == 0 { + return size != 0 } - filtered := make(PayloadEvents, 0, size) - for idx := range batch { - event := batch[idx] + filtered := make([]Event, 0, size) + for idx := range items { + event := items[idx] if f(event) { filtered = append(filtered, event) } } - if len(filtered) == 0 { - return e, false - } - e.Payload = filtered - return e, true -} - -// PayloadEvents is an Payload which contains multiple Events. -type PayloadEvents []Event - -// TODO: this method is not called, but needs to exist so that we can store -// a slice of events as a payload. In the future we should be able to refactor -// Event.Filter so that this FilterByKey includes the re-slicing. -func (e PayloadEvents) FilterByKey(_, _ string) bool { + p.Items = filtered return true } -func (e PayloadEvents) Events() []Event { - return e +// MatchesKey filters the PayloadEvents to those which match the key and namespace. +func (p *PayloadEvents) MatchesKey(key, namespace string) bool { + return p.filter(func(event Event) bool { + return event.Payload.MatchesKey(key, namespace) + }) +} + +func (p *PayloadEvents) Len() int { + return len(p.Items) +} + +// HasReadPermission filters the PayloadEvents to those which are authorized +// for reading by authz. +func (p *PayloadEvents) HasReadPermission(authz acl.Authorizer) bool { + return p.filter(func(event Event) bool { + return event.Payload.HasReadPermission(authz) + }) } // IsEndOfSnapshot returns true if this is a framing event that indicates the @@ -100,24 +113,34 @@ func (e Event) IsNewSnapshotToFollow() bool { return e.Payload == newSnapshotToFollow{} } -type endOfSnapshot struct{} +type framingEvent struct{} -func (endOfSnapshot) FilterByKey(string, string) bool { +func (framingEvent) MatchesKey(string, string) bool { return true } -type newSnapshotToFollow struct{} - -func (newSnapshotToFollow) FilterByKey(string, string) bool { +func (framingEvent) HasReadPermission(acl.Authorizer) bool { return true } +type endOfSnapshot struct { + framingEvent +} + +type newSnapshotToFollow struct { + framingEvent +} + type closeSubscriptionPayload struct { tokensSecretIDs []string } -func (closeSubscriptionPayload) FilterByKey(string, string) bool { - return true +func (closeSubscriptionPayload) MatchesKey(string, string) bool { + return false +} + +func (closeSubscriptionPayload) HasReadPermission(acl.Authorizer) bool { + return false } // NewCloseSubscriptionEvent returns a special Event that is handled by the diff --git a/agent/consul/stream/event_publisher.go b/agent/consul/stream/event_publisher.go index 379bfdfa8d..769e875d83 100644 --- a/agent/consul/stream/event_publisher.go +++ b/agent/consul/stream/event_publisher.go @@ -91,7 +91,8 @@ func NewEventPublisher(handlers SnapshotHandlers, snapCacheTTL time.Duration) *E return e } -// Publish events to all subscribers of the event Topic. +// Publish events to all subscribers of the event Topic. The events will be shared +// with all subscriptions, so the Payload used in Event.Payload must be immutable. func (e *EventPublisher) Publish(events []Event) { if len(events) > 0 { e.publishCh <- events diff --git a/agent/consul/stream/event_publisher_test.go b/agent/consul/stream/event_publisher_test.go index f2a9e43a36..576d4ccc35 100644 --- a/agent/consul/stream/event_publisher_test.go +++ b/agent/consul/stream/event_publisher_test.go @@ -7,6 +7,8 @@ import ( "time" "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/acl" ) type intTopic int @@ -63,17 +65,22 @@ var testSnapshotEvent = Event{ } type simplePayload struct { - key string - value string + key string + value string + noReadPerm bool } -func (p simplePayload) FilterByKey(key, _ string) bool { +func (p simplePayload) MatchesKey(key, _ string) bool { if key == "" { return true } return p.key == key } +func (p simplePayload) HasReadPermission(acl.Authorizer) bool { + return !p.noReadPerm +} + func newTestSnapshotHandlers() SnapshotHandlers { return SnapshotHandlers{ testTopic: func(req SubscribeRequest, buf SnapshotAppender) (uint64, error) { diff --git a/agent/consul/stream/event_test.go b/agent/consul/stream/event_test.go index 182f0d5122..8b36ee8d15 100644 --- a/agent/consul/stream/event_test.go +++ b/agent/consul/stream/event_test.go @@ -15,3 +15,163 @@ func TestEvent_IsEndOfSnapshot(t *testing.T) { require.False(t, e.IsEndOfSnapshot()) }) } + +func newSimpleEvent(key string, index uint64) Event { + return Event{Index: index, Payload: simplePayload{key: key}} +} + +func TestPayloadEvents_FilterByKey(t *testing.T) { + type testCase struct { + name string + req SubscribeRequest + events []Event + expectEvent bool + expected *PayloadEvents + expectedCap int + } + + fn := func(t *testing.T, tc testCase) { + events := make([]Event, 0, 5) + events = append(events, tc.events...) + + pe := &PayloadEvents{Items: events} + ok := pe.MatchesKey(tc.req.Key, tc.req.Namespace) + require.Equal(t, tc.expectEvent, ok) + if !tc.expectEvent { + return + } + + require.Equal(t, tc.expected, pe) + // test if there was a new array allocated or not + require.Equal(t, tc.expectedCap, cap(pe.Items)) + } + + var testCases = []testCase{ + { + name: "all events match, no key or namespace", + req: SubscribeRequest{Topic: testTopic}, + events: []Event{ + newSimpleEvent("One", 102), + newSimpleEvent("Two", 102)}, + expectEvent: true, + expected: newPayloadEvents( + newSimpleEvent("One", 102), + newSimpleEvent("Two", 102)), + expectedCap: 5, + }, + { + name: "all events match, no namespace", + req: SubscribeRequest{Topic: testTopic, Key: "Same"}, + events: []Event{ + newSimpleEvent("Same", 103), + newSimpleEvent("Same", 103)}, + expectEvent: true, + expected: newPayloadEvents( + newSimpleEvent("Same", 103), + newSimpleEvent("Same", 103)), + expectedCap: 5, + }, + { + name: "all events match, no key", + req: SubscribeRequest{Topic: testTopic, Namespace: "apps"}, + events: []Event{ + newNSEvent("Something", "apps"), + newNSEvent("Other", "apps")}, + expectEvent: true, + expected: newPayloadEvents( + newNSEvent("Something", "apps"), + newNSEvent("Other", "apps")), + expectedCap: 5, + }, + { + name: "some evens match, no namespace", + req: SubscribeRequest{Topic: testTopic, Key: "Same"}, + events: []Event{ + newSimpleEvent("Same", 104), + newSimpleEvent("Other", 104), + newSimpleEvent("Same", 104)}, + expectEvent: true, + expected: newPayloadEvents( + newSimpleEvent("Same", 104), + newSimpleEvent("Same", 104)), + expectedCap: 2, + }, + { + name: "some events match, no key", + req: SubscribeRequest{Topic: testTopic, Namespace: "apps"}, + events: []Event{ + newNSEvent("app1", "apps"), + newNSEvent("db1", "dbs"), + newNSEvent("app2", "apps")}, + expectEvent: true, + expected: newPayloadEvents( + newNSEvent("app1", "apps"), + newNSEvent("app2", "apps")), + expectedCap: 2, + }, + { + name: "no events match key", + req: SubscribeRequest{Topic: testTopic, Key: "Other"}, + events: []Event{ + newSimpleEvent("Same", 0), + newSimpleEvent("Same", 0)}, + }, + { + name: "no events match namespace", + req: SubscribeRequest{Topic: testTopic, Namespace: "apps"}, + events: []Event{ + newNSEvent("app1", "group1"), + newNSEvent("app2", "group2")}, + expectEvent: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fn(t, tc) + }) + } +} + +func newNSEvent(key, namespace string) Event { + return Event{Index: 22, Payload: nsPayload{key: key, namespace: namespace}} +} + +type nsPayload struct { + framingEvent + key string + namespace string + value string +} + +func (p nsPayload) MatchesKey(key, namespace string) bool { + return (key == "" || key == p.key) && (namespace == "" || namespace == p.namespace) +} + +func TestPayloadEvents_HasReadPermission(t *testing.T) { + t.Run("some events filtered", func(t *testing.T) { + ep := newPayloadEvents( + Event{Payload: simplePayload{key: "one", noReadPerm: true}}, + Event{Payload: simplePayload{key: "two", noReadPerm: false}}, + Event{Payload: simplePayload{key: "three", noReadPerm: true}}, + Event{Payload: simplePayload{key: "four", noReadPerm: false}}) + + require.True(t, ep.HasReadPermission(nil)) + expected := []Event{ + {Payload: simplePayload{key: "two"}}, + {Payload: simplePayload{key: "four"}}, + } + require.Equal(t, expected, ep.Items) + }) + + t.Run("all events filtered", func(t *testing.T) { + ep := newPayloadEvents( + Event{Payload: simplePayload{key: "one", noReadPerm: true}}, + Event{Payload: simplePayload{key: "two", noReadPerm: true}}, + Event{Payload: simplePayload{key: "three", noReadPerm: true}}, + Event{Payload: simplePayload{key: "four", noReadPerm: true}}) + + require.False(t, ep.HasReadPermission(nil)) + }) + +} diff --git a/agent/consul/stream/subscription.go b/agent/consul/stream/subscription.go index 472b0ce90d..03069ea931 100644 --- a/agent/consul/stream/subscription.go +++ b/agent/consul/stream/subscription.go @@ -101,8 +101,8 @@ func (s *Subscription) Next(ctx context.Context) (Event, error) { if len(next.Events) == 0 { continue } - event, ok := filterByKey(s.req, next.Events) - if !ok { + event := newEventFromBatch(s.req, next.Events) + if !event.Payload.MatchesKey(s.req.Key, s.req.Namespace) { continue } return event, nil @@ -128,22 +128,10 @@ func newEventFromBatch(req SubscribeRequest, events []Event) Event { return Event{ Topic: req.Topic, Index: first.Index, - Payload: PayloadEvents(events), + Payload: newPayloadEvents(events...), } } -func filterByKey(req SubscribeRequest, events []Event) (Event, bool) { - event := newEventFromBatch(req, events) - if req.Key == "" && req.Namespace == "" { - return event, true - } - - fn := func(e Event) bool { - return e.Payload.FilterByKey(req.Key, req.Namespace) - } - return event.Filter(fn) -} - // Close the subscription. Subscribers will receive an error when they call Next, // and will need to perform a new Subscribe request. // It is safe to call from any goroutine. diff --git a/agent/consul/stream/subscription_test.go b/agent/consul/stream/subscription_test.go index 2c192b1840..02368f61d7 100644 --- a/agent/consul/stream/subscription_test.go +++ b/agent/consul/stream/subscription_test.go @@ -138,147 +138,29 @@ func publishTestEvent(index uint64, b *eventBuffer, key string) { b.Append([]Event{e}) } -func newSimpleEvent(key string, index uint64) Event { - return Event{Index: index, Payload: simplePayload{key: key}} -} - -func TestFilterByKey(t *testing.T) { - type testCase struct { - name string - req SubscribeRequest - events []Event - expectEvent bool - expected Event - expectedCap int - } - - fn := func(t *testing.T, tc testCase) { - events := make(PayloadEvents, 0, 5) - events = append(events, tc.events...) - - actual, ok := filterByKey(tc.req, events) - require.Equal(t, tc.expectEvent, ok) - if !tc.expectEvent { - return +func TestNewEventsFromBatch(t *testing.T) { + t.Run("single item", func(t *testing.T) { + first := Event{ + Topic: testTopic, + Index: 1234, + Payload: simplePayload{key: "key"}, } - - require.Equal(t, tc.expected, actual) - // test if there was a new array allocated or not - require.Equal(t, tc.expectedCap, cap(actual.Payload.(PayloadEvents))) - } - - var testCases = []testCase{ - { - name: "all events match, no key or namespace", - req: SubscribeRequest{Topic: testTopic}, - events: []Event{ - newSimpleEvent("One", 102), - newSimpleEvent("Two", 102)}, - expectEvent: true, - expected: Event{ - Topic: testTopic, - Index: 102, - Payload: PayloadEvents{ - newSimpleEvent("One", 102), - newSimpleEvent("Two", 102)}}, - expectedCap: 5, - }, - { - name: "all events match, no namespace", - req: SubscribeRequest{Topic: testTopic, Key: "Same"}, - events: []Event{ - newSimpleEvent("Same", 103), - newSimpleEvent("Same", 103)}, - expectEvent: true, - expected: Event{ - Topic: testTopic, - Index: 103, - Payload: PayloadEvents{ - newSimpleEvent("Same", 103), - newSimpleEvent("Same", 103)}}, - expectedCap: 5, - }, - { - name: "all events match, no key", - req: SubscribeRequest{Topic: testTopic, Namespace: "apps"}, - events: []Event{ - newNSEvent("Something", "apps"), - newNSEvent("Other", "apps")}, - expectEvent: true, - expected: Event{ - Topic: testTopic, - Index: 22, - Payload: PayloadEvents{ - newNSEvent("Something", "apps"), - newNSEvent("Other", "apps")}}, - expectedCap: 5, - }, - { - name: "some evens match, no namespace", - req: SubscribeRequest{Topic: testTopic, Key: "Same"}, - events: []Event{ - newSimpleEvent("Same", 104), - newSimpleEvent("Other", 104), - newSimpleEvent("Same", 104)}, - expectEvent: true, - expected: Event{ - Topic: testTopic, - Index: 104, - Payload: PayloadEvents{ - newSimpleEvent("Same", 104), - newSimpleEvent("Same", 104)}}, - expectedCap: 2, - }, - { - name: "some events match, no key", - req: SubscribeRequest{Topic: testTopic, Namespace: "apps"}, - events: []Event{ - newNSEvent("app1", "apps"), - newNSEvent("db1", "dbs"), - newNSEvent("app2", "apps")}, - expectEvent: true, - expected: Event{ - Topic: testTopic, - Index: 22, - Payload: PayloadEvents{ - newNSEvent("app1", "apps"), - newNSEvent("app2", "apps")}}, - expectedCap: 2, - }, - { - name: "no events match key", - req: SubscribeRequest{Topic: testTopic, Key: "Other"}, - events: []Event{ - newSimpleEvent("Same", 0), - newSimpleEvent("Same", 0)}, - }, - { - name: "no events match namespace", - req: SubscribeRequest{Topic: testTopic, Namespace: "apps"}, - events: []Event{ - newNSEvent("app1", "group1"), - newNSEvent("app2", "group2")}, - expectEvent: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - fn(t, tc) - }) - } -} - -func newNSEvent(key, namespace string) Event { - return Event{Index: 22, Payload: nsPayload{key: key, namespace: namespace}} -} - -type nsPayload struct { - key string - namespace string - value string -} - -func (p nsPayload) FilterByKey(key, namespace string) bool { - return (key == "" || key == p.key) && (namespace == "" || namespace == p.namespace) + e := newEventFromBatch(SubscribeRequest{}, []Event{first}) + require.Equal(t, first, e) + }) + t.Run("many items", func(t *testing.T) { + events := []Event{ + newSimpleEvent("foo", 9999), + newSimpleEvent("foo", 9999), + newSimpleEvent("zee", 9999), + } + req := SubscribeRequest{Topic: testTopic} + e := newEventFromBatch(req, events) + expected := Event{ + Topic: testTopic, + Index: 9999, + Payload: newPayloadEvents(events...), + } + require.Equal(t, expected, e) + }) } diff --git a/agent/metadata/server.go b/agent/metadata/server.go index d145a9d266..5b35ec78ec 100644 --- a/agent/metadata/server.go +++ b/agent/metadata/server.go @@ -40,7 +40,7 @@ type Server struct { RaftVersion int Addr net.Addr Status serf.MemberStatus - NonVoter bool + ReadReplica bool ACLs structs.ACLMode FeatureFlags map[string]int @@ -160,7 +160,10 @@ func IsConsulServer(m serf.Member) (bool, *Server) { } // Check if the server is a non voter + // DEPRECATED - remove looking for the nonvoter tag eventually once we don't have to support + // read replicas running v1.8.x and below. _, nonVoter := m.Tags["nonvoter"] + _, readReplica := m.Tags["read_replica"] addr := &net.TCPAddr{IP: m.Addr, Port: port} @@ -182,7 +185,8 @@ func IsConsulServer(m serf.Member) (bool, *Server) { RaftVersion: raftVsn, Status: m.Status, UseTLS: useTLS, - NonVoter: nonVoter, + // DEPRECATED - remove nonVoter check once support for that tag is removed + ReadReplica: nonVoter || readReplica, ACLs: acls, FeatureFlags: featureFlags, } diff --git a/agent/metadata/server_test.go b/agent/metadata/server_test.go index 6b96785ccf..4f479472c0 100644 --- a/agent/metadata/server_test.go +++ b/agent/metadata/server_test.go @@ -66,7 +66,7 @@ func TestIsConsulServer(t *testing.T) { "expect": "3", "raft_vsn": "3", "use_tls": "1", - "nonvoter": "1", + "read_replica": "1", }, Status: serf.StatusLeft, } @@ -101,7 +101,7 @@ func TestIsConsulServer(t *testing.T) { if !parts.UseTLS { t.Fatalf("bad: %v", parts.UseTLS) } - if !parts.NonVoter { + if !parts.ReadReplica { t.Fatalf("unexpected voter") } m.Tags["bootstrap"] = "1" @@ -130,10 +130,16 @@ func TestIsConsulServer(t *testing.T) { t.Fatalf("unexpected bootstrap") } - delete(m.Tags, "nonvoter") + delete(m.Tags, "read_replica") ok, parts = metadata.IsConsulServer(m) - if !ok || parts.NonVoter { - t.Fatalf("unexpected nonvoter") + if !ok || parts.ReadReplica { + t.Fatalf("unexpected read replica") + } + + m.Tags["nonvoter"] = "1" + ok, parts = metadata.IsConsulServer(m) + if !ok || !parts.ReadReplica { + t.Fatalf("expected read replica") } delete(m.Tags, "role") diff --git a/agent/rpc/subscribe/auth.go b/agent/rpc/subscribe/auth.go deleted file mode 100644 index b41b1fdc40..0000000000 --- a/agent/rpc/subscribe/auth.go +++ /dev/null @@ -1,22 +0,0 @@ -package subscribe - -import ( - "github.com/hashicorp/consul/acl" - "github.com/hashicorp/consul/agent/consul/state" - "github.com/hashicorp/consul/agent/consul/stream" -) - -// EnforceACL takes an acl.Authorizer and returns the decision for whether the -// event is allowed to be sent to this client or not. -func enforceACL(authz acl.Authorizer, e stream.Event) acl.EnforcementDecision { - switch { - case e.IsEndOfSnapshot(), e.IsNewSnapshotToFollow(): - return acl.Allow - } - - switch p := e.Payload.(type) { - case state.EventPayloadCheckServiceNode: - return p.Value.CanRead(authz) - } - return acl.Deny -} diff --git a/agent/rpc/subscribe/logger.go b/agent/rpc/subscribe/logger.go index ddddb20ca5..99394f5465 100644 --- a/agent/rpc/subscribe/logger.go +++ b/agent/rpc/subscribe/logger.go @@ -58,12 +58,20 @@ func (l *eventLogger) Trace(e stream.Event) { case e.IsEndOfSnapshot(): l.snapshotDone = true l.logger.Trace("snapshot complete", "index", e.Index, "sent", l.count) + return case e.IsNewSnapshotToFollow(): l.logger.Trace("starting new snapshot", "sent", l.count) return - case l.snapshotDone: - l.logger.Trace("sending events", "index", e.Index, "sent", l.count, "batch_size", e.Len()) } - l.count += uint64(e.Len()) + size := 1 + if l, ok := e.Payload.(length); ok { + size = l.Len() + } + l.logger.Trace("sending events", "index", e.Index, "sent", l.count, "batch_size", size) + l.count += uint64(size) +} + +type length interface { + Len() int } diff --git a/agent/rpc/subscribe/subscribe.go b/agent/rpc/subscribe/subscribe.go index 71919babab..0e98893f92 100644 --- a/agent/rpc/subscribe/subscribe.go +++ b/agent/rpc/subscribe/subscribe.go @@ -132,10 +132,8 @@ func filterByAuth(authz acl.Authorizer, event stream.Event) (stream.Event, bool) if authz == nil { return event, true } - fn := func(e stream.Event) bool { - return enforceACL(authz, e) == acl.Allow - } - return event.Filter(fn) + + return event, event.Payload.HasReadPermission(authz) } func newEventFromStreamEvent(event stream.Event) *pbsubscribe.Event { @@ -154,10 +152,10 @@ func newEventFromStreamEvent(event stream.Event) *pbsubscribe.Event { func setPayload(e *pbsubscribe.Event, payload stream.Payload) { switch p := payload.(type) { - case stream.PayloadEvents: + case *stream.PayloadEvents: e.Payload = &pbsubscribe.Event_EventBatch{ EventBatch: &pbsubscribe.EventBatch{ - Events: batchEventsFromEventSlice(p), + Events: batchEventsFromEventSlice(p.Items), }, } case state.EventPayloadCheckServiceNode: diff --git a/agent/rpc/subscribe/subscribe_test.go b/agent/rpc/subscribe/subscribe_test.go index bc41ed1e88..cf37e75bbc 100644 --- a/agent/rpc/subscribe/subscribe_test.go +++ b/agent/rpc/subscribe/subscribe_test.go @@ -917,8 +917,8 @@ func TestNewEventFromSteamEvent(t *testing.T) { name: "event batch", event: stream.Event{ Index: 2002, - Payload: stream.PayloadEvents{ - { + Payload: newPayloadEvents( + stream.Event{ Index: 2002, Payload: state.EventPayloadCheckServiceNode{ Op: pbsubscribe.CatalogOp_Register, @@ -928,7 +928,7 @@ func TestNewEventFromSteamEvent(t *testing.T) { }, }, }, - { + stream.Event{ Index: 2002, Payload: state.EventPayloadCheckServiceNode{ Op: pbsubscribe.CatalogOp_Deregister, @@ -937,8 +937,7 @@ func TestNewEventFromSteamEvent(t *testing.T) { Service: &structs.NodeService{Service: "web1"}, }, }, - }, - }, + }), }, expected: pbsubscribe.Event{ Index: 2002, @@ -1008,6 +1007,10 @@ func TestNewEventFromSteamEvent(t *testing.T) { } } +func newPayloadEvents(items ...stream.Event) *stream.PayloadEvents { + return &stream.PayloadEvents{Items: items} +} + // newEventFromSubscription is used to return framing events. EndOfSnapshot and // NewSnapshotToFollow are not exported, but we can get them from a subscription. func newEventFromSubscription(t *testing.T, index uint64) stream.Event { diff --git a/agent/structs/structs_filtering_test.go b/agent/structs/structs_filtering_test.go index e13c98043c..319f47e9e2 100644 --- a/agent/structs/structs_filtering_test.go +++ b/agent/structs/structs_filtering_test.go @@ -11,12 +11,47 @@ import ( "github.com/hashicorp/consul/api" bexpr "github.com/hashicorp/go-bexpr" + "github.com/mitchellh/pointerstructure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var dumpFieldConfig = flag.Bool("dump-field-config", false, "generate field config dump file") +func TestPointerStructure(t *testing.T) { + csn := CheckServiceNode{ + Node: &Node{ + ID: "f18f3a10-2153-40ae-af7d-68db0e856498", + Node: "node1", + Address: "198.18.0.1", + }, + Service: &NodeService{ + ID: "test", + Service: "test", + Port: 1234, + TaggedAddresses: map[string]ServiceAddress{ + "wan": { + Address: "1.1.1.1", + Port: 443, + }, + }, + }, + } + + ptr := pointerstructure.Pointer{ + Parts: []string{ + "Service", + "TaggedAddresses", + "wan", + "Address", + }, + } + + val, err := ptr.Get(csn) + require.NoError(t, err) + require.Equal(t, "1.1.1.1", val) +} + /////////////////////////////////////////////////////////////////////////////// // // NOTE: The tests within this file are designed to validate that the fields diff --git a/api/agent.go b/api/agent.go index b15c2dee16..a4cc143f05 100644 --- a/api/agent.go +++ b/api/agent.go @@ -160,6 +160,15 @@ const ( // configured to use TLS. Any other value indicates that it was not setup in // that manner. MemberTagValueUseTLS = "1" + + // MemberTagKeyReadReplica is the key used to indicate that the member is a read + // replica server (will remain a Raft non-voter). + // Read Replicas are a Consul Enterprise feature. + MemberTagKeyReadReplica = "read_replica" + // MemberTagValueReadReplica is the value of the MemberTagKeyReadReplica key when + // the member is in fact a read-replica. Any other value indicates that it is not. + // Read Replicas are a Consul Enterprise feature. + MemberTagValueReadReplica = "1" ) type MemberACLMode string diff --git a/connect/proxy/config_test.go b/connect/proxy/config_test.go index 6ba5603089..f076d626cc 100644 --- a/connect/proxy/config_test.go +++ b/connect/proxy/config_test.go @@ -5,12 +5,12 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/connect" "github.com/hashicorp/consul/sdk/testutil" - "github.com/stretchr/testify/require" ) func TestUpstreamResolverFuncFromClient(t *testing.T) { @@ -79,8 +79,6 @@ func TestUpstreamResolverFuncFromClient(t *testing.T) { } func TestAgentConfigWatcherSidecarProxy(t *testing.T) { - t.Parallel() - a := agent.StartTestAgent(t, agent.TestAgent{Name: "agent_smith"}) defer a.Shutdown() diff --git a/connect/proxy/proxy_test.go b/connect/proxy/proxy_test.go index 64765ca8a2..f280387a1d 100644 --- a/connect/proxy/proxy_test.go +++ b/connect/proxy/proxy_test.go @@ -5,7 +5,7 @@ import ( "net" "testing" - "github.com/hashicorp/consul/testrpc" + "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent" agConnect "github.com/hashicorp/consul/agent/connect" @@ -14,12 +14,10 @@ import ( "github.com/hashicorp/consul/sdk/freeport" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" - "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/testrpc" ) func TestProxy_public(t *testing.T) { - t.Parallel() - require := require.New(t) ports := freeport.MustTake(1) diff --git a/connect/service_test.go b/connect/service_test.go index 49c4877007..1ee66ce83d 100644 --- a/connect/service_test.go +++ b/connect/service_test.go @@ -14,13 +14,13 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" - "github.com/stretchr/testify/require" ) // Assert io.Closer implementation @@ -89,8 +89,8 @@ func TestService_Dial(t *testing.T) { err := testSvr.Serve() require.NoError(err) }() - defer testSvr.Close() <-testSvr.Listening + defer testSvr.Close() } // Always expect to be connecting to a "DB" diff --git a/connect/testing.go b/connect/testing.go index 4554add0e8..30a517b61f 100644 --- a/connect/testing.go +++ b/connect/testing.go @@ -10,11 +10,12 @@ import ( "net/http" "sync/atomic" + "github.com/hashicorp/go-hclog" + testing "github.com/mitchellh/go-testing-interface" + "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/sdk/freeport" - "github.com/hashicorp/go-hclog" - testing "github.com/mitchellh/go-testing-interface" ) // TestService returns a Service instance based on a static TLS Config. @@ -124,8 +125,8 @@ func (s *TestServer) Serve() error { if err != nil { return err } - close(s.Listening) s.l = l + close(s.Listening) log.Printf("test connect service listening on %s", s.Addr) for { diff --git a/connect/tls.go b/connect/tls.go index e465d3a10a..a79fe7c8a3 100644 --- a/connect/tls.go +++ b/connect/tls.go @@ -13,9 +13,10 @@ import ( "strings" "sync" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/api" - "github.com/hashicorp/go-hclog" ) // parseLeafX509Cert will parse an X509 certificate @@ -460,5 +461,7 @@ func (cfg *dynamicTLSConfig) Ready() bool { // method will not stop returning a nil chan in that case. It is only useful // for initial startup. For ongoing health Ready() should be used. func (cfg *dynamicTLSConfig) ReadyWait() <-chan struct{} { + cfg.RLock() + defer cfg.RUnlock() return cfg.readyCh } diff --git a/logging/logfile.go b/logging/logfile.go index ff0f324306..153473482f 100644 --- a/logging/logfile.go +++ b/logging/logfile.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strconv" "strings" "sync" @@ -93,23 +94,28 @@ func (l *LogFile) pruneFiles() error { if l.MaxFiles == 0 { return nil } - pattern := l.fileNamePattern() - //get all the files that match the log file pattern - globExpression := filepath.Join(l.logPath, fmt.Sprintf(pattern, "*")) - matches, err := filepath.Glob(globExpression) + + pattern := filepath.Join(l.logPath, fmt.Sprintf(l.fileNamePattern(), "*")) + matches, err := filepath.Glob(pattern) if err != nil { return err } - var stale int - if l.MaxFiles <= -1 { - // Prune everything - stale = len(matches) - } else { - // Prune if there are more files stored than the configured max - stale = len(matches) - l.MaxFiles + + switch { + case l.MaxFiles < 0: + return removeFiles(matches) + case len(matches) < l.MaxFiles: + return nil } - for i := 0; i < stale; i++ { - if err := os.Remove(matches[i]); err != nil { + + sort.Strings(matches) + last := len(matches) - l.MaxFiles + return removeFiles(matches[:last]) +} + +func removeFiles(files []string) error { + for _, file := range files { + if err := os.Remove(file); err != nil { return err } } diff --git a/logging/logfile_test.go b/logging/logfile_test.go index 115f1bcff4..d56e4d42c0 100644 --- a/logging/logfile_test.go +++ b/logging/logfile_test.go @@ -2,135 +2,122 @@ package logging import ( "io/ioutil" + "os" "path/filepath" + "sort" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/sdk/testutil" ) -const ( - testFileName = "Consul.log" - testDuration = 50 * time.Millisecond - testBytes = 10 -) - -func TestLogFile_timeRotation(t *testing.T) { - tempDir := testutil.TempDir(t, "LogWriterTime") +func TestLogFile_Rotation_MaxDuration(t *testing.T) { + tempDir := testutil.TempDir(t, "") logFile := LogFile{ - fileName: testFileName, + fileName: "consul.log", logPath: tempDir, - duration: testDuration, + duration: 50 * time.Millisecond, } + logFile.Write([]byte("Hello World")) - time.Sleep(3 * testDuration) + time.Sleep(3 * logFile.duration) logFile.Write([]byte("Second File")) - want := 2 - if got, _ := ioutil.ReadDir(tempDir); len(got) != want { - t.Errorf("Expected %d files, got %v file(s)", want, len(got)) - } + require.Len(t, listDir(t, tempDir), 2) } func TestLogFile_openNew(t *testing.T) { - tempDir := testutil.TempDir(t, "LogWriterOpen") - logFile := LogFile{fileName: testFileName, logPath: tempDir, duration: testDuration} - if err := logFile.openNew(); err != nil { - t.Errorf("Expected open file %s, got an error (%s)", testFileName, err) + logFile := LogFile{ + fileName: "consul.log", + logPath: testutil.TempDir(t, ""), + duration: defaultRotateDuration, } + err := logFile.openNew() + require.NoError(t, err) - if _, err := ioutil.ReadFile(logFile.FileInfo.Name()); err != nil { - t.Errorf("Expected readable file %s, got an error (%s)", logFile.FileInfo.Name(), err) - } + msg := "[INFO] Something" + _, err = logFile.Write([]byte(msg)) + require.NoError(t, err) + + content, err := ioutil.ReadFile(logFile.FileInfo.Name()) + require.NoError(t, err) + require.Contains(t, string(content), msg) } -func TestLogFile_byteRotation(t *testing.T) { +func TestLogFile_Rotation_MaxBytes(t *testing.T) { tempDir := testutil.TempDir(t, "LogWriterBytes") logFile := LogFile{ - fileName: testFileName, + fileName: "somefile.log", logPath: tempDir, - MaxBytes: testBytes, - duration: 24 * time.Hour, + MaxBytes: 10, + duration: defaultRotateDuration, } logFile.Write([]byte("Hello World")) logFile.Write([]byte("Second File")) - want := 2 - tempFiles, _ := ioutil.ReadDir(tempDir) - if got := tempFiles; len(got) != want { - t.Errorf("Expected %d files, got %v file(s)", want, len(got)) - } + require.Len(t, listDir(t, tempDir), 2) } -func TestLogFile_deleteArchives(t *testing.T) { - tempDir := testutil.TempDir(t, "LogWriteDeleteArchives") +func TestLogFile_PruneFiles(t *testing.T) { + tempDir := testutil.TempDir(t, t.Name()) logFile := LogFile{ - fileName: testFileName, + fileName: "consul.log", logPath: tempDir, - MaxBytes: testBytes, - duration: 24 * time.Hour, + MaxBytes: 10, + duration: defaultRotateDuration, MaxFiles: 1, } logFile.Write([]byte("[INFO] Hello World")) logFile.Write([]byte("[INFO] Second File")) logFile.Write([]byte("[INFO] Third File")) - want := 2 - tempFiles, _ := ioutil.ReadDir(tempDir) - if got := tempFiles; len(got) != want { - t.Errorf("Expected %d files, got %v file(s)", want, len(got)) - return - } - for _, tempFile := range tempFiles { - var bytes []byte - var err error - path := filepath.Join(tempDir, tempFile.Name()) - if bytes, err = ioutil.ReadFile(path); err != nil { - t.Errorf(err.Error()) - return - } - contents := string(bytes) - if contents == "[INFO] Hello World" { - t.Errorf("Should have deleted the eldest log file") - return - } - } + logFiles := listDir(t, tempDir) + sort.Strings(logFiles) + require.Len(t, logFiles, 2) + + content, err := ioutil.ReadFile(filepath.Join(tempDir, logFiles[0])) + require.NoError(t, err) + require.Contains(t, string(content), "Second File") + + content, err = ioutil.ReadFile(filepath.Join(tempDir, logFiles[1])) + require.NoError(t, err) + require.Contains(t, string(content), "Third File") } -func TestLogFile_deleteArchivesDisabled(t *testing.T) { +func TestLogFile_PruneFiles_Disabled(t *testing.T) { tempDir := testutil.TempDir(t, t.Name()) logFile := LogFile{ - fileName: testFileName, + fileName: "somename.log", logPath: tempDir, - MaxBytes: testBytes, - duration: 24 * time.Hour, + MaxBytes: 10, + duration: defaultRotateDuration, MaxFiles: 0, } logFile.Write([]byte("[INFO] Hello World")) logFile.Write([]byte("[INFO] Second File")) logFile.Write([]byte("[INFO] Third File")) - want := 3 - tempFiles, _ := ioutil.ReadDir(tempDir) - if got := tempFiles; len(got) != want { - t.Errorf("Expected %d files, got %v file(s)", want, len(got)) - return - } + require.Len(t, listDir(t, tempDir), 3) } -func TestLogFile_rotationDisabled(t *testing.T) { +func TestLogFile_FileRotation_Disabled(t *testing.T) { tempDir := testutil.TempDir(t, t.Name()) logFile := LogFile{ - fileName: testFileName, + fileName: "consul.log", logPath: tempDir, - MaxBytes: testBytes, - duration: 24 * time.Hour, + MaxBytes: 10, MaxFiles: -1, } logFile.Write([]byte("[INFO] Hello World")) logFile.Write([]byte("[INFO] Second File")) logFile.Write([]byte("[INFO] Third File")) - want := 1 - tempFiles, _ := ioutil.ReadDir(tempDir) - if got := tempFiles; len(got) != want { - t.Errorf("Expected %d files, got %v file(s)", want, len(got)) - return - } + require.Len(t, listDir(t, tempDir), 1) +} + +func listDir(t *testing.T, name string) []string { + t.Helper() + fh, err := os.Open(name) + require.NoError(t, err) + files, err := fh.Readdirnames(100) + require.NoError(t, err) + return files } diff --git a/logging/logger.go b/logging/logger.go index a528c92796..dfc05785cf 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -40,15 +40,8 @@ type Config struct { LogRotateMaxFiles int } -const ( - // defaultRotateDuration is the default time taken by the agent to rotate logs - defaultRotateDuration = 24 * time.Hour -) - -var ( - logRotateDuration time.Duration - logRotateBytes int -) +// defaultRotateDuration is the default time taken by the agent to rotate logs +const defaultRotateDuration = 24 * time.Hour type LogSetupErrorFn func(string) @@ -86,28 +79,22 @@ func Setup(config Config, out io.Writer) (hclog.InterceptLogger, error) { // Create a file logger if the user has specified the path to the log file if config.LogFilePath != "" { dir, fileName := filepath.Split(config.LogFilePath) - // If a path is provided but has no fileName a default is provided. if fileName == "" { fileName = "consul.log" } - // Try to enter the user specified log rotation duration first - if config.LogRotateDuration != 0 { - logRotateDuration = config.LogRotateDuration - } else { - // Default to 24 hrs if no rotation period is specified - logRotateDuration = defaultRotateDuration - } - // User specified byte limit for log rotation if one is provided - if config.LogRotateBytes != 0 { - logRotateBytes = config.LogRotateBytes + if config.LogRotateDuration == 0 { + config.LogRotateDuration = defaultRotateDuration } logFile := &LogFile{ fileName: fileName, logPath: dir, - duration: logRotateDuration, - MaxBytes: logRotateBytes, + duration: config.LogRotateDuration, + MaxBytes: config.LogRotateBytes, MaxFiles: config.LogRotateMaxFiles, } + if err := logFile.pruneFiles(); err != nil { + return nil, fmt.Errorf("Failed to prune log files: %w", err) + } if err := logFile.openNew(); err != nil { return nil, fmt.Errorf("Failed to setup logging: %w", err) } diff --git a/ui/packages/consul-ui/app/components/consul/tomography/graph/index.hbs b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.hbs index f4332892b9..15ceb90173 100644 --- a/ui/packages/consul-ui/app/components/consul/tomography/graph/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.hbs @@ -1,35 +1,46 @@ - - - - - - - - - - - {{#each distances as |item|}} - - {{/each}} - - - - - - {{format-number milliseconds.[0] maximumFractionDigits=2}}ms - - - - {{format-number milliseconds.[1] maximumFractionDigits=2}}ms - - - - {{format-number milliseconds.[2] maximumFractionDigits=2}}ms - - - - {{format-number milliseconds.[3] maximumFractionDigits=2}}ms - - - - +
+ + + + + + + + + + + {{#each this.distances as |item|}} + + {{/each}} + + + + + + {{format-number this.milliseconds.[0] maximumFractionDigits=2}}ms + + + + {{format-number this.milliseconds.[1] maximumFractionDigits=2}}ms + + + + {{format-number this.milliseconds.[2] maximumFractionDigits=2}}ms + + + + {{format-number this.milliseconds.[3] maximumFractionDigits=2}}ms + + + + +
diff --git a/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js index 095a1bc349..26d3764c28 100644 --- a/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js +++ b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js @@ -1,5 +1,5 @@ -import Component from '@ember/component'; -import { computed, set, get } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; const size = 336; const insetSize = size / 2 - 8; @@ -9,52 +9,39 @@ const inset = function(num) { const milliseconds = function(num, max) { return max > 0 ? parseInt(max * num) / 100 : 0; }; -export default Component.extend({ - size: size, - tomography: 0, - max: -999999999, - init: function() { - this._super(...arguments); - this.circle = [inset(1), inset(0.25), inset(0.5), inset(0.75), inset(1)]; - this.labels = [inset(-0.25), inset(-0.5), inset(-0.75), inset(-1)]; - }, - milliseconds: computed('distances', 'max', function() { - const max = get(this, 'max'); - return [ - milliseconds(25, max), - milliseconds(50, max), - milliseconds(75, max), - milliseconds(100, max), - ]; - }), - distances: computed('tomography', function() { - const tomography = get(this, 'tomography'); - let distances = get(tomography, 'distances') || []; - // TODO: This should probably be moved into the milliseconds computedProperty - /*eslint ember/no-side-effects: "warn"*/ - distances.forEach((d, i) => { - if (d.distance > get(this, 'max')) { - set(this, 'max', d.distance); - } - }); - let n = get(distances, 'length'); - if (n > 360) { +export default class TomographyGraph extends Component { + @tracked max = -999999999; + size = size; + + circle = [inset(1), inset(0.25), inset(0.5), inset(0.75), inset(1)]; + labels = [inset(-0.25), inset(-0.5), inset(-0.75), inset(-1)]; + + get milliseconds() { + const distances = this.args.distances || []; + const max = distances.reduce((prev, d) => Math.max(prev, d.distance), this.max); + return [25, 50, 75, 100].map(item => milliseconds(item, max)); + } + + get distances() { + const distances = this.args.distances || []; + const max = distances.reduce((prev, d) => Math.max(prev, d.distance), this.max); + const len = distances.length; + if (len > 360) { // We have more nodes than we want to show, take a random sampling to keep // the number around 360. - const sampling = 360 / n; + const sampling = 360 / len; distances = distances.filter(function(_, i) { - return i == 0 || i == n - 1 || Math.random() < sampling; + return i == 0 || i == len - 1 || Math.random() < sampling; }); - n = get(distances, 'length'); } return distances.map((d, i) => { return { - rotate: (i * 360) / n, - y2: -insetSize * (d.distance / get(this, 'max')), + rotate: (i * 360) / distances.length, + y2: -insetSize * (d.distance / max), node: d.node, distance: d.distance, segment: d.segment, }; }); - }), -}); + } +} diff --git a/ui/packages/consul-ui/app/components/consul/tomography/graph/index.scss b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.scss new file mode 100644 index 0000000000..45f343d816 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.scss @@ -0,0 +1,33 @@ +.tomography-graph { + .background { + fill: $gray-050; + } + .axis { + fill: none; + stroke: $gray-300; + stroke-dasharray: 4 4; + } + .border { + fill: none; + stroke: $gray-300; + } + .point { + stroke: $gray-400; + fill: $magenta-600; + } + .lines line { + stroke: $magenta-600; + } + .lines line:hover { + stroke: $gray-300; + stroke-width: 2px; + } + .tick line { + stroke: $gray-300; + } + .tick text { + font-size: $typo-size-600; + text-anchor: start; + color: $gray-900; + } +} diff --git a/ui/packages/consul-ui/app/components/list-collection/index.hbs b/ui/packages/consul-ui/app/components/list-collection/index.hbs index ce7fbe30cc..a5dc40837d 100644 --- a/ui/packages/consul-ui/app/components/list-collection/index.hbs +++ b/ui/packages/consul-ui/app/components/list-collection/index.hbs @@ -1,6 +1,8 @@
diff --git a/ui/packages/consul-ui/app/components/list-collection/index.js b/ui/packages/consul-ui/app/components/list-collection/index.js index e4089ac8fd..6762e42c65 100644 --- a/ui/packages/consul-ui/app/components/list-collection/index.js +++ b/ui/packages/consul-ui/app/components/list-collection/index.js @@ -2,17 +2,16 @@ import { inject as service } from '@ember/service'; import { computed, get, set } from '@ember/object'; import Component from 'ember-collection/components/ember-collection'; import PercentageColumns from 'ember-collection/layouts/percentage-columns'; -import style from 'ember-computed-style'; import Slotted from 'block-slots'; const formatItemStyle = PercentageColumns.prototype.formatItemStyle; export default Component.extend(Slotted, { + tagName: '', dom: service('dom'), tagName: '', height: 500, cellHeight: 70, - style: style('getStyle'), checked: null, scroll: 'virtual', init: function() { @@ -43,7 +42,7 @@ export default Component.extend(Slotted, { return style; }; }, - getStyle: computed('height', function() { + style: computed('height', function() { if (this.scroll !== 'virtual') { return {}; } diff --git a/ui/packages/consul-ui/app/components/tab-nav/index.hbs b/ui/packages/consul-ui/app/components/tab-nav/index.hbs index 62a978ecb4..1e9d2d7cb3 100644 --- a/ui/packages/consul-ui/app/components/tab-nav/index.hbs +++ b/ui/packages/consul-ui/app/components/tab-nav/index.hbs @@ -1,5 +1,12 @@
- + diff --git a/ui/packages/consul-ui/app/templates/dc/nspaces/edit.hbs b/ui/packages/consul-ui/app/templates/dc/nspaces/edit.hbs index 72bdd234d0..bf2a5eab65 100644 --- a/ui/packages/consul-ui/app/templates/dc/nspaces/edit.hbs +++ b/ui/packages/consul-ui/app/templates/dc/nspaces/edit.hbs @@ -1,7 +1,7 @@ {{#if create }} - {{title 'New Namespace'}} + {{page-title 'New Namespace'}} {{else}} - {{title 'Edit Namespace'}} + {{page-title 'Edit Namespace'}} {{/if}} diff --git a/ui/packages/consul-ui/app/templates/dc/nspaces/index.hbs b/ui/packages/consul-ui/app/templates/dc/nspaces/index.hbs index 32bd2627a1..5abe5a8a34 100644 --- a/ui/packages/consul-ui/app/templates/dc/nspaces/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/nspaces/index.hbs @@ -1,4 +1,4 @@ -{{title 'Namespaces'}} +{{page-title 'Namespaces'}} {{#let (or sortBy "Name:asc") as |sort|}} 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 4d06e0f7b9..b9101e336e 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/index.hbs @@ -1,4 +1,4 @@ -{{title 'Services'}} +{{page-title 'Services'}} {{#let (hash statuses=(if status (split status ',') undefined) diff --git a/ui/packages/consul-ui/app/templates/dc/services/instance.hbs b/ui/packages/consul-ui/app/templates/dc/services/instance.hbs index 2bf7bb194b..bddb962723 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/instance.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/instance.hbs @@ -1,4 +1,4 @@ -{{title item.Service.ID}} +{{page-title item.Service.ID}} 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 c5f454158f..a051e3cafe 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show.hbs @@ -4,7 +4,7 @@ -{{title item.Service.Service}} +{{page-title item.Service.Service}}

diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json index b031d10465..9be2546019 100644 --- a/ui/packages/consul-ui/package.json +++ b/ui/packages/consul-ui/package.json @@ -104,7 +104,6 @@ "ember-cli-yadda": "^0.5.0", "ember-collection": "^1.0.0-alpha.9", "ember-composable-helpers": "~4.0.0", - "ember-computed-style": "^0.3.0", "ember-data": "~3.20.4", "ember-data-model-fragments": "5.0.0-beta.0", "ember-exam": "^4.0.0", diff --git a/ui/yarn.lock b/ui/yarn.lock index 2fabb5364d..d9f2fbca75 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -7935,13 +7935,6 @@ ember-composable-helpers@~4.0.0: ember-cli-babel "^7.11.1" resolve "^1.10.0" -ember-computed-style@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/ember-computed-style/-/ember-computed-style-0.3.0.tgz#a04375f8b48fbf72fd61e76da3358075bd195ae9" - integrity sha512-EMMlKa8HKUCUapkJVgi/2VPyqUTWxIGs/rNsxMcMiOLFErHv6D1Mw8tvByuXeCtW8KrbwAFX3vaCMNYsAZAjvQ== - dependencies: - ember-cli-babel "^6.6.0" - ember-concurrency-decorators@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ember-concurrency-decorators/-/ember-concurrency-decorators-2.0.1.tgz#f5465785e6cf44684fb158ae6ab3aa1b131fae43" diff --git a/website/data/version.js b/website/data/version.js index b6d1f4d11e..6c19e115a2 100644 --- a/website/data/version.js +++ b/website/data/version.js @@ -1 +1 @@ -export default '1.9.0' +export default '1.9.0' \ No newline at end of file diff --git a/website/pages/docs/agent/options.mdx b/website/pages/docs/agent/options.mdx index 9b0461daf1..74928e2a4b 100644 --- a/website/pages/docs/agent/options.mdx +++ b/website/pages/docs/agent/options.mdx @@ -479,7 +479,10 @@ The options below are all specified on the command-line. This overrides the default server RPC port 8300. This is available in Consul 1.2.2 and later. -- `-non-voting-server` ((#\_non_voting_server)) - This +- `-non-voting-server` ((#\_non_voting_server)) - **This field + is deprecated in Consul 1.9.1. See the [`-read-replica`](#_read_replica) flag instead.** + +- `-read-replica` ((#\_read_replica)) - This flag is used to make the server not participate in the Raft quorum, and have it only receive the data replication stream. This can be used to add read scalability to a cluster in cases where a high volume of reads to servers are needed. @@ -1853,7 +1856,9 @@ Valid time units are 'ns', 'us' (or 'µs'), 'ms', 's', 'm', 'h'." - `server` Equivalent to the [`-server` command-line flag](#_server). -- `non_voting_server` - Equivalent to the [`-non-voting-server` command-line flag](#_non_voting_server). +- `non_voting_server` - **This field is deprecated in Consul 1.9.1. See the [`read_replica`](#read_replica) field instead.** + +- `read_replica` - Equivalent to the [`-read-replica` command-line flag](#_read_replica). - `server_name` When provided, this overrides the [`node_name`](#_node) for the TLS certificate. It can be used to ensure that the certificate name matches diff --git a/website/pages/docs/enterprise/read-scale.mdx b/website/pages/docs/enterprise/read-scale.mdx index a5bb4102f7..cf2dd536fa 100644 --- a/website/pages/docs/enterprise/read-scale.mdx +++ b/website/pages/docs/enterprise/read-scale.mdx @@ -4,12 +4,10 @@ page_title: Consul Enterprise Enhanced Read Scalability sidebar_title: Enhanced Read Scalability description: >- Consul Enterprise supports increased read scalability without impacting write - latency by introducing - - non-voting servers. + latency by introducing read replicas. --- -# Enhanced Read Scalability with Non-Voting Servers +# Enhanced Read Scalability with Read Replicas This feature requires{' '} @@ -18,10 +16,10 @@ description: >- Consul Enterprise provides the ability to scale clustered Consul servers -to include voting and non-voting servers. Non-voting servers still receive data from the cluster replication, +to include voting servers and read replicas. Read replicas still receive data from the cluster replication, however, they do not take part in quorum election operations. Expanding your Consul cluster in this way can scale reads without impacting write latency. For more details, review the [Consul server configuration](/docs/agent/options) -documentation and the [-non-voting-server](/docs/agent/options#_non_voting_server) +documentation and the [-read-replica](/docs/agent/options#_read_replica) configuration flag.