533 lines
17 KiB
Go
533 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package state
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-memdb"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
)
|
|
|
|
const (
|
|
serviceNamesUsageTable = "service-names"
|
|
kvUsageTable = "kv-entries"
|
|
connectNativeInstancesTable = "connect-native"
|
|
connectPrefix = "connect-mesh"
|
|
|
|
tableUsage = "usage"
|
|
)
|
|
|
|
var allConnectKind = []string{
|
|
string(structs.ServiceKindConnectProxy),
|
|
string(structs.ServiceKindIngressGateway),
|
|
string(structs.ServiceKindMeshGateway),
|
|
string(structs.ServiceKindTerminatingGateway),
|
|
string(structs.ServiceKindAPIGateway),
|
|
connectNativeInstancesTable,
|
|
}
|
|
|
|
// usageTableSchema returns a new table schema used for tracking various indexes
|
|
// for the Raft log.
|
|
func usageTableSchema() *memdb.TableSchema {
|
|
return &memdb.TableSchema{
|
|
Name: tableUsage,
|
|
Indexes: map[string]*memdb.IndexSchema{
|
|
indexID: {
|
|
Name: indexID,
|
|
AllowMissing: false,
|
|
Unique: true,
|
|
Indexer: &memdb.StringFieldIndex{
|
|
Field: "ID",
|
|
Lowercase: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// UsageEntry represents a count of some arbitrary identifier within the
|
|
// state store, along with the last seen index.
|
|
type UsageEntry struct {
|
|
ID string
|
|
Index uint64
|
|
Count int
|
|
}
|
|
|
|
// NodeUsage contains all of the usage data related to nodes
|
|
type NodeUsage struct {
|
|
Nodes int
|
|
EnterpriseNodeUsage
|
|
}
|
|
|
|
// PeeringUsage contains all of the usage data related to peerings.
|
|
type PeeringUsage struct {
|
|
// Number of peerings.
|
|
Peerings int
|
|
EnterprisePeeringUsage
|
|
}
|
|
|
|
type KVUsage struct {
|
|
KVCount int
|
|
EnterpriseKVUsage
|
|
}
|
|
|
|
type ConfigEntryUsage struct {
|
|
ConfigByKind map[string]int
|
|
EnterpriseConfigEntryUsage
|
|
}
|
|
|
|
type uniqueServiceState int
|
|
|
|
const (
|
|
NoChange uniqueServiceState = 0
|
|
Deleted uniqueServiceState = 1
|
|
Created uniqueServiceState = 2
|
|
)
|
|
|
|
// updateUsage takes a set of memdb changes and computes a delta for specific
|
|
// usage metrics that we track.
|
|
func updateUsage(tx WriteTxn, changes Changes) error {
|
|
usageDeltas := make(map[string]int)
|
|
serviceNameChanges := make(map[structs.ServiceName]int)
|
|
for _, change := range changes.Changes {
|
|
var delta int
|
|
if change.Created() {
|
|
delta = 1
|
|
} else if change.Deleted() {
|
|
delta = -1
|
|
}
|
|
|
|
switch change.Table {
|
|
case tableNodes:
|
|
node := changeObject(change).(*structs.Node)
|
|
if node.PeerName != "" {
|
|
// TODO(peering) track peered nodes separately. For now not tracking to avoid double billing.
|
|
continue
|
|
}
|
|
usageDeltas[change.Table] += delta
|
|
addEnterpriseNodeUsage(usageDeltas, change)
|
|
|
|
case tablePeering:
|
|
usageDeltas[change.Table] += delta
|
|
addEnterprisePeeringUsage(usageDeltas, change)
|
|
|
|
case tableServices:
|
|
svc := changeObject(change).(*structs.ServiceNode)
|
|
if svc.PeerName != "" {
|
|
// TODO(peering) track peered services separately. For now not tracking to avoid double billing.
|
|
continue
|
|
}
|
|
usageDeltas[change.Table] += delta
|
|
addEnterpriseServiceInstanceUsage(usageDeltas, change)
|
|
|
|
connectDeltas(change, usageDeltas, delta)
|
|
billableServiceInstancesDeltas(change, usageDeltas, delta)
|
|
|
|
// Construct a mapping of all of the various service names that were
|
|
// changed, in order to compare it with the finished memdb state.
|
|
// Make sure to account for the fact that services can change their names.
|
|
if serviceNameChanged(change) {
|
|
serviceNameChanges[svc.CompoundServiceName().ServiceName] += 1
|
|
before := change.Before.(*structs.ServiceNode)
|
|
serviceNameChanges[before.CompoundServiceName().ServiceName] -= 1
|
|
} else {
|
|
serviceNameChanges[svc.CompoundServiceName().ServiceName] += delta
|
|
}
|
|
|
|
case "kvs":
|
|
usageDeltas[change.Table] += delta
|
|
addEnterpriseKVUsage(usageDeltas, change)
|
|
case tableConfigEntries:
|
|
entry := changeObject(change).(structs.ConfigEntry)
|
|
usageDeltas[configEntryUsageTableName(entry.GetKind())] += delta
|
|
addEnterpriseConfigEntryUsage(usageDeltas, change)
|
|
}
|
|
}
|
|
|
|
serviceStates, err := updateServiceNameUsage(tx, usageDeltas, serviceNameChanges)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addEnterpriseServiceUsage(usageDeltas, serviceStates)
|
|
|
|
idx := changes.Index
|
|
// This will happen when restoring from a snapshot, just take the max index
|
|
// of the tables we are tracking.
|
|
if idx == 0 {
|
|
// TODO(partitions? namespaces?)
|
|
idx = maxIndexTxn(tx, tableNodes, tableServices, "kvs")
|
|
}
|
|
|
|
return writeUsageDeltas(tx, idx, usageDeltas)
|
|
}
|
|
|
|
func updateServiceNameUsage(tx WriteTxn, usageDeltas map[string]int, serviceNameChanges map[structs.ServiceName]int) (map[structs.ServiceName]uniqueServiceState, error) {
|
|
serviceStates := make(map[structs.ServiceName]uniqueServiceState, len(serviceNameChanges))
|
|
for svc, delta := range serviceNameChanges {
|
|
q := Query{
|
|
Value: svc.Name,
|
|
EnterpriseMeta: svc.EnterpriseMeta,
|
|
}
|
|
serviceIter, err := tx.Get(tableServices, indexService, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Count the number of service instances associated with the given service
|
|
// name at the end of this transaction, and compare that with how many were
|
|
// added/removed during the transaction. This allows us to handle a single
|
|
// transaction committing multiple changes related to a single service
|
|
// name.
|
|
var count int
|
|
for service := serviceIter.Next(); service != nil; service = serviceIter.Next() {
|
|
count += 1
|
|
}
|
|
|
|
var serviceState uniqueServiceState
|
|
switch {
|
|
case count == 0:
|
|
// If no services exist, we know we deleted the last service
|
|
// instance.
|
|
serviceState = Deleted
|
|
usageDeltas[serviceNamesUsageTable] -= 1
|
|
case count == delta:
|
|
// If the current number of service instances equals the number added,
|
|
// than we know we created a new service name.
|
|
serviceState = Created
|
|
usageDeltas[serviceNamesUsageTable] += 1
|
|
default:
|
|
serviceState = NoChange
|
|
}
|
|
|
|
serviceStates[svc] = serviceState
|
|
}
|
|
|
|
return serviceStates, nil
|
|
}
|
|
|
|
// serviceNameChanged returns a boolean that indicates whether the
|
|
// provided change resulted in an update to the service's service name.
|
|
func serviceNameChanged(change memdb.Change) bool {
|
|
if change.Updated() {
|
|
before := change.Before.(*structs.ServiceNode)
|
|
after := change.After.(*structs.ServiceNode)
|
|
return before.ServiceName != after.ServiceName
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// connectUsageTableEntry is a convenience function to make prefix addition in 1 place
|
|
func connectUsageTableName(kind string) string {
|
|
return fmt.Sprintf("%s-%s", connectPrefix, kind)
|
|
}
|
|
|
|
// configEntryUsageTableName is a convenience function to easily get the prefix + config entry kind in 1 place
|
|
func configEntryUsageTableName(kind string) string {
|
|
return fmt.Sprintf("%s-%s", tableConfigEntries, kind)
|
|
}
|
|
|
|
func connectDeltas(change memdb.Change, usageDeltas map[string]int, delta int) {
|
|
// Connect metrics for updated services are more complicated. Check for:
|
|
// 1. Did ServiceKind change?
|
|
// 2. Is before ServiceKind typical? don't remove from old service kind
|
|
// 3. Is After ServiceKind typical? don't add to new service kind
|
|
// 4. Add and remove to both ServiceKind's
|
|
if change.Updated() {
|
|
before := change.Before.(*structs.ServiceNode)
|
|
after := change.After.(*structs.ServiceNode)
|
|
if before.ServiceKind != structs.ServiceKindTypical {
|
|
usageDeltas[connectUsageTableName(string(before.ServiceKind))] -= 1
|
|
addEnterpriseConnectServiceInstanceUsage(usageDeltas, before, -1)
|
|
}
|
|
if after.ServiceKind != structs.ServiceKindTypical {
|
|
usageDeltas[connectUsageTableName(string(after.ServiceKind))] += 1
|
|
addEnterpriseConnectServiceInstanceUsage(usageDeltas, after, 1)
|
|
}
|
|
|
|
if before.ServiceConnect.Native != after.ServiceConnect.Native {
|
|
if before.ServiceConnect.Native {
|
|
usageDeltas[connectUsageTableName(string(connectNativeInstancesTable))] -= 1
|
|
addEnterpriseConnectServiceInstanceUsage(usageDeltas, before, -1)
|
|
} else {
|
|
usageDeltas[connectUsageTableName(connectNativeInstancesTable)] += 1
|
|
addEnterpriseConnectServiceInstanceUsage(usageDeltas, after, 1)
|
|
}
|
|
}
|
|
} else {
|
|
svc := changeObject(change).(*structs.ServiceNode)
|
|
if svc.ServiceKind != structs.ServiceKindTypical {
|
|
usageDeltas[connectUsageTableName(string(svc.ServiceKind))] += delta
|
|
}
|
|
if svc.ServiceConnect.Native {
|
|
usageDeltas[connectUsageTableName(connectNativeInstancesTable)] += delta
|
|
}
|
|
addEnterpriseConnectServiceInstanceUsage(usageDeltas, svc, delta)
|
|
}
|
|
}
|
|
|
|
// billableServiceInstancesDeltas calculates deltas for the billable services. Billable services
|
|
// are of "typical" service kind (i.e. non-connect or connect-native), excluding the "consul" service.
|
|
func billableServiceInstancesDeltas(change memdb.Change, usageDeltas map[string]int, delta int) {
|
|
// Billable service instances = # of typical service instances (i.e. non-connect) + connect-native service instances.
|
|
// Specifically, it should exclude "consul" service instances from the count.
|
|
//
|
|
// If the service has been updated, then we check
|
|
// 1. If the service name changed to or from "consul" and update deltas such that we exclude consul server service instances.
|
|
// This case is a bit contrived because we don't expect consul service to change once it's registered (beyond changing its instance count).
|
|
// a) If changed to "consul" -> decrement deltas by one
|
|
// b) If changed from "consul" and it's not a "connect" service -> increase deltas by one
|
|
// 2. If the service kind changed to or from "typical", we need to we need to update deltas so that we only account
|
|
// for non-connect or connect-native instances.
|
|
if change.Updated() {
|
|
// When there's an update, the delta arg passed to this function is 0, and so we need to explicitly increment
|
|
// or decrement by 1 depending on the situation.
|
|
before := change.Before.(*structs.ServiceNode)
|
|
after := change.After.(*structs.ServiceNode)
|
|
// Service name changed away from "consul" means we now need to account for this service instances unless it's a "connect" service.
|
|
if before.ServiceName == structs.ConsulServiceName && after.ServiceName != structs.ConsulServiceName {
|
|
if after.ServiceKind == structs.ServiceKindTypical {
|
|
usageDeltas[billableServiceInstancesTableName()] += 1
|
|
addEnterpriseBillableServiceInstanceUsage(usageDeltas, after, 1)
|
|
}
|
|
}
|
|
if before.ServiceName != structs.ConsulServiceName && after.ServiceName == structs.ConsulServiceName {
|
|
usageDeltas[billableServiceInstancesTableName()] -= 1
|
|
addEnterpriseBillableServiceInstanceUsage(usageDeltas, before, -1)
|
|
}
|
|
|
|
if before.ServiceKind != structs.ServiceKindTypical && after.ServiceKind == structs.ServiceKindTypical {
|
|
usageDeltas[billableServiceInstancesTableName()] += 1
|
|
addEnterpriseBillableServiceInstanceUsage(usageDeltas, after, 1)
|
|
} else if before.ServiceKind == structs.ServiceKindTypical && after.ServiceKind != structs.ServiceKindTypical {
|
|
usageDeltas[billableServiceInstancesTableName()] -= 1
|
|
addEnterpriseBillableServiceInstanceUsage(usageDeltas, before, -1)
|
|
}
|
|
} else {
|
|
svc := changeObject(change).(*structs.ServiceNode)
|
|
// If it's not an update, only update delta if it's a typical service and not the "consul" service.
|
|
if svc.ServiceKind == structs.ServiceKindTypical && svc.ServiceName != structs.ConsulServiceName {
|
|
usageDeltas[billableServiceInstancesTableName()] += delta
|
|
addEnterpriseBillableServiceInstanceUsage(usageDeltas, svc, delta)
|
|
}
|
|
}
|
|
}
|
|
|
|
// writeUsageDeltas will take in a map of IDs to deltas and update each
|
|
// entry accordingly, checking for integer underflow. The index that is
|
|
// passed in will be recorded on the entry as well.
|
|
func writeUsageDeltas(tx WriteTxn, idx uint64, usageDeltas map[string]int) error {
|
|
for id, delta := range usageDeltas {
|
|
u, err := tx.First(tableUsage, indexID, id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve existing usage entry: %s", err)
|
|
}
|
|
|
|
if u == nil {
|
|
if delta < 0 {
|
|
// Don't return an error here, since we don't want to block updates
|
|
// from happening to the state store. But, set the delta to 0 so that
|
|
// we do not accidentally underflow the uint64 and begin reporting
|
|
// large numbers.
|
|
delta = 0
|
|
}
|
|
err = tx.Insert(tableUsage, &UsageEntry{
|
|
ID: id,
|
|
Count: delta,
|
|
Index: idx,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update usage entry: %s", err)
|
|
}
|
|
} else if cur, ok := u.(*UsageEntry); ok {
|
|
updated := cur.Count + delta
|
|
if updated < 0 {
|
|
// Don't return an error here, since we don't want to block updates
|
|
// from happening to the state store. But, set the delta to 0 so that
|
|
// we do not accidentally underflow the uint64 and begin reporting
|
|
// large numbers.
|
|
updated = 0
|
|
}
|
|
err := tx.Insert(tableUsage, &UsageEntry{
|
|
ID: id,
|
|
Count: updated,
|
|
Index: idx,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update usage entry: %s", err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NodeUsage returns the latest seen Raft index, a compiled set of node usage
|
|
// data, and any errors.
|
|
func (s *Store) NodeUsage() (uint64, NodeUsage, error) {
|
|
tx := s.db.ReadTxn()
|
|
defer tx.Abort()
|
|
|
|
nodes, err := firstUsageEntry(nil, tx, tableNodes)
|
|
if err != nil {
|
|
return 0, NodeUsage{}, fmt.Errorf("failed nodes lookup: %s", err)
|
|
}
|
|
|
|
usage := NodeUsage{
|
|
Nodes: nodes.Count,
|
|
}
|
|
results, err := compileEnterpriseNodeUsage(tx, usage)
|
|
if err != nil {
|
|
return 0, NodeUsage{}, fmt.Errorf("failed nodes lookup: %s", err)
|
|
}
|
|
|
|
return nodes.Index, results, nil
|
|
}
|
|
|
|
// PeeringUsage returns the latest seen Raft index, a compiled set of peering usage
|
|
// data, and any errors.
|
|
func (s *Store) PeeringUsage() (uint64, PeeringUsage, error) {
|
|
tx := s.db.ReadTxn()
|
|
defer tx.Abort()
|
|
|
|
peerings, err := firstUsageEntry(nil, tx, tablePeering)
|
|
if err != nil {
|
|
return 0, PeeringUsage{}, fmt.Errorf("failed peerings lookup: %s", err)
|
|
}
|
|
|
|
usage := PeeringUsage{
|
|
Peerings: peerings.Count,
|
|
}
|
|
results, err := compileEnterprisePeeringUsage(tx, usage)
|
|
if err != nil {
|
|
return 0, PeeringUsage{}, fmt.Errorf("failed peerings lookup: %s", err)
|
|
}
|
|
|
|
return peerings.Index, results, nil
|
|
}
|
|
|
|
// ServiceUsage returns the latest seen Raft index, a compiled set of service
|
|
// usage data, and any errors.
|
|
func (s *Store) ServiceUsage(ws memdb.WatchSet) (uint64, structs.ServiceUsage, error) {
|
|
tx := s.db.ReadTxn()
|
|
defer tx.Abort()
|
|
|
|
serviceInstances, err := firstUsageEntry(ws, tx, tableServices)
|
|
if err != nil {
|
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
|
}
|
|
|
|
services, err := firstUsageEntry(ws, tx, serviceNamesUsageTable)
|
|
if err != nil {
|
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
|
}
|
|
|
|
nodes, err := firstUsageEntry(ws, tx, tableNodes)
|
|
if err != nil {
|
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed nodes lookup: %s", err)
|
|
}
|
|
|
|
serviceKindInstances := make(map[string]int)
|
|
for _, kind := range allConnectKind {
|
|
usage, err := firstUsageEntry(ws, tx, connectUsageTableName(kind))
|
|
if err != nil {
|
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
|
}
|
|
serviceKindInstances[kind] = usage.Count
|
|
}
|
|
|
|
billableServiceInstances, err := firstUsageEntry(ws, tx, billableServiceInstancesTableName())
|
|
if err != nil {
|
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed billable services lookup: %s", err)
|
|
}
|
|
|
|
usage := structs.ServiceUsage{
|
|
ServiceInstances: serviceInstances.Count,
|
|
Services: services.Count,
|
|
ConnectServiceInstances: serviceKindInstances,
|
|
BillableServiceInstances: billableServiceInstances.Count,
|
|
Nodes: nodes.Count,
|
|
}
|
|
results, err := compileEnterpriseServiceUsage(ws, tx, usage)
|
|
if err != nil {
|
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
|
}
|
|
|
|
return serviceInstances.Index, results, nil
|
|
}
|
|
|
|
func (s *Store) KVUsage() (uint64, KVUsage, error) {
|
|
tx := s.db.ReadTxn()
|
|
defer tx.Abort()
|
|
|
|
kvs, err := firstUsageEntry(nil, tx, "kvs")
|
|
if err != nil {
|
|
return 0, KVUsage{}, fmt.Errorf("failed kvs lookup: %s", err)
|
|
}
|
|
|
|
usage := KVUsage{
|
|
KVCount: kvs.Count,
|
|
}
|
|
results, err := compileEnterpriseKVUsage(tx, usage)
|
|
if err != nil {
|
|
return 0, KVUsage{}, fmt.Errorf("failed kvs lookup: %s", err)
|
|
}
|
|
|
|
return kvs.Index, results, nil
|
|
}
|
|
|
|
func (s *Store) ConfigEntryUsage() (uint64, ConfigEntryUsage, error) {
|
|
tx := s.db.ReadTxn()
|
|
defer tx.Abort()
|
|
|
|
configEntries := make(map[string]int)
|
|
var maxIdx uint64
|
|
for _, kind := range structs.AllConfigEntryKinds {
|
|
configEntry, err := firstUsageEntry(nil, tx, configEntryUsageTableName(kind))
|
|
if configEntry.Index > maxIdx {
|
|
maxIdx = configEntry.Index
|
|
}
|
|
if err != nil {
|
|
return 0, ConfigEntryUsage{}, fmt.Errorf("failed config entry usage lookup: %s", err)
|
|
}
|
|
configEntries[kind] = configEntry.Count
|
|
}
|
|
usage := ConfigEntryUsage{
|
|
ConfigByKind: configEntries,
|
|
}
|
|
results, err := compileEnterpriseConfigEntryUsage(tx, usage)
|
|
if err != nil {
|
|
return 0, ConfigEntryUsage{}, fmt.Errorf("failed config entry usage lookup: %s", err)
|
|
}
|
|
|
|
return maxIdx, results, nil
|
|
}
|
|
|
|
func firstUsageEntry(ws memdb.WatchSet, tx ReadTxn, id string) (*UsageEntry, error) {
|
|
watch, usage, err := tx.FirstWatch(tableUsage, indexID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ws.Add(watch)
|
|
|
|
// If no elements have been inserted, the usage entry will not exist. We
|
|
// return a valid value so that can be certain the return value is not nil
|
|
// when no error has occurred.
|
|
if usage == nil {
|
|
return &UsageEntry{ID: id, Count: 0}, nil
|
|
}
|
|
|
|
realUsage, ok := usage.(*UsageEntry)
|
|
if !ok {
|
|
return nil, fmt.Errorf("failed usage lookup: type %T is not *UsageEntry", usage)
|
|
}
|
|
|
|
return realUsage, nil
|
|
}
|
|
|
|
func billableServiceInstancesTableName() string {
|
|
return fmt.Sprintf("billable-%s", tableServices)
|
|
}
|