Add the `operator usage instances` command and api endpoint (#16205)
This endpoint shows total services, connect service instances and billable service instances in the local datacenter or globally. Billable instances = total service instances - connect services - consul server instances.
This commit is contained in:
parent
fd010a326c
commit
220ca06201
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
command: Adds the `operator usage instances` subcommand for displaying total services, connect service instances and billable service instances in the local datacenter or globally.
|
||||||
|
```
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
metrics "github.com/armon/go-metrics"
|
"github.com/armon/go-metrics"
|
||||||
"github.com/armon/go-metrics/prometheus"
|
"github.com/armon/go-metrics/prometheus"
|
||||||
|
|
||||||
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package consul
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/go-memdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Usage returns counts for service usage within catalog.
|
||||||
|
func (op *Operator) Usage(args *structs.OperatorUsageRequest, reply *structs.Usage) error {
|
||||||
|
reply.Usage = make(map[string]structs.ServiceUsage)
|
||||||
|
|
||||||
|
if args.Global {
|
||||||
|
remoteDCs := op.srv.router.GetDatacenters()
|
||||||
|
for _, dc := range remoteDCs {
|
||||||
|
remoteArgs := &structs.OperatorUsageRequest{
|
||||||
|
DCSpecificRequest: structs.DCSpecificRequest{
|
||||||
|
Datacenter: dc,
|
||||||
|
QueryOptions: structs.QueryOptions{
|
||||||
|
Token: args.Token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var resp structs.Usage
|
||||||
|
if _, err := op.srv.ForwardRPC("Operator.Usage", remoteArgs, &resp); err != nil {
|
||||||
|
op.logger.Warn("error forwarding usage request to remote datacenter", "datacenter", dc, "error", err)
|
||||||
|
}
|
||||||
|
if usage, ok := resp.Usage[dc]; ok {
|
||||||
|
reply.Usage[dc] = usage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
authz, err := op.srv.ResolveTokenAndDefaultMeta(args.Token, structs.DefaultEnterpriseMetaInDefaultPartition(), &authzContext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = authz.ToAllowAuthorizer().OperatorReadAllowed(&authzContext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = op.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return op.srv.blockingQuery(
|
||||||
|
&args.QueryOptions,
|
||||||
|
&reply.QueryMeta,
|
||||||
|
func(ws memdb.WatchSet, state *state.Store) error {
|
||||||
|
// Get service usage.
|
||||||
|
index, serviceUsage, err := state.ServiceUsage(ws)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.QueryMeta.Index, reply.Usage[op.srv.config.Datacenter] = index, serviceUsage
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -52,14 +52,6 @@ type UsageEntry struct {
|
||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceUsage contains all of the usage data related to services
|
|
||||||
type ServiceUsage struct {
|
|
||||||
Services int
|
|
||||||
ServiceInstances int
|
|
||||||
ConnectServiceInstances map[string]int
|
|
||||||
EnterpriseServiceUsage
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeUsage contains all of the usage data related to nodes
|
// NodeUsage contains all of the usage data related to nodes
|
||||||
type NodeUsage struct {
|
type NodeUsage struct {
|
||||||
Nodes int
|
Nodes int
|
||||||
|
@ -128,6 +120,8 @@ func updateUsage(tx WriteTxn, changes Changes) error {
|
||||||
addEnterpriseServiceInstanceUsage(usageDeltas, change)
|
addEnterpriseServiceInstanceUsage(usageDeltas, change)
|
||||||
|
|
||||||
connectDeltas(change, usageDeltas, delta)
|
connectDeltas(change, usageDeltas, delta)
|
||||||
|
billableServiceInstancesDeltas(change, usageDeltas, delta)
|
||||||
|
|
||||||
// Construct a mapping of all of the various service names that were
|
// Construct a mapping of all of the various service names that were
|
||||||
// changed, in order to compare it with the finished memdb state.
|
// changed, in order to compare it with the finished memdb state.
|
||||||
// Make sure to account for the fact that services can change their names.
|
// Make sure to account for the fact that services can change their names.
|
||||||
|
@ -271,6 +265,53 @@ func connectDeltas(change memdb.Change, usageDeltas map[string]int, delta int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// writeUsageDeltas will take in a map of IDs to deltas and update each
|
||||||
// entry accordingly, checking for integer underflow. The index that is
|
// entry accordingly, checking for integer underflow. The index that is
|
||||||
// passed in will be recorded on the entry as well.
|
// passed in will be recorded on the entry as well.
|
||||||
|
@ -289,7 +330,7 @@ func writeUsageDeltas(tx WriteTxn, idx uint64, usageDeltas map[string]int) error
|
||||||
// large numbers.
|
// large numbers.
|
||||||
delta = 0
|
delta = 0
|
||||||
}
|
}
|
||||||
err := tx.Insert(tableUsage, &UsageEntry{
|
err = tx.Insert(tableUsage, &UsageEntry{
|
||||||
ID: id,
|
ID: id,
|
||||||
Count: delta,
|
Count: delta,
|
||||||
Index: idx,
|
Index: idx,
|
||||||
|
@ -365,37 +406,43 @@ func (s *Store) PeeringUsage() (uint64, PeeringUsage, error) {
|
||||||
|
|
||||||
// ServiceUsage returns the latest seen Raft index, a compiled set of service
|
// ServiceUsage returns the latest seen Raft index, a compiled set of service
|
||||||
// usage data, and any errors.
|
// usage data, and any errors.
|
||||||
func (s *Store) ServiceUsage(ws memdb.WatchSet) (uint64, ServiceUsage, error) {
|
func (s *Store) ServiceUsage(ws memdb.WatchSet) (uint64, structs.ServiceUsage, error) {
|
||||||
tx := s.db.ReadTxn()
|
tx := s.db.ReadTxn()
|
||||||
defer tx.Abort()
|
defer tx.Abort()
|
||||||
|
|
||||||
serviceInstances, err := firstUsageEntry(ws, tx, tableServices)
|
serviceInstances, err := firstUsageEntry(ws, tx, tableServices)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
services, err := firstUsageEntry(ws, tx, serviceNamesUsageTable)
|
services, err := firstUsageEntry(ws, tx, serviceNamesUsageTable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceKindInstances := make(map[string]int)
|
serviceKindInstances := make(map[string]int)
|
||||||
for _, kind := range allConnectKind {
|
for _, kind := range allConnectKind {
|
||||||
usage, err := firstUsageEntry(ws, tx, connectUsageTableName(kind))
|
usage, err := firstUsageEntry(ws, tx, connectUsageTableName(kind))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
||||||
}
|
}
|
||||||
serviceKindInstances[kind] = usage.Count
|
serviceKindInstances[kind] = usage.Count
|
||||||
}
|
}
|
||||||
|
|
||||||
usage := ServiceUsage{
|
billableServiceInstances, err := firstUsageEntry(ws, tx, billableServiceInstancesTableName())
|
||||||
ServiceInstances: serviceInstances.Count,
|
if err != nil {
|
||||||
Services: services.Count,
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed billable services lookup: %s", err)
|
||||||
ConnectServiceInstances: serviceKindInstances,
|
}
|
||||||
|
|
||||||
|
usage := structs.ServiceUsage{
|
||||||
|
ServiceInstances: serviceInstances.Count,
|
||||||
|
Services: services.Count,
|
||||||
|
ConnectServiceInstances: serviceKindInstances,
|
||||||
|
BillableServiceInstances: billableServiceInstances.Count,
|
||||||
}
|
}
|
||||||
results, err := compileEnterpriseServiceUsage(ws, tx, usage)
|
results, err := compileEnterpriseServiceUsage(ws, tx, usage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
return 0, structs.ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return serviceInstances.Index, results, nil
|
return serviceInstances.Index, results, nil
|
||||||
|
@ -469,3 +516,7 @@ func firstUsageEntry(ws memdb.WatchSet, tx ReadTxn, id string) (*UsageEntry, err
|
||||||
|
|
||||||
return realUsage, nil
|
return realUsage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func billableServiceInstancesTableName() string {
|
||||||
|
return fmt.Sprintf("billable-%s", tableServices)
|
||||||
|
}
|
||||||
|
|
|
@ -25,11 +25,13 @@ func addEnterpriseServiceUsage(map[string]int, map[structs.ServiceName]uniqueSer
|
||||||
|
|
||||||
func addEnterpriseConnectServiceInstanceUsage(map[string]int, *structs.ServiceNode, int) {}
|
func addEnterpriseConnectServiceInstanceUsage(map[string]int, *structs.ServiceNode, int) {}
|
||||||
|
|
||||||
|
func addEnterpriseBillableServiceInstanceUsage(map[string]int, *structs.ServiceNode, int) {}
|
||||||
|
|
||||||
func addEnterpriseKVUsage(map[string]int, memdb.Change) {}
|
func addEnterpriseKVUsage(map[string]int, memdb.Change) {}
|
||||||
|
|
||||||
func addEnterpriseConfigEntryUsage(map[string]int, memdb.Change) {}
|
func addEnterpriseConfigEntryUsage(map[string]int, memdb.Change) {}
|
||||||
|
|
||||||
func compileEnterpriseServiceUsage(ws memdb.WatchSet, tx ReadTxn, usage ServiceUsage) (ServiceUsage, error) {
|
func compileEnterpriseServiceUsage(ws memdb.WatchSet, tx ReadTxn, usage structs.ServiceUsage) (structs.ServiceUsage, error) {
|
||||||
return usage, nil
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,6 +160,7 @@ func TestStateStore_Usage_ServiceUsageEmpty(t *testing.T) {
|
||||||
for k := range usage.ConnectServiceInstances {
|
for k := range usage.ConnectServiceInstances {
|
||||||
require.Equal(t, 0, usage.ConnectServiceInstances[k])
|
require.Equal(t, 0, usage.ConnectServiceInstances[k])
|
||||||
}
|
}
|
||||||
|
require.Equal(t, 0, usage.BillableServiceInstances)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStateStore_Usage_ServiceUsage(t *testing.T) {
|
func TestStateStore_Usage_ServiceUsage(t *testing.T) {
|
||||||
|
@ -184,6 +185,7 @@ func TestStateStore_Usage_ServiceUsage(t *testing.T) {
|
||||||
require.Equal(t, 8, usage.ServiceInstances)
|
require.Equal(t, 8, usage.ServiceInstances)
|
||||||
require.Equal(t, 2, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
require.Equal(t, 2, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
||||||
require.Equal(t, 3, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 3, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 6, usage.BillableServiceInstances)
|
||||||
|
|
||||||
testRegisterSidecarProxy(t, s, 16, "node2", "service2")
|
testRegisterSidecarProxy(t, s, 16, "node2", "service2")
|
||||||
|
|
||||||
|
@ -225,6 +227,7 @@ func TestStateStore_Usage_ServiceUsage_DeleteNode(t *testing.T) {
|
||||||
require.Equal(t, 4, usage.ServiceInstances)
|
require.Equal(t, 4, usage.ServiceInstances)
|
||||||
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
||||||
require.Equal(t, 1, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 1, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 3, usage.BillableServiceInstances)
|
||||||
|
|
||||||
require.NoError(t, s.DeleteNode(4, "node1", nil, ""))
|
require.NoError(t, s.DeleteNode(4, "node1", nil, ""))
|
||||||
|
|
||||||
|
@ -236,6 +239,7 @@ func TestStateStore_Usage_ServiceUsage_DeleteNode(t *testing.T) {
|
||||||
for k := range usage.ConnectServiceInstances {
|
for k := range usage.ConnectServiceInstances {
|
||||||
require.Equal(t, 0, usage.ConnectServiceInstances[k])
|
require.Equal(t, 0, usage.ConnectServiceInstances[k])
|
||||||
}
|
}
|
||||||
|
require.Equal(t, 0, usage.BillableServiceInstances)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that services from remote peers aren't counted in writes or deletes.
|
// Test that services from remote peers aren't counted in writes or deletes.
|
||||||
|
@ -263,6 +267,7 @@ func TestStateStore_Usage_ServiceUsagePeering(t *testing.T) {
|
||||||
require.Equal(t, 3, usage.ServiceInstances)
|
require.Equal(t, 3, usage.ServiceInstances)
|
||||||
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
||||||
require.Equal(t, 1, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 1, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 2, usage.BillableServiceInstances)
|
||||||
})
|
})
|
||||||
|
|
||||||
testutil.RunStep(t, "deletes", func(t *testing.T) {
|
testutil.RunStep(t, "deletes", func(t *testing.T) {
|
||||||
|
@ -275,6 +280,7 @@ func TestStateStore_Usage_ServiceUsagePeering(t *testing.T) {
|
||||||
require.Equal(t, 0, usage.ServiceInstances)
|
require.Equal(t, 0, usage.ServiceInstances)
|
||||||
require.Equal(t, 0, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
require.Equal(t, 0, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
||||||
require.Equal(t, 0, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 0, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 0, usage.BillableServiceInstances)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,6 +317,7 @@ func TestStateStore_Usage_Restore(t *testing.T) {
|
||||||
require.Equal(t, idx, uint64(9))
|
require.Equal(t, idx, uint64(9))
|
||||||
require.Equal(t, usage.Services, 1)
|
require.Equal(t, usage.Services, 1)
|
||||||
require.Equal(t, usage.ServiceInstances, 2)
|
require.Equal(t, usage.ServiceInstances, 2)
|
||||||
|
require.Equal(t, usage.BillableServiceInstances, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStateStore_Usage_updateUsage_Underflow(t *testing.T) {
|
func TestStateStore_Usage_updateUsage_Underflow(t *testing.T) {
|
||||||
|
@ -411,6 +418,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) {
|
||||||
require.Equal(t, idx, uint64(2))
|
require.Equal(t, idx, uint64(2))
|
||||||
require.Equal(t, usage.Services, 1)
|
require.Equal(t, usage.Services, 1)
|
||||||
require.Equal(t, usage.ServiceInstances, 1)
|
require.Equal(t, usage.ServiceInstances, 1)
|
||||||
|
require.Equal(t, usage.BillableServiceInstances, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("update service to be connect native", func(t *testing.T) {
|
t.Run("update service to be connect native", func(t *testing.T) {
|
||||||
|
@ -432,6 +440,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) {
|
||||||
require.Equal(t, usage.Services, 1)
|
require.Equal(t, usage.Services, 1)
|
||||||
require.Equal(t, usage.ServiceInstances, 1)
|
require.Equal(t, usage.ServiceInstances, 1)
|
||||||
require.Equal(t, 1, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 1, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 1, usage.BillableServiceInstances)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("update service to not be connect native", func(t *testing.T) {
|
t.Run("update service to not be connect native", func(t *testing.T) {
|
||||||
|
@ -453,6 +462,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) {
|
||||||
require.Equal(t, usage.Services, 1)
|
require.Equal(t, usage.Services, 1)
|
||||||
require.Equal(t, usage.ServiceInstances, 1)
|
require.Equal(t, usage.ServiceInstances, 1)
|
||||||
require.Equal(t, 0, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 0, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 1, usage.BillableServiceInstances)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("rename service with a multiple instances", func(t *testing.T) {
|
t.Run("rename service with a multiple instances", func(t *testing.T) {
|
||||||
|
@ -484,6 +494,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) {
|
||||||
require.Equal(t, usage.Services, 2)
|
require.Equal(t, usage.Services, 2)
|
||||||
require.Equal(t, usage.ServiceInstances, 3)
|
require.Equal(t, usage.ServiceInstances, 3)
|
||||||
require.Equal(t, 2, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 2, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 3, usage.BillableServiceInstances)
|
||||||
|
|
||||||
update := &structs.NodeService{
|
update := &structs.NodeService{
|
||||||
ID: "service2",
|
ID: "service2",
|
||||||
|
@ -502,6 +513,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) {
|
||||||
require.Equal(t, usage.Services, 3)
|
require.Equal(t, usage.Services, 3)
|
||||||
require.Equal(t, usage.ServiceInstances, 3)
|
require.Equal(t, usage.ServiceInstances, 3)
|
||||||
require.Equal(t, 2, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
require.Equal(t, 2, usage.ConnectServiceInstances[connectNativeInstancesTable])
|
||||||
|
require.Equal(t, 3, usage.BillableServiceInstances)
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -528,6 +540,7 @@ func TestStateStore_Usage_ServiceUsage_updatingConnectProxy(t *testing.T) {
|
||||||
require.Equal(t, usage.Services, 1)
|
require.Equal(t, usage.Services, 1)
|
||||||
require.Equal(t, usage.ServiceInstances, 1)
|
require.Equal(t, usage.ServiceInstances, 1)
|
||||||
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
||||||
|
require.Equal(t, 0, usage.BillableServiceInstances)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("rename service with a multiple instances", func(t *testing.T) {
|
t.Run("rename service with a multiple instances", func(t *testing.T) {
|
||||||
|
@ -554,6 +567,7 @@ func TestStateStore_Usage_ServiceUsage_updatingConnectProxy(t *testing.T) {
|
||||||
require.Equal(t, usage.Services, 2)
|
require.Equal(t, usage.Services, 2)
|
||||||
require.Equal(t, usage.ServiceInstances, 3)
|
require.Equal(t, usage.ServiceInstances, 3)
|
||||||
require.Equal(t, 2, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
require.Equal(t, 2, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
||||||
|
require.Equal(t, 1, usage.BillableServiceInstances)
|
||||||
|
|
||||||
update := &structs.NodeService{
|
update := &structs.NodeService{
|
||||||
ID: "service3",
|
ID: "service3",
|
||||||
|
@ -569,6 +583,7 @@ func TestStateStore_Usage_ServiceUsage_updatingConnectProxy(t *testing.T) {
|
||||||
require.Equal(t, usage.Services, 3)
|
require.Equal(t, usage.Services, 3)
|
||||||
require.Equal(t, usage.ServiceInstances, 3)
|
require.Equal(t, usage.ServiceInstances, 3)
|
||||||
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
|
||||||
|
require.Equal(t, 2, usage.BillableServiceInstances)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,10 @@ var Gauges = []prometheus.GaugeDefinition{
|
||||||
Name: []string{"state", "config_entries"},
|
Name: []string{"state", "config_entries"},
|
||||||
Help: "Measures the current number of unique configuration entries registered with Consul, labeled by Kind. It is only emitted by Consul servers. Added in v1.10.4.",
|
Help: "Measures the current number of unique configuration entries registered with Consul, labeled by Kind. It is only emitted by Consul servers. Added in v1.10.4.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: []string{"state", "billable_service_instances"},
|
||||||
|
Help: "Total number of billable service instances in the local datacenter.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type getMembersFunc func() []serf.Member
|
type getMembersFunc func() []serf.Member
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/consul/state"
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u *UsageMetricsReporter) emitNodeUsage(nodeUsage state.NodeUsage) {
|
func (u *UsageMetricsReporter) emitNodeUsage(nodeUsage state.NodeUsage) {
|
||||||
|
@ -74,7 +75,7 @@ func (u *UsageMetricsReporter) emitMemberUsage(members []serf.Member) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsageMetricsReporter) emitServiceUsage(serviceUsage state.ServiceUsage) {
|
func (u *UsageMetricsReporter) emitServiceUsage(serviceUsage structs.ServiceUsage) {
|
||||||
metrics.SetGaugeWithLabels(
|
metrics.SetGaugeWithLabels(
|
||||||
[]string{"consul", "state", "services"},
|
[]string{"consul", "state", "services"},
|
||||||
float32(serviceUsage.Services),
|
float32(serviceUsage.Services),
|
||||||
|
@ -96,6 +97,11 @@ func (u *UsageMetricsReporter) emitServiceUsage(serviceUsage state.ServiceUsage)
|
||||||
float32(serviceUsage.ServiceInstances),
|
float32(serviceUsage.ServiceInstances),
|
||||||
u.metricLabels,
|
u.metricLabels,
|
||||||
)
|
)
|
||||||
|
metrics.SetGaugeWithLabels(
|
||||||
|
[]string{"state", "billable_service_instances"},
|
||||||
|
float32(serviceUsage.BillableServiceInstances),
|
||||||
|
u.metricLabels,
|
||||||
|
)
|
||||||
|
|
||||||
for k, i := range serviceUsage.ConnectServiceInstances {
|
for k, i := range serviceUsage.ConnectServiceInstances {
|
||||||
metrics.SetGaugeWithLabels(
|
metrics.SetGaugeWithLabels(
|
||||||
|
|
|
@ -178,6 +178,13 @@ var baseCases = map[string]testCase{
|
||||||
{Name: "kind", Value: "connect-native"},
|
{Name: "kind", Value: "connect-native"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"consul.usage.test.state.billable_service_instances;datacenter=dc1": {
|
||||||
|
Name: "consul.usage.test.state.billable_service_instances",
|
||||||
|
Value: 0,
|
||||||
|
Labels: []metrics.Label{
|
||||||
|
{Name: "datacenter", Value: "dc1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
// --- kv ---
|
// --- kv ---
|
||||||
"consul.usage.test.consul.state.kv_entries;datacenter=dc1": { // Legacy
|
"consul.usage.test.consul.state.kv_entries;datacenter=dc1": { // Legacy
|
||||||
Name: "consul.usage.test.consul.state.kv_entries",
|
Name: "consul.usage.test.consul.state.kv_entries",
|
||||||
|
@ -598,6 +605,13 @@ var baseCases = map[string]testCase{
|
||||||
{Name: "kind", Value: "connect-native"},
|
{Name: "kind", Value: "connect-native"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"consul.usage.test.state.billable_service_instances;datacenter=dc1": {
|
||||||
|
Name: "consul.usage.test.state.billable_service_instances",
|
||||||
|
Value: 0,
|
||||||
|
Labels: []metrics.Label{
|
||||||
|
{Name: "datacenter", Value: "dc1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
// --- kv ---
|
// --- kv ---
|
||||||
"consul.usage.test.consul.state.kv_entries;datacenter=dc1": { // Legacy
|
"consul.usage.test.consul.state.kv_entries;datacenter=dc1": { // Legacy
|
||||||
Name: "consul.usage.test.consul.state.kv_entries",
|
Name: "consul.usage.test.consul.state.kv_entries",
|
||||||
|
@ -1176,6 +1190,13 @@ func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) {
|
||||||
{Name: "kind", Value: "connect-native"},
|
{Name: "kind", Value: "connect-native"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
nodesAndSvcsCase.expectedGauges["consul.usage.test.state.billable_service_instances;datacenter=dc1"] = metrics.GaugeValue{
|
||||||
|
Name: "consul.usage.test.state.billable_service_instances",
|
||||||
|
Value: 3,
|
||||||
|
Labels: []metrics.Label{
|
||||||
|
{Name: "datacenter", Value: "dc1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ // Legacy
|
nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ // Legacy
|
||||||
Name: "consul.usage.test.consul.state.config_entries",
|
Name: "consul.usage.test.consul.state.config_entries",
|
||||||
Value: 3,
|
Value: 3,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/consul/state"
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/lib/retry"
|
"github.com/hashicorp/consul/lib/retry"
|
||||||
)
|
)
|
||||||
|
@ -206,5 +205,5 @@ func (c *Controller) countProxies(ctx context.Context) (<-chan error, uint32, er
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
AbandonCh() <-chan struct{}
|
AbandonCh() <-chan struct{}
|
||||||
ServiceUsage(ws memdb.WatchSet) (uint64, state.ServiceUsage, error)
|
ServiceUsage(ws memdb.WatchSet) (uint64, structs.ServiceUsage, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,7 @@ func init() {
|
||||||
registerEndpoint("/v1/operator/raft/transfer-leader", []string{"POST"}, (*HTTPHandlers).OperatorRaftTransferLeader)
|
registerEndpoint("/v1/operator/raft/transfer-leader", []string{"POST"}, (*HTTPHandlers).OperatorRaftTransferLeader)
|
||||||
registerEndpoint("/v1/operator/raft/peer", []string{"DELETE"}, (*HTTPHandlers).OperatorRaftPeer)
|
registerEndpoint("/v1/operator/raft/peer", []string{"DELETE"}, (*HTTPHandlers).OperatorRaftPeer)
|
||||||
registerEndpoint("/v1/operator/keyring", []string{"GET", "POST", "PUT", "DELETE"}, (*HTTPHandlers).OperatorKeyringEndpoint)
|
registerEndpoint("/v1/operator/keyring", []string{"GET", "POST", "PUT", "DELETE"}, (*HTTPHandlers).OperatorKeyringEndpoint)
|
||||||
|
registerEndpoint("/v1/operator/usage", []string{"GET"}, (*HTTPHandlers).OperatorUsage)
|
||||||
registerEndpoint("/v1/operator/autopilot/configuration", []string{"GET", "PUT"}, (*HTTPHandlers).OperatorAutopilotConfiguration)
|
registerEndpoint("/v1/operator/autopilot/configuration", []string{"GET", "PUT"}, (*HTTPHandlers).OperatorAutopilotConfiguration)
|
||||||
registerEndpoint("/v1/operator/autopilot/health", []string{"GET"}, (*HTTPHandlers).OperatorServerHealth)
|
registerEndpoint("/v1/operator/autopilot/health", []string{"GET"}, (*HTTPHandlers).OperatorServerHealth)
|
||||||
registerEndpoint("/v1/operator/autopilot/state", []string{"GET"}, (*HTTPHandlers).OperatorAutopilotState)
|
registerEndpoint("/v1/operator/autopilot/state", []string{"GET"}, (*HTTPHandlers).OperatorAutopilotState)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/armon/go-metrics"
|
||||||
external "github.com/hashicorp/consul/agent/grpc-external"
|
external "github.com/hashicorp/consul/agent/grpc-external"
|
||||||
"github.com/hashicorp/consul/proto/pboperator"
|
"github.com/hashicorp/consul/proto/pboperator"
|
||||||
|
|
||||||
|
@ -366,6 +367,43 @@ func (s *HTTPHandlers) OperatorAutopilotState(resp http.ResponseWriter, req *htt
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *HTTPHandlers) OperatorUsage(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
|
metrics.IncrCounterWithLabels([]string{"client", "api", "operator_usage"}, 1,
|
||||||
|
s.nodeMetricsLabels())
|
||||||
|
|
||||||
|
var args structs.OperatorUsageRequest
|
||||||
|
|
||||||
|
if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if _, ok := req.URL.Query()["global"]; ok {
|
||||||
|
args.Global = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the RPC request
|
||||||
|
var out structs.Usage
|
||||||
|
defer setMeta(resp, &out.QueryMeta)
|
||||||
|
RETRY_ONCE:
|
||||||
|
err := s.agent.RPC(req.Context(), "Operator.Usage", &args, &out)
|
||||||
|
if err != nil {
|
||||||
|
metrics.IncrCounterWithLabels([]string{"client", "rpc", "error", "operator_usage"}, 1,
|
||||||
|
s.nodeMetricsLabels())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if args.QueryOptions.AllowStale && args.MaxStaleDuration > 0 && args.MaxStaleDuration < out.LastContact {
|
||||||
|
args.AllowStale = false
|
||||||
|
args.MaxStaleDuration = 0
|
||||||
|
goto RETRY_ONCE
|
||||||
|
}
|
||||||
|
out.ConsistencyLevel = args.QueryOptions.ConsistencyLevel()
|
||||||
|
metrics.IncrCounterWithLabels([]string{"client", "api", "success", "operator_usage"}, 1,
|
||||||
|
s.nodeMetricsLabels())
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func stringIDs(ids []raft.ServerID) []string {
|
func stringIDs(ids []raft.ServerID) []string {
|
||||||
out := make([]string, len(ids))
|
out := make([]string, len(ids))
|
||||||
for i, id := range ids {
|
for i, id := range ids {
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
//go:build !consulent
|
||||||
|
// +build !consulent
|
||||||
|
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOperator_Usage(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("too slow for testing.Short")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
a := NewTestAgent(t, "")
|
||||||
|
defer a.Shutdown()
|
||||||
|
req, err := http.NewRequest("GET", "/v1/operator/usage", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Register a few services
|
||||||
|
require.NoError(t, upsertTestService(a.RPC, "", "dc1", "web", "test-node", "", func(svc *structs.NodeService) {
|
||||||
|
svc.ID = "web1"
|
||||||
|
}))
|
||||||
|
require.NoError(t, upsertTestService(a.RPC, "", "dc1", "web", "test-node", "", func(svc *structs.NodeService) {
|
||||||
|
svc.ID = "web2"
|
||||||
|
}))
|
||||||
|
require.NoError(t, upsertTestService(a.RPC, "", "dc1", "db", "test-node", ""))
|
||||||
|
require.NoError(t, upsertTestService(a.RPC, "", "dc1", "web-proxy", "test-node", "", func(svc *structs.NodeService) {
|
||||||
|
svc.Kind = structs.ServiceKindConnectProxy
|
||||||
|
svc.Proxy = structs.ConnectProxyConfig{
|
||||||
|
DestinationServiceName: "web",
|
||||||
|
DestinationServiceID: "web1",
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Add connect-native service to check that we include it in the billable service instances
|
||||||
|
require.NoError(t, upsertTestService(a.RPC, "", "dc1", "connect-native-app", "test-node", "", func(svc *structs.NodeService) {
|
||||||
|
svc.Connect.Native = true
|
||||||
|
}))
|
||||||
|
|
||||||
|
raw, err := a.srv.OperatorUsage(httptest.NewRecorder(), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := map[string]structs.ServiceUsage{
|
||||||
|
"dc1": {
|
||||||
|
Services: 5,
|
||||||
|
ServiceInstances: 6,
|
||||||
|
ConnectServiceInstances: map[string]int{
|
||||||
|
"connect-native": 1,
|
||||||
|
"connect-proxy": 1,
|
||||||
|
"ingress-gateway": 0,
|
||||||
|
"mesh-gateway": 0,
|
||||||
|
"terminating-gateway": 0,
|
||||||
|
},
|
||||||
|
// 4 = 6 total service instances - 1 connect proxy - 1 consul service
|
||||||
|
BillableServiceInstances: 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(t, expected, raw.(structs.Usage).Usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertTestService(rpc rpcFn, secret, datacenter, name, node, partition string, modifyFuncs ...func(*structs.NodeService)) error {
|
||||||
|
req := structs.RegisterRequest{
|
||||||
|
Datacenter: datacenter,
|
||||||
|
Node: node,
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: name,
|
||||||
|
Service: name,
|
||||||
|
Port: 8080,
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: secret},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, modify := range modifyFuncs {
|
||||||
|
modify(req.Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out struct{}
|
||||||
|
return rpc(context.Background(), "Catalog.Register", &req, &out)
|
||||||
|
}
|
|
@ -629,6 +629,12 @@ func (r *DCSpecificRequest) CacheMinIndex() uint64 {
|
||||||
return r.QueryOptions.MinQueryIndex
|
return r.QueryOptions.MinQueryIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OperatorUsageRequest struct {
|
||||||
|
DCSpecificRequest
|
||||||
|
|
||||||
|
Global bool
|
||||||
|
}
|
||||||
|
|
||||||
type ServiceDumpRequest struct {
|
type ServiceDumpRequest struct {
|
||||||
Datacenter string
|
Datacenter string
|
||||||
ServiceKind ServiceKind
|
ServiceKind ServiceKind
|
||||||
|
@ -2240,6 +2246,21 @@ type IndexedServices struct {
|
||||||
QueryMeta
|
QueryMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Usage struct {
|
||||||
|
Usage map[string]ServiceUsage
|
||||||
|
|
||||||
|
QueryMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceUsage contains all of the usage data related to services
|
||||||
|
type ServiceUsage struct {
|
||||||
|
Services int
|
||||||
|
ServiceInstances int
|
||||||
|
ConnectServiceInstances map[string]int
|
||||||
|
BillableServiceInstances int
|
||||||
|
EnterpriseServiceUsage
|
||||||
|
}
|
||||||
|
|
||||||
// PeeredServiceName is a basic tuple of ServiceName and peer
|
// PeeredServiceName is a basic tuple of ServiceName and peer
|
||||||
type PeeredServiceName struct {
|
type PeeredServiceName struct {
|
||||||
ServiceName ServiceName
|
ServiceName ServiceName
|
||||||
|
|
|
@ -169,3 +169,5 @@ func (t *Intention) HasWildcardDestination() bool {
|
||||||
func (s *ServiceNode) NodeIdentity() Identity {
|
func (s *ServiceNode) NodeIdentity() Identity {
|
||||||
return Identity{ID: s.Node}
|
return Identity{ID: s.Node}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EnterpriseServiceUsage struct{}
|
||||||
|
|
|
@ -206,6 +206,10 @@ type QueryOptions struct {
|
||||||
// This can be used to ensure a full service definition is returned in the response
|
// This can be used to ensure a full service definition is returned in the response
|
||||||
// especially when the service might not be written into the catalog that way.
|
// especially when the service might not be written into the catalog that way.
|
||||||
MergeCentralConfig bool
|
MergeCentralConfig bool
|
||||||
|
|
||||||
|
// Global is used to request information from all datacenters. Currently only
|
||||||
|
// used for operator usage requests.
|
||||||
|
Global bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *QueryOptions) Context() context.Context {
|
func (o *QueryOptions) Context() context.Context {
|
||||||
|
@ -895,6 +899,9 @@ func (r *request) setQueryOptions(q *QueryOptions) {
|
||||||
if q.MergeCentralConfig {
|
if q.MergeCentralConfig {
|
||||||
r.params.Set("merge-central-config", "")
|
r.params.Set("merge-central-config", "")
|
||||||
}
|
}
|
||||||
|
if q.Global {
|
||||||
|
r.params.Set("global", "")
|
||||||
|
}
|
||||||
|
|
||||||
r.ctx = q.ctx
|
r.ctx = q.ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
type Usage struct {
|
||||||
|
// Usage is a map of datacenter -> usage information
|
||||||
|
Usage map[string]ServiceUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceUsage contains information about the number of services and service instances for a datacenter.
|
||||||
|
type ServiceUsage struct {
|
||||||
|
Services int
|
||||||
|
ServiceInstances int
|
||||||
|
ConnectServiceInstances map[string]int
|
||||||
|
|
||||||
|
// Billable services are of "typical" service kind (i.e. non-connect or connect-native),
|
||||||
|
// excluding the "consul" service.
|
||||||
|
BillableServiceInstances int
|
||||||
|
|
||||||
|
// A map of partition+namespace to number of unique services registered in that namespace
|
||||||
|
PartitionNamespaceServices map[string]map[string]int
|
||||||
|
|
||||||
|
// A map of partition+namespace to number of service instances registered in that namespace
|
||||||
|
PartitionNamespaceServiceInstances map[string]map[string]int
|
||||||
|
|
||||||
|
// A map of partition+namespace+kind to number of service-mesh instances registered in that namespace
|
||||||
|
PartitionNamespaceConnectServiceInstances map[string]map[string]map[string]int
|
||||||
|
|
||||||
|
// A map of partition+namespace to number of billable instances registered in that namespace
|
||||||
|
PartitionNamespaceBillableServiceInstances map[string]map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage is used to query for usage information in the given datacenter.
|
||||||
|
func (op *Operator) Usage(q *QueryOptions) (*Usage, *QueryMeta, error) {
|
||||||
|
r := op.c.newRequest("GET", "/v1/operator/usage")
|
||||||
|
r.setQueryOptions(q)
|
||||||
|
rtt, resp, err := op.c.doRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer closeResponseBody(resp)
|
||||||
|
if err := requireOK(resp); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qm := &QueryMeta{}
|
||||||
|
parseQueryMeta(resp, qm)
|
||||||
|
qm.RequestTime = rtt
|
||||||
|
|
||||||
|
var out *Usage
|
||||||
|
if err := decodeBody(resp, &out); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return out, qm, nil
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPI_OperatorUsage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c, s := makeClient(t)
|
||||||
|
defer s.Stop()
|
||||||
|
s.WaitForSerfCheck(t)
|
||||||
|
|
||||||
|
registerService := func(svc *AgentService) {
|
||||||
|
reg := &CatalogRegistration{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foobar",
|
||||||
|
Address: "192.168.10.10",
|
||||||
|
Service: svc,
|
||||||
|
}
|
||||||
|
if _, err := c.Catalog().Register(reg, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerService(&AgentService{
|
||||||
|
ID: "redis1",
|
||||||
|
Service: "redis",
|
||||||
|
Port: 8000,
|
||||||
|
})
|
||||||
|
registerService(&AgentService{
|
||||||
|
ID: "redis2",
|
||||||
|
Service: "redis",
|
||||||
|
Port: 8001,
|
||||||
|
})
|
||||||
|
registerService(&AgentService{
|
||||||
|
Kind: ServiceKindConnectProxy,
|
||||||
|
ID: "proxy1",
|
||||||
|
Service: "proxy",
|
||||||
|
Port: 9000,
|
||||||
|
Proxy: &AgentServiceConnectProxyConfig{DestinationServiceName: "foo"},
|
||||||
|
})
|
||||||
|
registerService(&AgentService{
|
||||||
|
ID: "web-native",
|
||||||
|
Service: "web",
|
||||||
|
Port: 8002,
|
||||||
|
Connect: &AgentServiceConnect{Native: true},
|
||||||
|
})
|
||||||
|
|
||||||
|
usage, _, err := c.Operator().Usage(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, usage.Usage, "dc1")
|
||||||
|
require.Equal(t, 4, usage.Usage["dc1"].Services)
|
||||||
|
require.Equal(t, 5, usage.Usage["dc1"].ServiceInstances)
|
||||||
|
require.Equal(t, map[string]int{
|
||||||
|
"connect-native": 1,
|
||||||
|
"connect-proxy": 1,
|
||||||
|
"ingress-gateway": 0,
|
||||||
|
"mesh-gateway": 0,
|
||||||
|
"terminating-gateway": 0,
|
||||||
|
}, usage.Usage["dc1"].ConnectServiceInstances)
|
||||||
|
require.Equal(t, 3, usage.Usage["dc1"].BillableServiceInstances)
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/command/flags"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(ui cli.Ui) *cmd {
|
||||||
|
c := &cmd{UI: ui}
|
||||||
|
c.init()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmd struct {
|
||||||
|
UI cli.Ui
|
||||||
|
flags *flag.FlagSet
|
||||||
|
http *flags.HTTPFlags
|
||||||
|
help string
|
||||||
|
|
||||||
|
// flags
|
||||||
|
onlyBillable bool
|
||||||
|
onlyConnect bool
|
||||||
|
allDatacenters bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cmd) init() {
|
||||||
|
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
|
c.flags.BoolVar(&c.onlyBillable, "billable", false, "Display only billable service info.")
|
||||||
|
c.flags.BoolVar(&c.onlyConnect, "connect", false, "Display only Connect service info.")
|
||||||
|
c.flags.BoolVar(&c.allDatacenters, "all-datacenters", false, "Display service counts from "+
|
||||||
|
"all datacenters.")
|
||||||
|
|
||||||
|
c.http = &flags.HTTPFlags{}
|
||||||
|
flags.Merge(c.flags, c.http.ClientFlags())
|
||||||
|
flags.Merge(c.flags, c.http.ServerFlags())
|
||||||
|
c.help = flags.Usage(help, c.flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cmd) Run(args []string) int {
|
||||||
|
if err := c.flags.Parse(args); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(c.flags.Args()); l > 0 {
|
||||||
|
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", l))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and test the HTTP client
|
||||||
|
client, err := c.http.APIClient()
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
billableTotal := 0
|
||||||
|
var datacenterBillableTotals []string
|
||||||
|
usage, _, err := client.Operator().Usage(&api.QueryOptions{Global: c.allDatacenters})
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error fetching usage information: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
for dc, usage := range usage.Usage {
|
||||||
|
billableTotal += usage.BillableServiceInstances
|
||||||
|
datacenterBillableTotals = append(datacenterBillableTotals,
|
||||||
|
fmt.Sprintf("%s Billable Service Instances: %d", dc, usage.BillableServiceInstances))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output billable service counts
|
||||||
|
if !c.onlyConnect {
|
||||||
|
c.UI.Output(fmt.Sprintf("Billable Service Instances Total: %d", billableTotal))
|
||||||
|
sort.Strings(datacenterBillableTotals)
|
||||||
|
for _, datacenterTotal := range datacenterBillableTotals {
|
||||||
|
c.UI.Output(datacenterTotal)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.UI.Output("\nBillable Services")
|
||||||
|
billableOutput, err := formatServiceCounts(usage.Usage, true, c.allDatacenters)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
c.UI.Output(billableOutput + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output Connect service counts
|
||||||
|
if !c.onlyBillable {
|
||||||
|
c.UI.Output("Connect Services")
|
||||||
|
connectOutput, err := formatServiceCounts(usage.Usage, false, c.allDatacenters)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
c.UI.Output(connectOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatServiceCounts(usageStats map[string]api.ServiceUsage, billable, showDatacenter bool) (string, error) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
tw := tabwriter.NewWriter(&output, 0, 2, 6, ' ', 0)
|
||||||
|
var serviceCounts []serviceCount
|
||||||
|
|
||||||
|
for datacenter, usage := range usageStats {
|
||||||
|
if billable {
|
||||||
|
serviceCounts = append(serviceCounts, getBillableInstanceCounts(usage, datacenter)...)
|
||||||
|
} else {
|
||||||
|
serviceCounts = append(serviceCounts, getConnectInstanceCounts(usage, datacenter)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortServiceCounts(serviceCounts)
|
||||||
|
|
||||||
|
if showDatacenter {
|
||||||
|
fmt.Fprintf(tw, "Datacenter\t")
|
||||||
|
}
|
||||||
|
if showPartitionNamespace {
|
||||||
|
fmt.Fprintf(tw, "Partition\tNamespace\t")
|
||||||
|
}
|
||||||
|
if !billable {
|
||||||
|
fmt.Fprintf(tw, "Type\t")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(tw, "Services\t")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tw, "Service instances\n")
|
||||||
|
|
||||||
|
serviceTotal := 0
|
||||||
|
instanceTotal := 0
|
||||||
|
for _, c := range serviceCounts {
|
||||||
|
if showDatacenter {
|
||||||
|
fmt.Fprintf(tw, "%s\t", c.datacenter)
|
||||||
|
}
|
||||||
|
if showPartitionNamespace {
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\t", c.partition, c.namespace)
|
||||||
|
}
|
||||||
|
if !billable {
|
||||||
|
fmt.Fprintf(tw, "%s\t", c.serviceType)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(tw, "%d\t", c.services)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tw, "%d\n", c.instanceCount)
|
||||||
|
|
||||||
|
serviceTotal += c.services
|
||||||
|
instanceTotal += c.instanceCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show total counts if there's multiple rows because of datacenter or partition/ns view
|
||||||
|
if showDatacenter || showPartitionNamespace {
|
||||||
|
if showDatacenter {
|
||||||
|
fmt.Fprint(tw, "\t")
|
||||||
|
}
|
||||||
|
if showPartitionNamespace {
|
||||||
|
fmt.Fprint(tw, "\t\t")
|
||||||
|
}
|
||||||
|
fmt.Fprint(tw, "\t\n")
|
||||||
|
fmt.Fprintf(tw, "Total")
|
||||||
|
if showPartitionNamespace {
|
||||||
|
fmt.Fprint(tw, "\t")
|
||||||
|
if showDatacenter {
|
||||||
|
fmt.Fprint(tw, "\t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if billable {
|
||||||
|
fmt.Fprintf(tw, "\t%d\t%d\n", serviceTotal, instanceTotal)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(tw, "\t\t%d\n", instanceTotal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tw.Flush(); err != nil {
|
||||||
|
return "", fmt.Errorf("Error flushing tabwriter: %s", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(output.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceCount struct {
|
||||||
|
datacenter string
|
||||||
|
partition string
|
||||||
|
namespace string
|
||||||
|
serviceType string
|
||||||
|
instanceCount int
|
||||||
|
services int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort entries by datacenter > partition > namespace
|
||||||
|
func sortServiceCounts(counts []serviceCount) {
|
||||||
|
sort.Slice(counts, func(i, j int) bool {
|
||||||
|
if counts[i].datacenter != counts[j].datacenter {
|
||||||
|
return counts[i].datacenter < counts[j].datacenter
|
||||||
|
}
|
||||||
|
if counts[i].partition != counts[j].partition {
|
||||||
|
return counts[i].partition < counts[j].partition
|
||||||
|
}
|
||||||
|
if counts[i].namespace != counts[j].namespace {
|
||||||
|
return counts[i].namespace < counts[j].namespace
|
||||||
|
}
|
||||||
|
return counts[i].serviceType < counts[j].serviceType
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cmd) Synopsis() string {
|
||||||
|
return synopsis
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cmd) Help() string {
|
||||||
|
return c.help
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
synopsis = "Display service instance usage information"
|
||||||
|
help = `
|
||||||
|
Usage: consul usage instances [options]
|
||||||
|
|
||||||
|
Retrieves usage information about the number of services registered in a given
|
||||||
|
datacenter. By default, the datacenter of the local agent is queried.
|
||||||
|
|
||||||
|
To retrieve the service usage data:
|
||||||
|
|
||||||
|
$ consul usage instances
|
||||||
|
|
||||||
|
To show only billable service instance counts:
|
||||||
|
|
||||||
|
$ consul usage instances -billable
|
||||||
|
|
||||||
|
To show only connect service instance counts:
|
||||||
|
|
||||||
|
$ consul usage instances -connect
|
||||||
|
|
||||||
|
For a full list of options and examples, please see the Consul documentation.
|
||||||
|
`
|
||||||
|
)
|
|
@ -0,0 +1,39 @@
|
||||||
|
//go:build !consulent
|
||||||
|
// +build !consulent
|
||||||
|
|
||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const showPartitionNamespace = false
|
||||||
|
|
||||||
|
func getBillableInstanceCounts(usage api.ServiceUsage, datacenter string) []serviceCount {
|
||||||
|
return []serviceCount{
|
||||||
|
{
|
||||||
|
datacenter: datacenter,
|
||||||
|
partition: acl.DefaultPartitionName,
|
||||||
|
namespace: acl.DefaultNamespaceName,
|
||||||
|
instanceCount: usage.BillableServiceInstances,
|
||||||
|
services: usage.Services,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConnectInstanceCounts(usage api.ServiceUsage, datacenter string) []serviceCount {
|
||||||
|
var counts []serviceCount
|
||||||
|
|
||||||
|
for serviceType, instanceCount := range usage.ConnectServiceInstances {
|
||||||
|
counts = append(counts, serviceCount{
|
||||||
|
datacenter: datacenter,
|
||||||
|
partition: acl.DefaultPartitionName,
|
||||||
|
namespace: acl.DefaultNamespaceName,
|
||||||
|
serviceType: serviceType,
|
||||||
|
instanceCount: instanceCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
//go:build !consulent
|
||||||
|
// +build !consulent
|
||||||
|
|
||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUsageInstances_formatServiceCounts(t *testing.T) {
|
||||||
|
usageBasic := map[string]api.ServiceUsage{
|
||||||
|
"dc1": {
|
||||||
|
Services: 10,
|
||||||
|
ServiceInstances: 35,
|
||||||
|
ConnectServiceInstances: map[string]int{
|
||||||
|
"connect-native": 1,
|
||||||
|
"connect-proxy": 3,
|
||||||
|
"ingress-gateway": 4,
|
||||||
|
"mesh-gateway": 2,
|
||||||
|
"terminating-gateway": 5,
|
||||||
|
},
|
||||||
|
BillableServiceInstances: 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
usageMultiDC := map[string]api.ServiceUsage{
|
||||||
|
"dc1": {
|
||||||
|
Services: 10,
|
||||||
|
ServiceInstances: 35,
|
||||||
|
ConnectServiceInstances: map[string]int{
|
||||||
|
"connect-native": 1,
|
||||||
|
"connect-proxy": 3,
|
||||||
|
"ingress-gateway": 4,
|
||||||
|
"mesh-gateway": 2,
|
||||||
|
"terminating-gateway": 5,
|
||||||
|
},
|
||||||
|
BillableServiceInstances: 20,
|
||||||
|
},
|
||||||
|
"dc2": {
|
||||||
|
Services: 23,
|
||||||
|
ServiceInstances: 11,
|
||||||
|
ConnectServiceInstances: map[string]int{
|
||||||
|
"connect-native": 9,
|
||||||
|
"connect-proxy": 8,
|
||||||
|
"ingress-gateway": 7,
|
||||||
|
"mesh-gateway": 6,
|
||||||
|
"terminating-gateway": 0,
|
||||||
|
},
|
||||||
|
BillableServiceInstances: 33,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
usageStats map[string]api.ServiceUsage
|
||||||
|
showDatacenter bool
|
||||||
|
expectedBillable string
|
||||||
|
expectedConnect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
usageStats: usageBasic,
|
||||||
|
expectedBillable: `
|
||||||
|
Services Service instances
|
||||||
|
10 20`,
|
||||||
|
expectedConnect: `
|
||||||
|
Type Service instances
|
||||||
|
connect-native 1
|
||||||
|
connect-proxy 3
|
||||||
|
ingress-gateway 4
|
||||||
|
mesh-gateway 2
|
||||||
|
terminating-gateway 5`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi-datacenter",
|
||||||
|
usageStats: usageMultiDC,
|
||||||
|
showDatacenter: true,
|
||||||
|
expectedBillable: `
|
||||||
|
Datacenter Services Service instances
|
||||||
|
dc1 10 20
|
||||||
|
dc2 23 33
|
||||||
|
|
||||||
|
Total 33 53`,
|
||||||
|
expectedConnect: `
|
||||||
|
Datacenter Type Service instances
|
||||||
|
dc1 connect-native 1
|
||||||
|
dc1 connect-proxy 3
|
||||||
|
dc1 ingress-gateway 4
|
||||||
|
dc1 mesh-gateway 2
|
||||||
|
dc1 terminating-gateway 5
|
||||||
|
dc2 connect-native 9
|
||||||
|
dc2 connect-proxy 8
|
||||||
|
dc2 ingress-gateway 7
|
||||||
|
dc2 mesh-gateway 6
|
||||||
|
dc2 terminating-gateway 0
|
||||||
|
|
||||||
|
Total 45`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
billableOutput, err := formatServiceCounts(tc.usageStats, true, tc.showDatacenter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, strings.TrimSpace(tc.expectedBillable), billableOutput)
|
||||||
|
|
||||||
|
connectOutput, err := formatServiceCounts(tc.usageStats, false, tc.showDatacenter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, strings.TrimSpace(tc.expectedConnect), connectOutput)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/testrpc"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUsageInstancesCommand(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("too slow for testing.Short")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
a := agent.NewTestAgent(t, ``)
|
||||||
|
defer a.Shutdown()
|
||||||
|
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
|
||||||
|
|
||||||
|
// Add another 2 services for testing
|
||||||
|
if err := a.Client().Agent().ServiceRegister(&api.AgentServiceRegistration{
|
||||||
|
Name: "testing",
|
||||||
|
Port: 8080,
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := a.Client().Agent().ServiceRegister(&api.AgentServiceRegistration{
|
||||||
|
Name: "testing2",
|
||||||
|
Port: 8081,
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
c := New(ui)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
require.Contains(t, output, "Billable Service Instances Total: 2")
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package usage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/consul/command/flags"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *cmd {
|
||||||
|
return &cmd{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmd struct{}
|
||||||
|
|
||||||
|
func (c *cmd) Run(args []string) int {
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cmd) Synopsis() string {
|
||||||
|
return synopsis
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cmd) Help() string {
|
||||||
|
return flags.Usage(help, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
const synopsis = "Provides cluster-level usage information"
|
||||||
|
const help = `
|
||||||
|
Usage: consul operator usage <subcommand> [options] [args]
|
||||||
|
|
||||||
|
This command has subcommands for displaying usage information. The subcommands
|
||||||
|
default to working with services registered with the local datacenter.
|
||||||
|
|
||||||
|
For more examples, ask for subcommand help or view the documentation.
|
||||||
|
`
|
|
@ -96,6 +96,8 @@ import (
|
||||||
operraftlist "github.com/hashicorp/consul/command/operator/raft/listpeers"
|
operraftlist "github.com/hashicorp/consul/command/operator/raft/listpeers"
|
||||||
operraftremove "github.com/hashicorp/consul/command/operator/raft/removepeer"
|
operraftremove "github.com/hashicorp/consul/command/operator/raft/removepeer"
|
||||||
"github.com/hashicorp/consul/command/operator/raft/transferleader"
|
"github.com/hashicorp/consul/command/operator/raft/transferleader"
|
||||||
|
"github.com/hashicorp/consul/command/operator/usage"
|
||||||
|
"github.com/hashicorp/consul/command/operator/usage/instances"
|
||||||
"github.com/hashicorp/consul/command/peering"
|
"github.com/hashicorp/consul/command/peering"
|
||||||
peerdelete "github.com/hashicorp/consul/command/peering/delete"
|
peerdelete "github.com/hashicorp/consul/command/peering/delete"
|
||||||
peerestablish "github.com/hashicorp/consul/command/peering/establish"
|
peerestablish "github.com/hashicorp/consul/command/peering/establish"
|
||||||
|
@ -223,6 +225,8 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory {
|
||||||
entry{"operator raft list-peers", func(ui cli.Ui) (cli.Command, error) { return operraftlist.New(ui), nil }},
|
entry{"operator raft list-peers", func(ui cli.Ui) (cli.Command, error) { return operraftlist.New(ui), nil }},
|
||||||
entry{"operator raft remove-peer", func(ui cli.Ui) (cli.Command, error) { return operraftremove.New(ui), nil }},
|
entry{"operator raft remove-peer", func(ui cli.Ui) (cli.Command, error) { return operraftremove.New(ui), nil }},
|
||||||
entry{"operator raft transfer-leader", func(ui cli.Ui) (cli.Command, error) { return transferleader.New(ui), nil }},
|
entry{"operator raft transfer-leader", func(ui cli.Ui) (cli.Command, error) { return transferleader.New(ui), nil }},
|
||||||
|
entry{"operator usage", func(ui cli.Ui) (cli.Command, error) { return usage.New(), nil }},
|
||||||
|
entry{"operator usage instances", func(ui cli.Ui) (cli.Command, error) { return instances.New(ui), nil }},
|
||||||
entry{"peering", func(cli.Ui) (cli.Command, error) { return peering.New(), nil }},
|
entry{"peering", func(cli.Ui) (cli.Command, error) { return peering.New(), nil }},
|
||||||
entry{"peering delete", func(ui cli.Ui) (cli.Command, error) { return peerdelete.New(ui), nil }},
|
entry{"peering delete", func(ui cli.Ui) (cli.Command, error) { return peerdelete.New(ui), nil }},
|
||||||
entry{"peering generate-token", func(ui cli.Ui) (cli.Command, error) { return peergenerate.New(ui), nil }},
|
entry{"peering generate-token", func(ui cli.Ui) (cli.Command, error) { return peergenerate.New(ui), nil }},
|
||||||
|
|
Loading…
Reference in New Issue