metering

package
v0.2.49 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2026 License: Apache-2.0 Imports: 38 Imported by: 0

Documentation

Overview

Package metering is how the hub finds the contrib-metering deployment and lets the admin area read it.

Metering is a standalone deployment, not a provider: nothing registers it with the hub. What the hub and metering share is kcp, so discovery goes through kcp. Metering's init publishes, in the workspace it owns (--metering-workspace), a ConfigMap "default/metering-endpoints" with the URL of its controller (portal at /, API under /api/, /readyz) and a Secret "default/metering-admin" with its operator token. The hub, as kcp-admin, polls for them: present means installed. The admin REST then serves GET /api/admin/metering (installed, version, ready) and GET /api/admin/metering/{collection}, a read-only proxy of the metering API with the token injected, so the portal's admin pages render metering data behind the hub's own admin gate and never see that token.

Index

Constants

View Source
const (
	// PlanSourceLabel records who owns an
	// Entitlement's spec.plan. The hub writes PlanSourceOperator while an
	// admin's Organization.spec.plan stands, and clears it with the assignment.
	// Any external subscription reconciler must honor this label before writing
	// spec.plan; this is an integration contract, not a contrib-metering feature.
	// See docs/organizations.md#metering-deployment-compatibility.
	// Use our domain: kcp reserves *.kcp.io metadata and rejects writes to it.
	PlanSourceLabel = "metering.railgrid.ai/plan-source"
	// PlanSourceOperator is the PlanSourceLabel value for a plan set by hand.
	PlanSourceOperator = "operator"
	// PlanArchivedAnnotation marks a Plan as retained for existing assignments
	// but unavailable for edits or new assignments. It lives in metadata so the
	// metering Plan schema does not need to change.
	PlanArchivedAnnotation = "metering.railgrid.ai/archived"
)
View Source
const (
	// Reporter is the hub's name on the MembershipReports it writes. Each
	// reporter owns its own report per account; metering unions them.
	Reporter = "railgrid-hub"
	// FieldManager is the hub's server-side-apply manager in the metering
	// workspace.
	FieldManager = "railgrid-hub"

	// LabelOrganization marks the hub-owned metering objects of an
	// Organization with its name.
	LabelOrganization = "railgrid.ai/organization"
	// LabelPlanFromSpec marks an Entitlement whose plan the hub wrote from
	// Organization.spec.plan. Clearing spec.plan later puts such an
	// Entitlement back on the default plan; an Entitlement without the label
	// carries a plan an admin set directly and is left alone.
	LabelPlanFromSpec = "railgrid.ai/plan-from-spec"

	// WorkspacesChangedAnnotation on an Organization records when the hub
	// last created or deleted one of its child workspaces. Any replica
	// writes it (WorkspaceNotifier.Notify); the organization reconciler,
	// which runs on the leader only, sees the update through its watch on
	// Organizations and re-lists the membership report. The value is the
	// time of the change, so every change is a distinct update.
	WorkspacesChangedAnnotation = "metering.railgrid.ai/workspaces-changed-at"
)
View Source
const (
	// ProviderBindingName is the APIBinding every platform provider workspace
	// carries to metering's provider-facing export, which is also the
	// export's name: it serves UsageRecord and MeteringConfig, the two kinds
	// a provider writes to report usage.
	ProviderBindingName = "metering-provider"
	// ProviderBindingLabel marks the bindings this reconciler owns.
	ProviderBindingLabel = "metering.railgrid.ai/managed-by"
)

Variables

This section is empty.

Functions

func AssignPlan added in v0.2.42

func AssignPlan(discovery Installed, store dynamic.Interface, logger klog.Logger) func(ctx context.Context, org, plan string) error

AssignPlan updates an existing organization's entitlement as part of the admin assignment request. The Organization remains the source of truth; the reconciler repairs interruptions and creates missing entitlements. Overrides and unrelated labels are preserved, including on conflict retries.

func EntitlementPlan added in v0.2.34

func EntitlementPlan(discovery Installed, store dynamic.Interface, logger klog.Logger) func(ctx context.Context, org string) (plan string, ok bool, err error)

EntitlementPlan returns a lookup for the plan an organization is on according to its metering Entitlement, the one thing that decides what the organization may use. It is preferred over Organization.spec.plan, which is only an admin's assignment: a plan set on the Entitlement directly, or by a subscription, never reaches that field. ok is false when metering has nothing to say (not installed, no Entitlement yet, or the read failed), and the caller falls back to spec.plan.

func IsPlanArchived added in v0.2.42

func IsPlanArchived(plan *unstructured.Unstructured) bool

IsPlanArchived reports whether plan is retained for existing assignments only. The marker is an annotation rather than a Plan spec field so no external metering schema change is needed.

func MeteringClient

func MeteringClient(base *rest.Config, workspace string) (dynamic.Interface, error)

MeteringClient returns a dynamic client scoped to the metering workspace.

func PlanExists added in v0.2.32

func PlanExists(discovery Installed, store dynamic.Interface) func(ctx context.Context, name string) (bool, error)

PlanExists returns a lookup that answers whether a metering Plan exists and remains assignable, for callers that record a plan name on a hub object (Organization.spec.plan) and must not accept a typo or an archived plan. It errors while metering is not installed: without the store there is nothing to validate against, and an Organization cannot be detached onto a plan nobody serves.

func PlanIsArchived added in v0.2.42

func PlanIsArchived(store dynamic.Interface) func(ctx context.Context, name string) (bool, error)

PlanIsArchived returns a secondary lookup for callers that need to tell an archived retained plan from a missing plan after PlanExists returned false.

func ResetPlan added in v0.2.34

func ResetPlan(discovery Installed, store dynamic.Interface, defaultPlan string, logger klog.Logger) func(ctx context.Context, org string) error

ResetPlan returns the action behind an explicit "back to the default plan" on the admin plan route. Clearing Organization.spec.plan alone only moves an Entitlement whose plan the hub wrote (LabelPlanFromSpec): one an admin edited directly keeps its plan, by design, so a clear of an already-empty field would change nothing. The reset puts the Entitlement on defaultPlan and drops the hub's labels, whoever set the plan; a missing Entitlement is fine (the reconciler creates it on the default plan). Overrides are untouched.

func SetupOrganizationReconciler

func SetupOrganizationReconciler(mgr manager.Manager, opts OrganizationOptions) error

SetupOrganizationReconciler registers the reconciler with the hub's organization manager (the users workspace, where Organizations live).

func SetupProviderReconciler

func SetupProviderReconciler(mgr mcmanager.Manager, opts ProviderOptions) error

SetupProviderReconciler registers the reconciler with the hub's admin multicluster manager, where Provider objects live.

func ValidateDefaultPlan added in v0.2.42

func ValidateDefaultPlan(store dynamic.Interface, defaultPlan string) func(ctx context.Context) error

ValidateDefaultPlan returns a live check for the default plan configured on this hub. Admin plan-reset routes use it before clearing Organization intent; ResetPlan repeats the check before writing the Entitlement.

func WorkspaceClients

func WorkspaceClients(base *rest.Config) func(path string) (dynamic.Interface, error)

WorkspaceClients returns a Workspaces func over base, caching one dynamic client per path.

Types

type Billing added in v0.2.34

type Billing struct {
	// contains filtered or unexported fields
}

Billing holds what the seam needs: discovery for the deployment's URL and token, and the kcp-admin client into the metering workspace for reads.

func NewBilling added in v0.2.34

func NewBilling(d Installed, store dynamic.Interface, logger klog.Logger) *Billing

NewBilling builds the seam over discovery (the hub's Discovery; tests substitute a constant) and the kcp-admin client into the metering workspace.

func (*Billing) Checkout added in v0.2.34

func (b *Billing) Checkout(ctx context.Context, org, plan, successURL, cancelURL string) (string, error)

Checkout asks the deployment for a Checkout URL that puts org on plan. An organization whose plan a platform administrator assigned (PlanSourceOperator on its Entitlement) is refused. A compatible external subscription integration must preserve that plan until the administrator releases it.

func (*Billing) CheckoutQuoted added in v0.2.43

func (b *Billing) CheckoutQuoted(ctx context.Context, org, plan, expectedPrice, successURL, cancelURL string) (string, error)

CheckoutQuoted preserves the price the member reviewed, including across a concurrent catalog update. The deployment also verifies this mapping.

func (*Billing) ForwardWebhook added in v0.2.34

func (b *Billing) ForwardWebhook(w http.ResponseWriter, r *http.Request)

ForwardWebhook relays the provider's delivery to the deployment, body and signature header unchanged, and returns its answer. No hub-side authentication: the deployment verifies the provider's signature, which is the only credential a webhook carries. The route is registered on every hub, so a nil Billing (metering not configured) answers 503 and the provider retries later rather than taking a generic answer for a receipt.

func (*Billing) Portal added in v0.2.34

func (b *Billing) Portal(ctx context.Context, org, returnURL string) (string, error)

Portal asks the deployment for a Customer Portal URL for org.

func (*Billing) Summary added in v0.2.34

func (b *Billing) Summary(ctx context.Context, org string) (OrgBilling, error)

Summary composes OrgBilling for org.

type BillingError added in v0.2.34

type BillingError struct {
	Status  int
	Message string
}

BillingError carries the deployment's answer (or the seam's refusal) to the REST caller with its status.

func (*BillingError) Error added in v0.2.34

func (e *BillingError) Error() string

type Discovery

type Discovery struct {
	// contains filtered or unexported fields
}

Discovery polls the metering workspace for the published objects.

func NewDiscovery

func NewDiscovery(base *rest.Config, workspace string, logger klog.Logger) (*Discovery, error)

NewDiscovery builds a Discovery from the hub's kcp config (any cluster path; it is re-scoped to workspace).

func (*Discovery) Current

func (d *Discovery) Current() (Endpoints, bool)

Current returns the last discovery result; false when metering is not installed (or not yet looked for).

func (*Discovery) Installed added in v0.2.34

func (d *Discovery) Installed() (installed, known bool)

Installed says whether a deployment is discovered and whether that is a definite answer: known is false until the first poll has reached kcp. A gate that must fail closed treats "not known" as installed.

func (*Discovery) Refresh

func (d *Discovery) Refresh(ctx context.Context) (Endpoints, bool)

Refresh re-reads the published objects now (the admin area's manual refresh) and returns the result.

func (*Discovery) Run

func (d *Discovery) Run(ctx context.Context)

Run polls until ctx ends. Never fatal: a missing workspace means "not installed" until it is, and a workspace that cannot be read right now keeps the last answer (see refresh).

type Endpoints

type Endpoints struct {
	PortalURL  string `json:"portalURL"`
	WebhookURL string `json:"webhookURL,omitempty"`
	AuditURL   string `json:"auditURL,omitempty"`
	Version    string `json:"version,omitempty"`
	Workspace  string `json:"workspace"`
	// contains filtered or unexported fields
}

Endpoints is what the hub learned about the deployment.

type EntitlementUpdate

type EntitlementUpdate struct {
	Plan      string  `json:"plan"`
	Overrides []Limit `json:"overrides"`
}

EntitlementUpdate is the body of PUT .../entitlements/{name}.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler serves the admin-area view of metering. Mount it under the admin subrouter: the gate is the caller's.

func NewHandler

func NewHandler(d *Discovery) *Handler

NewHandler builds a Handler.

func (*Handler) Register

func (h *Handler) Register(r *mux.Router)

Register adds the routes to r (a router already rooted at /api/admin).

func (*Handler) WithWriter

func (h *Handler) WithWriter(w *Writer) *Handler

WithWriter enables the write routes on h.

type Installed

type Installed interface {
	Current() (Endpoints, bool)
}

Installed answers whether metering is discovered; Discovery implements it and tests substitute a constant.

type Limit

type Limit struct {
	Meter   string `json:"meter"`
	Shape   string `json:"shape,omitempty"`
	Enforce string `json:"enforce"`
	Max     string `json:"max,omitempty"`
}

Limit is one per-meter row, the wire shape of contrib-metering's PlanLimit.

type OrgBilling added in v0.2.34

type OrgBilling struct {
	// Installed is whether metering is discovered at all; without it there
	// are no plans, limits or usage to show.
	Installed bool `json:"installed"`
	// Enabled is whether the deployment sells plans: Checkout and the
	// Customer Portal are available.
	Enabled bool `json:"enabled"`
	// Livemode is false when the deployment runs its provider's test
	// environment.
	Livemode    bool   `json:"livemode"`
	DefaultPlan string `json:"defaultPlan,omitempty"`
	// Plan is the organization's current plan (its Entitlement's spec.plan;
	// the default plan until the Entitlement exists).
	Plan            string `json:"plan,omitempty"`
	PlanDisplayName string `json:"planDisplayName,omitempty"`
	// ManagedBy is who set the plan: "billing" (a subscription), "operator"
	// (a platform admin), empty for the default plan.
	ManagedBy string  `json:"managedBy,omitempty"`
	Limits    []Limit `json:"limits"`
	// Overrides persist across plan changes. Expose their declaration even
	// when equal to the current plan's allowance: effective limits alone do
	// not reveal whether a different plan would change the allowance.
	// The empty list confirms that no overrides apply.
	Overrides     []Limit           `json:"overrides"`
	Usage         map[string]string `json:"usage"`
	Window        *UsageWindow      `json:"window,omitempty"`
	OverSoftLimit bool              `json:"overSoftLimit"`
	OverHardLimit bool              `json:"overHardLimit"`
	// Subscription is the provider's view, when the deployment has synced
	// one.
	Subscription *OrgSubscription `json:"subscription,omitempty"`
	Plans        []PlanOffer      `json:"plans"`
}

OrgBilling is GET /api/orgs/{org}/billing: the organization's plan and what it buys, what it uses, the subscription behind it, and the plans on offer.

type OrgSubscription added in v0.2.34

type OrgSubscription struct {
	EntitledPlan      string `json:"entitledPlan,omitempty"`
	Status            string `json:"status,omitempty"`
	CurrentPeriodEnd  string `json:"currentPeriodEnd,omitempty"`
	CancelAtPeriodEnd bool   `json:"cancelAtPeriodEnd"`
	SyncedAt          string `json:"syncedAt,omitempty"`
}

OrgSubscription is Entitlement.status.billing as the portal shows it.

type OrganizationOptions

type OrganizationOptions struct {
	// Discovery gates the work: no metering, nothing is written.
	Discovery Installed
	// Metering is a client scoped to the metering workspace.
	Metering dynamic.Interface
	// Workspaces returns a client scoped to a kcp workspace path, for reading
	// an organization's Workspace objects.
	Workspaces func(path string) (dynamic.Interface, error)
	// DefaultPlan is the Plan an organization's Entitlement names while
	// Organization.spec.plan is empty.
	DefaultPlan string
	Logger      klog.Logger
}

OrganizationOptions configures the reconciler.

type OrganizationReconciler

type OrganizationReconciler struct {
	client.Client
	// contains filtered or unexported fields
}

OrganizationReconciler gives every Organization its own billing objects in the metering workspace: an Account and a MembershipReport the hub owns and keeps current, and an Entitlement whose plan follows Organization.spec.plan (the default plan while that is empty). The Entitlement is otherwise left alone: its overrides, and a plan an admin set on it directly, are never reset by a resync. When an Organization is gone, its three objects go too.

One account per organization is the design (docs/organizations.md O-5): the organization is the tenancy boundary everywhere else in the hub and so is the billing unit too. How many free organizations one person may hold is a cap the hub applies when an organization is created, not something metering attributes to a user.

func (*OrganizationReconciler) Reconcile

func (r *OrganizationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)

Reconcile is the per-Organization step.

type PlanOffer added in v0.2.34

type PlanOffer struct {
	Name        string             `json:"name"`
	DisplayName string             `json:"displayName,omitempty"`
	Sellable    bool               `json:"sellable"`
	Current     bool               `json:"current"`
	Limits      []Limit            `json:"limits"`
	Price       *SubscriptionPrice `json:"price,omitempty"`
}

PlanOffer is one plan: its limits, whether the deployment sells it, and whether the organization is on it.

type PlanUpdate

type PlanUpdate struct {
	DisplayName string  `json:"displayName"`
	Limits      []Limit `json:"limits"`
}

PlanUpdate is the body of PUT .../plans/{name}; the plan is created when it does not exist yet.

type ProviderGetter

type ProviderGetter interface {
	Get(ctx context.Context, clusterName multicluster.ClusterName, key types.NamespacedName) (*adminv1alpha1.Provider, error)
}

ProviderGetter reads one Provider from the cluster the request names; the multicluster manager implements it and tests substitute a map.

type ProviderOptions

type ProviderOptions struct {
	// Discovery is the feature gate: until metering's init has published its
	// endpoints in the metering workspace, no provider workspace is touched.
	Discovery Installed
	// Providers reads Provider objects; defaults to the manager's clients.
	Providers ProviderGetter
	// Workspaces returns a client scoped to a kcp workspace path, for the
	// APIBinding in each provider's workspace.
	Workspaces func(path string) (dynamic.Interface, error)
	Logger     klog.Logger
}

ProviderOptions configures the reconciler.

type ProviderReconciler

type ProviderReconciler struct {
	// contains filtered or unexported fields
}

ProviderReconciler binds metering's provider export into every platform provider workspace once metering is installed. The binding is what lets a provider emit UsageRecords (docs/usage-contract.md in contrib-metering): metering's controller engages any workspace that binds the export, so the binding is the whole wiring. The reconciler is additive. It never removes a binding, because kcp refuses to delete one while bound objects remain and the records in a provider workspace are its usage history.

func (*ProviderReconciler) Reconcile

Reconcile is the per-Provider step.

type SubscriptionPrice added in v0.2.43

type SubscriptionPrice struct {
	ID            string `json:"id"`
	Amount        string `json:"amount"`
	Currency      string `json:"currency"`
	Interval      string `json:"interval"`
	IntervalCount int64  `json:"intervalCount"`
	TaxBehavior   string `json:"taxBehavior,omitempty"`
}

SubscriptionPrice is supplied by the metering billing provider, never by a hub-authored price label. Amount is a decimal in major currency units.

type UsageWindow added in v0.2.34

type UsageWindow struct {
	Start string `json:"start"`
	End   string `json:"end"`
}

UsageWindow is the window the usage values cover.

type WorkspaceNotifier added in v0.2.34

type WorkspaceNotifier struct {
	// contains filtered or unexported fields
}

WorkspaceNotifier tells the organization reconciler that an organization's child workspaces changed, from any replica. The hub serves workspace creates and runs the soft-delete cascade on whichever replica holds the request or the lease, but reconciles metering membership on the leader only, so an in-process signal is lost whenever those differ. Notify instead stamps WorkspacesChangedAnnotation on the Organization; the leader's watch on Organizations turns that into a reconcile. A failed stamp is logged, not returned: the periodic re-list covers it.

The REST workspace create and the soft-delete workspace cascade call it. The organization bootstrap controller, which creates an organization's first workspace, does not: it writes the Organization's status in the same reconcile, and a stamp there would make that write conflict on every pass. Its status update re-enqueues this reconciler anyway.

func NewWorkspaceNotifier added in v0.2.34

func NewWorkspaceNotifier(users dynamic.Interface, logger klog.Logger) *WorkspaceNotifier

NewWorkspaceNotifier returns a notifier over the Organizations that users targets (the root:railgrid:users workspace).

func (*WorkspaceNotifier) Notify added in v0.2.34

func (n *WorkspaceNotifier) Notify(ctx context.Context, orgUUID string)

Notify records that orgUUID's child workspaces changed. It runs synchronously in the caller (a request handler or the cascade sweep) and makes one kcp patch bounded by ctx; it never returns an error, so the caller's own work is never failed by it. Safe on a nil notifier (a hub without metering). An Organization that is already gone has nothing to report into and is skipped.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer performs the admin writes against the metering workspace.

func NewWriter

func NewWriter(client dynamic.Interface, defaultPlan string, caller func(context.Context) string, logger logr.Logger) *Writer

NewWriter builds a Writer. caller returns the admin identity of a request for the audit line; defaultPlan is the plan the hub puts new organizations on, which therefore cannot be archived.

func (*Writer) ArchivePlan added in v0.2.42

func (wr *Writer) ArchivePlan(ctx context.Context, name string) error

ArchivePlan retains plan name and its limits for existing entitlements while preventing new assignments. Repeating the operation is a no-op. The default plan cannot be archived because new organizations depend on it.

func (*Writer) PutPlan

func (wr *Writer) PutPlan(ctx context.Context, name string, body PlanUpdate) (*unstructured.Unstructured, bool, error)

PutPlan creates or updates plan name. On update everything but displayName and limits (the backend meter mapping) is preserved.

func (*Writer) UpdateEntitlement

func (wr *Writer) UpdateEntitlement(ctx context.Context, name string, body EntitlementUpdate) (*unstructured.Unstructured, error)

UpdateEntitlement moves entitlement name to body.Plan with body.Overrides.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL