|
|
|
@@ -0,0 +1,111 @@
|
|
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/mrhid6/vantage/server/internal/db"
|
|
|
|
|
"github.com/mrhid6/vantage/shared/license"
|
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// LimitError is returned when a licence cap would be exceeded. The API maps it
|
|
|
|
|
// to 403 with a machine-readable body.
|
|
|
|
|
type LimitError struct {
|
|
|
|
|
Limit string
|
|
|
|
|
Current int
|
|
|
|
|
Max int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *LimitError) Error() string {
|
|
|
|
|
return fmt.Sprintf("licence limit reached: %s (%d of %d)", e.Limit, e.Current, e.Max)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func limitCtx() (context.Context, context.CancelFunc) {
|
|
|
|
|
return context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CheckServerLimit refuses a new server when the instance is at its cap.
|
|
|
|
|
//
|
|
|
|
|
// Counts live rows only. An instance already over its cap keeps every server it
|
|
|
|
|
// has — nothing is truncated — it simply cannot add another.
|
|
|
|
|
func CheckServerLimit(instanceID string) error {
|
|
|
|
|
st := GetLicenseState(instanceID)
|
|
|
|
|
ctx, cancel := limitCtx()
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
n, err := db.Col("servers").CountDocuments(ctx, bson.M{"instance_id": instanceID})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !license.WithinLimit(int(n), st.Limits.MaxServers) {
|
|
|
|
|
return &LimitError{Limit: "max_servers", Current: int(n), Max: st.Limits.MaxServers}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CheckSecretGroupLimit refuses a NEW group at the cap. Writing to a group that
|
|
|
|
|
// already exists is always allowed, so a capped customer can still rotate the
|
|
|
|
|
// secrets they have.
|
|
|
|
|
func CheckSecretGroupLimit(instanceID, group string) error {
|
|
|
|
|
st := GetLicenseState(instanceID)
|
|
|
|
|
ctx, cancel := limitCtx()
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
existing, err := db.Col("secrets").CountDocuments(ctx,
|
|
|
|
|
bson.M{"instance_id": instanceID, "group": group})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if existing > 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var groups []string
|
|
|
|
|
if err := db.Col("secrets").Distinct(ctx, "group",
|
|
|
|
|
bson.M{"instance_id": instanceID}).Decode(&groups); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !license.WithinLimit(len(groups), st.Limits.MaxSecretGroups) {
|
|
|
|
|
return &LimitError{Limit: "max_secret_groups", Current: len(groups), Max: st.Limits.MaxSecretGroups}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CheckChannelLimit refuses a new notification channel at the cap.
|
|
|
|
|
func CheckChannelLimit(instanceID string) error {
|
|
|
|
|
st := GetLicenseState(instanceID)
|
|
|
|
|
ctx, cancel := limitCtx()
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
n, err := db.Col("notification_channels").CountDocuments(ctx, bson.M{"instance_id": instanceID})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !license.WithinLimit(int(n), st.Limits.MaxChannels) {
|
|
|
|
|
return &LimitError{Limit: "max_channels", Current: int(n), Max: st.Limits.MaxChannels}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LicenseUsage reports current counts, so the UI can say "12 of 3 servers"
|
|
|
|
|
// honestly when an instance is over its cap rather than pretending.
|
|
|
|
|
func LicenseUsage(instanceID string) (servers, secretGroups, channels int) {
|
|
|
|
|
ctx, cancel := limitCtx()
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
if n, err := db.Col("servers").CountDocuments(ctx, bson.M{"instance_id": instanceID}); err == nil {
|
|
|
|
|
servers = int(n)
|
|
|
|
|
}
|
|
|
|
|
var groups []string
|
|
|
|
|
if err := db.Col("secrets").Distinct(ctx, "group",
|
|
|
|
|
bson.M{"instance_id": instanceID}).Decode(&groups); err == nil {
|
|
|
|
|
secretGroups = len(groups)
|
|
|
|
|
}
|
|
|
|
|
if n, err := db.Col("notification_channels").CountDocuments(ctx,
|
|
|
|
|
bson.M{"instance_id": instanceID}); err == nil {
|
|
|
|
|
channels = int(n)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|